Version 1.5.0-pre2:

* Add per-player restock cooldowns
* Finished commenting code

TO-DO:
* Add enchantment, custom model data, names, and lores to ingredient & result settings?
* GUI Editor
This commit is contained in:
PretzelJohn 2022-01-11 04:22:52 -05:00
parent 5110befd30
commit e708187b33
15 changed files with 590 additions and 100 deletions

12
pom.xml
View File

@ -43,6 +43,18 @@
<version>1.18.1-R0.1-SNAPSHOT</version> <version>1.18.1-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.36.0.3</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>de.tr7zw</groupId> <groupId>de.tr7zw</groupId>
<artifactId>functional-annotations</artifactId> <artifactId>functional-annotations</artifactId>

View File

@ -2,11 +2,15 @@ package com.pretzel.dev.villagertradelimiter;
import com.pretzel.dev.villagertradelimiter.commands.CommandManager; import com.pretzel.dev.villagertradelimiter.commands.CommandManager;
import com.pretzel.dev.villagertradelimiter.commands.CommandBase; import com.pretzel.dev.villagertradelimiter.commands.CommandBase;
import com.pretzel.dev.villagertradelimiter.data.PlayerData;
import com.pretzel.dev.villagertradelimiter.database.DatabaseManager;
import com.pretzel.dev.villagertradelimiter.listeners.InventoryListener;
import com.pretzel.dev.villagertradelimiter.settings.ConfigUpdater; import com.pretzel.dev.villagertradelimiter.settings.ConfigUpdater;
import com.pretzel.dev.villagertradelimiter.lib.Metrics; import com.pretzel.dev.villagertradelimiter.lib.Metrics;
import com.pretzel.dev.villagertradelimiter.lib.Util; import com.pretzel.dev.villagertradelimiter.lib.Util;
import com.pretzel.dev.villagertradelimiter.listeners.PlayerListener; import com.pretzel.dev.villagertradelimiter.listeners.PlayerListener;
import com.pretzel.dev.villagertradelimiter.settings.Lang; import com.pretzel.dev.villagertradelimiter.settings.Lang;
import com.pretzel.dev.villagertradelimiter.settings.Settings;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
@ -25,13 +29,16 @@ public class VillagerTradeLimiter extends JavaPlugin {
private FileConfiguration cfg; private FileConfiguration cfg;
private Lang lang; private Lang lang;
private CommandManager commandManager; private CommandManager commandManager;
private DatabaseManager databaseManager;
private PlayerListener playerListener; private PlayerListener playerListener;
private HashMap<UUID, PlayerData> playerData;
//Initial plugin load/unload /** Initial plugin load/unload */
public void onEnable() { public void onEnable() {
//Initialize instance variables //Initialize instance variables
this.cfg = null; this.cfg = null;
this.commandManager = new CommandManager(this); this.commandManager = new CommandManager(this);
this.playerData = new HashMap<>();
//Copy default settings & load settings //Copy default settings & load settings
this.getConfig().options().copyDefaults(); this.getConfig().options().copyDefaults();
@ -40,7 +47,6 @@ public class VillagerTradeLimiter extends JavaPlugin {
this.loadBStats(); this.loadBStats();
//Register commands and listeners //Register commands and listeners
this.playerListener = new PlayerListener(this);
this.registerCommands(); this.registerCommands();
this.registerListeners(); this.registerListeners();
@ -48,7 +54,15 @@ public class VillagerTradeLimiter extends JavaPlugin {
Util.consoleMsg(PREFIX+PLUGIN_NAME+" is running!"); Util.consoleMsg(PREFIX+PLUGIN_NAME+" is running!");
} }
//Loads or reloads config.yml and messages.yml /** Save database on plugin stop, server stop */
public void onDisable() {
for(UUID uuid : playerData.keySet()) {
this.databaseManager.savePlayer(uuid, false);
}
this.playerData.clear();
}
/** Loads or reloads config.yml and messages.yml */
public void loadSettings() { public void loadSettings() {
final String mainPath = this.getDataFolder().getPath()+"/"; final String mainPath = this.getDataFolder().getPath()+"/";
final File file = new File(mainPath, "config.yml"); final File file = new File(mainPath, "config.yml");
@ -59,35 +73,45 @@ public class VillagerTradeLimiter extends JavaPlugin {
} }
this.cfg = YamlConfiguration.loadConfiguration(file); this.cfg = YamlConfiguration.loadConfiguration(file);
this.lang = new Lang(this, this.getTextResource("messages.yml"), mainPath); this.lang = new Lang(this, this.getTextResource("messages.yml"), mainPath);
//Load/reload database manager
if(this.databaseManager == null) this.databaseManager = new DatabaseManager(this);
this.databaseManager.load();
} }
//Load and initialize the bStats class with the plugin id /** Load and initialize the bStats class with the plugin id */
private void loadBStats() { private void loadBStats() {
if(this.cfg.getBoolean("bStats", true)) { if(this.cfg.getBoolean("bStats", true)) {
new Metrics(this, BSTATS_ID); new Metrics(this, BSTATS_ID);
} }
} }
//Registers plugin commands /** Registers plugin commands */
private void registerCommands() { private void registerCommands() {
final CommandBase cmd = this.commandManager.getCommands(); final CommandBase cmd = this.commandManager.getCommands();
this.getCommand("villagertradelimiter").setExecutor(cmd); this.getCommand("villagertradelimiter").setExecutor(cmd);
this.getCommand("villagertradelimiter").setTabCompleter(cmd); this.getCommand("villagertradelimiter").setTabCompleter(cmd);
} }
//Registers plugin listeners /** Registers plugin listeners */
private void registerListeners() { private void registerListeners() {
final Settings settings = new Settings(this);
this.playerListener = new PlayerListener(this, settings);
this.getServer().getPluginManager().registerEvents(this.playerListener, this); this.getServer().getPluginManager().registerEvents(this.playerListener, this);
this.getServer().getPluginManager().registerEvents(new InventoryListener(this, settings), this);
} }
// ------------------------- Getters ------------------------- // ------------------------- Getters -------------------------
//Returns the settings from config.yml /** Returns the settings from config.yml */
public FileConfiguration getCfg() { return this.cfg; } public FileConfiguration getCfg() { return this.cfg; }
//Returns a language setting from messages.yml /** Returns a language setting from messages.yml */
public String getLang(final String path) { return this.lang.get(path); } public String getLang(final String path) { return this.lang.get(path); }
//Returns this plugin's player listener /** Returns this plugin's player listener */
public PlayerListener getPlayerListener() { return this.playerListener; } public PlayerListener getPlayerListener() { return this.playerListener; }
/** Returns a player's data container */
public HashMap<UUID, PlayerData> getPlayerData() { return this.playerData; }
} }

View File

@ -0,0 +1,33 @@
package com.pretzel.dev.villagertradelimiter.data;
import com.pretzel.dev.villagertradelimiter.lib.Util;
public class Cooldown {
private enum Interval {
s(1000L),
m(60000L),
h(3600000L),
d(86400000L),
w(604800000L);
final long factor;
Interval(long factor) {
this.factor = factor;
}
}
/**
* @param timeStr The cooldown time as written in config.yml (7d, 30s, 5m, etc)
* @return The cooldown time in milliseconds
*/
public static long parseTime(final String timeStr) {
try {
long time = Long.parseLong(timeStr.substring(0, timeStr.length()-1));
String interval = timeStr.substring(timeStr.length()-1).toLowerCase();
return time * Interval.valueOf(interval).factor;
} catch (Exception e) {
Util.errorMsg(e);
}
return 0;
}
}

View File

@ -1,22 +1,23 @@
package com.pretzel.dev.villagertradelimiter.data; package com.pretzel.dev.villagertradelimiter.data;
import com.pretzel.dev.villagertradelimiter.wrappers.VillagerWrapper; import com.pretzel.dev.villagertradelimiter.wrappers.VillagerWrapper;
import org.bukkit.entity.Player;
import java.util.HashMap;
public class PlayerData { public class PlayerData {
private final Player player; private final HashMap<String, Long> tradingCooldowns;
private VillagerWrapper tradingVillager; private VillagerWrapper tradingVillager;
public PlayerData(final Player player) { public PlayerData() {
this.player = player; this.tradingCooldowns = new HashMap<>();
this.tradingVillager = null; this.tradingVillager = null;
} }
/** @param tradingVillager The villager that this player is currently trading with */ /** @return The map of items to timestamps for the player's trading history */
public void setTradingVillager(VillagerWrapper tradingVillager) { this.tradingVillager = tradingVillager; } public HashMap<String, Long> getTradingCooldowns() { return this.tradingCooldowns; }
/** @return The player that this data is for */ /** @param tradingVillager The villager that this player is currently trading with */
public Player getPlayer() { return this.player; } public void setTradingVillager(final VillagerWrapper tradingVillager) { this.tradingVillager = tradingVillager; }
/** @return The villager that this player is currently trading with */ /** @return The villager that this player is currently trading with */
public VillagerWrapper getTradingVillager() { return this.tradingVillager; } public VillagerWrapper getTradingVillager() { return this.tradingVillager; }

View File

@ -0,0 +1,65 @@
package com.pretzel.dev.villagertradelimiter.database;
import com.pretzel.dev.villagertradelimiter.lib.Callback;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
public abstract class Database {
protected final JavaPlugin instance;
public Database(final JavaPlugin instance) {
this.instance = instance;
}
//Tests a DataSource
public void test() {
try {
try (Connection conn = this.getSource().getConnection()) {
if (!conn.isValid(1000)) throw new SQLException("Could not connect to database!");
else Util.consoleMsg("Connected to database!");
}
} catch (SQLException e) {
Util.consoleMsg("Could not connect to database!");
}
}
//Executes a statement or query in the database
public ArrayList<String> execute(final String sql, boolean query) {
try(Connection conn = this.getSource().getConnection(); PreparedStatement statement = conn.prepareStatement(sql)) {
if(query) {
final ResultSet result = statement.executeQuery();
int columns = result.getMetaData().getColumnCount();
final ArrayList<String> res = new ArrayList<>();
while(result.next()){
String row = "";
for(int j = 0; j < columns; j++)
row += result.getString(j+1)+(j < columns-1?",":"");
res.add(row);
}
return res;
} else statement.execute();
} catch (SQLException e) {
Util.errorMsg(e);
}
return null;
}
public void execute(final String sql, boolean query, final Callback<ArrayList<String>> callback) {
Bukkit.getScheduler().runTaskAsynchronously(this.instance, () -> {
final ArrayList<String> result = execute(sql, query);
if(callback != null) Bukkit.getScheduler().runTask(this.instance, () -> callback.call(result));
});
}
public abstract void load(final ConfigurationSection cfg);
public abstract boolean isMySQL();
protected abstract DataSource getSource();
}

View File

@ -0,0 +1,96 @@
package com.pretzel.dev.villagertradelimiter.database;
import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
import com.pretzel.dev.villagertradelimiter.data.Cooldown;
import com.pretzel.dev.villagertradelimiter.data.PlayerData;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import org.bukkit.configuration.ConfigurationSection;
import java.util.UUID;
public class DatabaseManager {
private static final String CREATE_TABLE_COOLDOWN =
"CREATE TABLE IF NOT EXISTS vtl_cooldown("+
"uuid CHAR(36) NOT NULL,"+
"item VARCHAR(255) NOT NULL,"+
"time BIGINT NOT NULL,"+
"PRIMARY KEY(uuid, item));";
private static final String SELECT_ITEMS = "SELECT * FROM vtl_cooldown;";
private static final String INSERT_ITEM = "INSERT OR IGNORE INTO vtl_cooldown(uuid,item,time) VALUES?;"; //INSERT IGNORE INTO for MySQL
private static final String DELETE_ITEMS = "DELETE FROM vtl_cooldown WHERE uuid='?';";
private final VillagerTradeLimiter instance;
private Database database;
public DatabaseManager(final VillagerTradeLimiter instance) {
this.instance = instance;
}
public void load() {
final ConfigurationSection cfg = instance.getCfg().getConfigurationSection("database");
if(cfg == null) {
Util.consoleMsg("Database settings missing from config.yml!");
this.database = null;
return;
}
boolean mysql = cfg.getBoolean("mysql", false);
if(this.database != null && ((mysql && this.database.isMySQL()) || (!mysql && !this.database.isMySQL()))) this.database.load(cfg);
else this.database = (mysql?new MySQL(instance, cfg):new SQLite(instance));
this.database.execute(CREATE_TABLE_COOLDOWN, false);
//Loads all the data
this.database.execute(SELECT_ITEMS, true, (result,args) -> {
if(result != null) {
for(String row : result) {
final String[] tokens = row.split(",");
UUID uuid = UUID.fromString(tokens[0]);
String item = tokens[1];
long time = Long.parseLong(tokens[2]);
PlayerData playerData = instance.getPlayerData().get(uuid);
if(playerData == null) {
playerData = new PlayerData();
instance.getPlayerData().put(uuid, playerData);
}
String cooldown = instance.getCfg().getString("Overrides."+item+".Cooldown", null);
if(cooldown != null && System.currentTimeMillis() < time + Cooldown.parseTime(cooldown)) {
playerData.getTradingCooldowns().put(item, time);
}
}
}
});
}
public void savePlayer(final UUID uuid, boolean async) {
if(this.database == null) return;
//Delete existing rows for player
final String uuidStr = uuid.toString();
if(async) this.database.execute(DELETE_ITEMS.replace("?", uuidStr), false, (result,args) -> save(uuid, true));
else {
this.database.execute(DELETE_ITEMS.replace("?", uuidStr), false);
save(uuid, false);
}
}
private void save(final UUID uuid, boolean async) {
//Insert new rows for player pages
final PlayerData playerData = instance.getPlayerData().get(uuid);
if(playerData == null) return;
String values = "";
for(String item : playerData.getTradingCooldowns().keySet()) {
long time = playerData.getTradingCooldowns().get(item);
if(!values.isEmpty()) values += ",";
values += "('"+uuid+"','"+item+"','"+time+"')";
}
if(values.isEmpty()) return;
String sql = INSERT_ITEM.replace("?", values);
if(this.database.isMySQL()) sql = sql.replace(" OR ", " ");
if(async) this.database.execute(sql, false, (result,args) -> {});
else this.database.execute(sql, false);
}
}

View File

@ -0,0 +1,35 @@
package com.pretzel.dev.villagertradelimiter.database;
import com.mysql.cj.jdbc.MysqlConnectionPoolDataSource;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import javax.sql.DataSource;
import java.sql.SQLException;
public class MySQL extends Database {
private final MysqlConnectionPoolDataSource source;
public MySQL(final JavaPlugin instance, final ConfigurationSection cfg) {
super(instance);
this.source = new MysqlConnectionPoolDataSource();
this.load(cfg);
}
public void load(final ConfigurationSection cfg) {
this.source.setServerName(cfg.getString("host", "localhost"));
this.source.setPort(cfg.getInt("port", 3306));
this.source.setDatabaseName(cfg.getString("database", "sagas_holo"));
this.source.setUser(cfg.getString("username", "root"));
this.source.setPassword(cfg.getString("password", "root"));
try {
this.source.setCharacterEncoding(cfg.getString("encoding", "utf8"));
this.source.setUseSSL(cfg.getBoolean("useSSL", false));
} catch (SQLException e) {}
this.test();
}
public boolean isMySQL() { return true; }
public DataSource getSource() { return this.source; }
}

View File

@ -0,0 +1,25 @@
package com.pretzel.dev.villagertradelimiter.database;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import org.sqlite.javax.SQLiteConnectionPoolDataSource;
import javax.sql.DataSource;
public class SQLite extends Database {
private final SQLiteConnectionPoolDataSource source;
public SQLite(final JavaPlugin instance) {
super(instance);
this.source = new SQLiteConnectionPoolDataSource();
this.load(null);
}
public void load(final ConfigurationSection cfg) {
this.source.setUrl("jdbc:sqlite:"+instance.getDataFolder().getPath()+"/database.db");
this.test();
}
public boolean isMySQL() { return false; }
public DataSource getSource() { return this.source; }
}

View File

@ -6,5 +6,5 @@ public interface Callback<T> {
* @param result Any type of result to be passed into the callback function * @param result Any type of result to be passed into the callback function
* @param args Any extra arguments to be passed into the callback function * @param args Any extra arguments to be passed into the callback function
*/ */
void call(T result, String[] args); void call(T result, String... args);
} }

View File

@ -14,28 +14,49 @@ import org.bukkit.entity.Villager;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
public class Util { public class Util {
//Sends a message to the sender of a command /**
public static void sendMsg(String msg, Player p) { * Sends a message to the sender of a command
if(p == null) consoleMsg(msg); * @param msg The message to send
else p.sendMessage(msg); * @param player The player (or console) to sent the message to
*/
public static void sendMsg(String msg, Player player) {
if(player == null) consoleMsg(msg);
else player.sendMessage(msg);
} }
//Sends a message to a player if they have permission //Sends a message to a player if they have permission
/**
* Sends a message to a player if they have permission
* @param perm The name of the permission to check
* @param msg The message to send
* @param player The player (or console) to sent the message to
*/
public static void sendIfPermitted(String perm, String msg, Player player) { public static void sendIfPermitted(String perm, String msg, Player player) {
if(player.hasPermission(perm)) player.sendMessage(msg); if(player.hasPermission(perm)) player.sendMessage(msg);
} }
//Sends a message to the console /**
* Sends a message to the console
* @param msg The message to send
*/
public static void consoleMsg(String msg) { public static void consoleMsg(String msg) {
if(msg != null) Bukkit.getServer().getConsoleSender().sendMessage(msg); if(msg != null) Bukkit.getServer().getConsoleSender().sendMessage(msg);
} }
/**
* Replaces the color tags to bukkit color tags
* @param in The string to replace color tags for
* @return The string with replaced colors
*/
public static String replaceColors(String in) { public static String replaceColors(String in) {
if(in == null) return null; if(in == null) return null;
return in.replace("&", "\u00A7"); return in.replace("&", "\u00A7");
} }
//Sends an error message to the console /**
* Sends an error message to the console
* @param e The error to send a message for
*/
public static void errorMsg(Exception e) { public static void errorMsg(Exception e) {
String error = e.toString(); String error = e.toString();
for(StackTraceElement x : e.getStackTrace()) { for(StackTraceElement x : e.getStackTrace()) {
@ -44,26 +65,47 @@ public class Util {
consoleMsg(ChatColor.RED+"ERROR: "+error); consoleMsg(ChatColor.RED+"ERROR: "+error);
} }
//Returns whether a player is a Citizens NPC or not /**
* Checks whether a player is a Citizens NPC or not
* @param player The player to check
* @return True if the player is an NPC, false otherwise
*/
public static boolean isNPC(Player player) { public static boolean isNPC(Player player) {
return player.hasMetadata("NPC"); return player.hasMetadata("NPC");
} }
//Returns whether a villager is a Citizens NPC or not /**
* Returns whether a villager is a Citizens NPC or not
* @param villager The villager to check
* @return True if the villager is an NPC, false otherwise
*/
public static boolean isNPC(Villager villager) { public static boolean isNPC(Villager villager) {
return villager.hasMetadata("NPC"); return villager.hasMetadata("NPC");
} }
//Converts an int array to a string /**
public static String intArrayToString(int[] arr) { * Combines the elements of an int[] into a string
* @param arr The int[] to combine
* @param separator (optional) The string to place between elements of the int[]
* @return The combined string of the int[]
*/
public static String intArrayToString(int[] arr, String separator) {
String res = ""; String res = "";
for(int a : arr) { res += a+""; } for(int a : arr) { res += a+separator; }
return res; return res;
} }
public static String intArrayToString(int[] arr) {
return intArrayToString(arr, "");
}
/**
* Reads the lines of a file into a String[]
* @param reader The file reader
* @return The lines of the file, as a String[]
*/
public static String[] readFile(Reader reader) { public static String[] readFile(Reader reader) {
String out = ""; String out = "";
BufferedReader br = null; BufferedReader br;
try { try {
br = new BufferedReader(reader); br = new BufferedReader(reader);
String line; String line;
@ -76,6 +118,12 @@ public class Util {
} }
return null; return null;
} }
/**
* Reads the lines of a file into a String[]
* @param file The file
* @return The lines of the file, as a String[]
*/
public static String[] readFile(File file) { public static String[] readFile(File file) {
try { try {
return readFile(new FileReader(file)); return readFile(new FileReader(file));
@ -85,8 +133,13 @@ public class Util {
} }
} }
/**
* Writes a String to a file
* @param file The file to write the String to
* @param out The String to write to the file
*/
public static void writeFile(File file, String out) { public static void writeFile(File file, String out) {
BufferedWriter bw = null; BufferedWriter bw;
try { try {
bw = new BufferedWriter(new FileWriter(file)); bw = new BufferedWriter(new FileWriter(file));
bw.write(out); bw.write(out);

View File

@ -0,0 +1,114 @@
package com.pretzel.dev.villagertradelimiter.listeners;
import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
import com.pretzel.dev.villagertradelimiter.data.PlayerData;
import com.pretzel.dev.villagertradelimiter.lib.Util;
import com.pretzel.dev.villagertradelimiter.settings.Settings;
import com.pretzel.dev.villagertradelimiter.wrappers.VillagerWrapper;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.MerchantRecipe;
public class InventoryListener implements Listener {
private final VillagerTradeLimiter instance;
private final Settings settings;
/**
* @param instance The instance of VillagerTradeLimiter.java
* @param settings The settings instance
*/
public InventoryListener(final VillagerTradeLimiter instance, final Settings settings) {
this.instance = instance;
this.settings = settings;
}
/** Handles when a player stops trading with a villager */
@EventHandler
public void onPlayerStopTrading(final InventoryCloseEvent event) {
//Don't do anything unless the player is actually finished trading with a villager
if(event.getInventory().getType() != InventoryType.MERCHANT) return;
if(!(event.getInventory().getHolder() instanceof Villager)) return;
if(!(event.getPlayer() instanceof Player)) return;
final Player player = (Player)event.getPlayer();
if(Util.isNPC(player)) return;
//Reset the villager's NBT data when a player is finished trading
final PlayerData playerData = instance.getPlayerData().get(player.getUniqueId());
if(playerData == null) return;
final VillagerWrapper villager = playerData.getTradingVillager();
if(villager == null) return;
playerData.setTradingVillager(null);
villager.reset();
}
/** Handles when a player successfully trades with a villager */
@EventHandler
public void onPlayerMakeTrade(final InventoryClickEvent event) {
if(event.getInventory().getType() != InventoryType.MERCHANT) return;
if(!(event.getInventory().getHolder() instanceof Villager)) return;
if(!(event.getWhoClicked() instanceof Player)) return;
if(event.getRawSlot() != 2) return;
final Player player = (Player)event.getWhoClicked();
if(Util.isNPC(player)) return;
//Get the items involved in the trade
final ItemStack result = event.getCurrentItem();
ItemStack ingredient1 = event.getInventory().getItem(0);
ItemStack ingredient2 = event.getInventory().getItem(1);
if(result == null || result.getType() == Material.AIR) return;
if(ingredient1 == null) ingredient1 = new ItemStack(Material.AIR, 1);
if(ingredient2 == null) ingredient2 = new ItemStack(Material.AIR, 1);
//Check if there is a cooldown set for the trade
final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides");
if(overrides == null) return;
final String type = settings.getType(result, ingredient1, ingredient2);
if(type == null || !overrides.contains(type+".Cooldown")) return;
//Get the selected recipe by the items in the slots
final MerchantRecipe selectedRecipe = getSelectedRecipe((Villager)event.getInventory().getHolder(), ingredient1, ingredient2, result);
if(selectedRecipe == null) {
event.setCancelled(true);
return;
}
//Add a cooldown to the trade if the player has reached the max uses
final PlayerData playerData = instance.getPlayerData().get(player.getUniqueId());
if(playerData == null || playerData.getTradingVillager() == null) return;
Bukkit.getScheduler().runTaskLater(instance, () -> {
int uses = selectedRecipe.getUses();
if(!playerData.getTradingCooldowns().containsKey(type) && uses >= selectedRecipe.getMaxUses()) {
playerData.getTradingCooldowns().put(type, System.currentTimeMillis());
}
}, 1);
}
/**
* @param villager The villager to get the recipe from
* @param ingredient1 The item in the first ingredient slot of the trade interface
* @param ingredient2 The item in the second ingredient slot of the trade interface
* @param result The item in the result slot of the trade interface
* @return The villager's recipe that matches the items in the slots
*/
private MerchantRecipe getSelectedRecipe(final Villager villager, final ItemStack ingredient1, final ItemStack ingredient2, final ItemStack result) {
for(MerchantRecipe recipe : villager.getRecipes()) {
final ItemStack item1 = recipe.getIngredients().get(0);
final ItemStack item2 = recipe.getIngredients().get(1);
if(!recipe.getResult().isSimilar(result)) continue;
if((item1.isSimilar(ingredient1) && item2.isSimilar(ingredient2)) || (item1.isSimilar(ingredient2) && item2.isSimilar(ingredient1)))
return recipe;
}
return null;
}
}

View File

@ -1,6 +1,7 @@
package com.pretzel.dev.villagertradelimiter.listeners; package com.pretzel.dev.villagertradelimiter.listeners;
import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter; import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
import com.pretzel.dev.villagertradelimiter.data.Cooldown;
import com.pretzel.dev.villagertradelimiter.data.PlayerData; import com.pretzel.dev.villagertradelimiter.data.PlayerData;
import com.pretzel.dev.villagertradelimiter.lib.Util; import com.pretzel.dev.villagertradelimiter.lib.Util;
import com.pretzel.dev.villagertradelimiter.settings.Settings; import com.pretzel.dev.villagertradelimiter.settings.Settings;
@ -11,31 +12,28 @@ import org.bukkit.entity.Player;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryPickupItemEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionEffectType;
import java.util.HashMap;
import java.util.List; import java.util.List;
public class PlayerListener implements Listener { public class PlayerListener implements Listener {
private final VillagerTradeLimiter instance; private final VillagerTradeLimiter instance;
private final Settings settings; private final Settings settings;
private final HashMap<Player, PlayerData> playerData;
/** @param instance The instance of VillagerTradeLimiter.java */ /**
public PlayerListener(VillagerTradeLimiter instance) { * @param instance The instance of VillagerTradeLimiter.java
* @param settings The settings instance
*/
public PlayerListener(final VillagerTradeLimiter instance, final Settings settings) {
this.instance = instance; this.instance = instance;
this.settings = new Settings(instance); this.settings = settings;
this.playerData = new HashMap<>();
} }
/** Handles when a player begins trading with a villager */ /** Handles when a player begins trading with a villager */
@EventHandler @EventHandler
public void onPlayerBeginTrading(PlayerInteractEntityEvent event) { public void onPlayerBeginTrading(final PlayerInteractEntityEvent event) {
if(!(event.getRightClicked() instanceof Villager)) return; if(!(event.getRightClicked() instanceof Villager)) return;
final Villager villager = (Villager)event.getRightClicked(); final Villager villager = (Villager)event.getRightClicked();
if(Util.isNPC(villager)) return; //Skips NPCs if(Util.isNPC(villager)) return; //Skips NPCs
@ -44,11 +42,13 @@ public class PlayerListener implements Listener {
//DisableTrading feature //DisableTrading feature
if(instance.getCfg().isBoolean("DisableTrading")) { if(instance.getCfg().isBoolean("DisableTrading")) {
//If all trading is disabled
if(instance.getCfg().getBoolean("DisableTrading", false)) { if(instance.getCfg().getBoolean("DisableTrading", false)) {
event.setCancelled(true); event.setCancelled(true);
return; return;
} }
} else { } else {
//If trading in the world the player is in is disabled
final List<String> disabledWorlds = instance.getCfg().getStringList("DisableTrading"); final List<String> disabledWorlds = instance.getCfg().getStringList("DisableTrading");
final String world = event.getPlayer().getWorld().getName(); final String world = event.getPlayer().getWorld().getName();
for(String disabledWorld : disabledWorlds) { for(String disabledWorld : disabledWorlds) {
@ -59,28 +59,15 @@ public class PlayerListener implements Listener {
} }
} }
//Cancel the original event, and open the adjusted trade view
event.setCancelled(true); event.setCancelled(true);
final Player player = event.getPlayer(); final Player player = event.getPlayer();
if(!instance.getPlayerData().containsKey(player.getUniqueId())) {
instance.getPlayerData().put(player.getUniqueId(), new PlayerData());
}
this.see(villager, player, player); this.see(villager, player, player);
} }
/** Handles when a player stops trading with a villager */
@EventHandler
public void onPlayerStopTrading(final InventoryCloseEvent event) {
//Don't do anything unless the player is actually finished trading with a villager
if(event.getInventory().getType() != InventoryType.MERCHANT) return;
if(!(event.getPlayer() instanceof Player)) return;
final Player player = (Player)event.getPlayer();
if(Util.isNPC(player)) return;
if(getPlayerData(player).getTradingVillager() == null) return;
//Reset the villager's NBT data when a player is finished trading
final VillagerWrapper villager = playerData.get(player).getTradingVillager();
getPlayerData(player).setTradingVillager(null);
if(villager == null) return;
villager.reset();
}
/** /**
* Opens the villager's trading menu, with the adjusted trades of another player (or the same player) * Opens the villager's trading menu, with the adjusted trades of another player (or the same player)
* @param villager The villager whose trades you want to see * @param villager The villager whose trades you want to see
@ -93,6 +80,9 @@ public class PlayerListener implements Listener {
final PlayerWrapper otherWrapper = new PlayerWrapper(other); final PlayerWrapper otherWrapper = new PlayerWrapper(other);
if(Util.isNPC(villager) || Util.isNPC(player) || otherWrapper.isNPC()) return; //Skips NPCs if(Util.isNPC(villager) || Util.isNPC(player) || otherWrapper.isNPC()) return; //Skips NPCs
final PlayerData playerData = instance.getPlayerData().get(other.getUniqueId());
if(playerData != null) playerData.setTradingVillager(villagerWrapper);
//Checks if the version is old, before the 1.16 UUID changes //Checks if the version is old, before the 1.16 UUID changes
String version = instance.getServer().getClass().getPackage().getName(); String version = instance.getServer().getClass().getPackage().getName();
boolean isOld = version.contains("1_13_") || version.contains("1_14_") || version.contains("1_15_"); boolean isOld = version.contains("1_13_") || version.contains("1_14_") || version.contains("1_15_");
@ -108,7 +98,7 @@ public class PlayerListener implements Listener {
recipe.setSpecialPrice(getDiscount(recipe, totalReputation, hotvDiscount)); recipe.setSpecialPrice(getDiscount(recipe, totalReputation, hotvDiscount));
//Set ingredient materials and amounts //Set ingredient materials and amounts
final ConfigurationSection override = settings.getOverride(recipe); final ConfigurationSection override = settings.getOverride(recipe.getItemStack("buy"), recipe.getItemStack("sell"));
if(override != null) { if(override != null) {
setIngredient(override.getConfigurationSection("Item1"), recipe.getIngredient1()); setIngredient(override.getConfigurationSection("Item1"), recipe.getIngredient1());
setIngredient(override.getConfigurationSection("Item2"), recipe.getIngredient2()); setIngredient(override.getConfigurationSection("Item2"), recipe.getIngredient2());
@ -116,19 +106,13 @@ public class PlayerListener implements Listener {
} }
//Set the maximum number of uses (trades/day) //Set the maximum number of uses (trades/day)
recipe.setMaxUses(getMaxUses(recipe)); recipe.setMaxUses(getMaxUses(recipe, other));
} }
//Open the villager's trading menu //Open the villager's trading menu
getPlayerData(player).setTradingVillager(villagerWrapper);
player.openMerchant(villager, false); player.openMerchant(villager, false);
} }
@EventHandler
public void onPickupItem(InventoryPickupItemEvent event) {
Util.consoleMsg("Picked up!");
}
/** /**
* @param recipe The recipe to get the base price for * @param recipe The recipe to get the base price for
* @return The initial price of a recipe/trade, before any discounts are applied * @return The initial price of a recipe/trade, before any discounts are applied
@ -178,11 +162,28 @@ public class PlayerListener implements Listener {
* @param recipe The recipe to get the MaxUses for * @param recipe The recipe to get the MaxUses for
* @return The current maximum number of times a player can make a trade before the villager restocks * @return The current maximum number of times a player can make a trade before the villager restocks
*/ */
private int getMaxUses(final RecipeWrapper recipe) { private int getMaxUses(final RecipeWrapper recipe, final OfflinePlayer player) {
int uses = recipe.getMaxUses(); int uses = recipe.getMaxUses();
int maxUses = settings.fetchInt(recipe, "MaxUses", -1); int maxUses = settings.fetchInt(recipe, "MaxUses", -1);
boolean disabled = settings.fetchBoolean(recipe, "Disabled", false); boolean disabled = settings.fetchBoolean(recipe, "Disabled", false);
final PlayerData playerData = instance.getPlayerData().get(player.getUniqueId());
if(playerData != null && playerData.getTradingVillager() != null) {
final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides");
if(overrides != null) {
final String type = settings.getType(recipe.getItemStack("sell"), recipe.getItemStack("buy"), recipe.getItemStack("buyB"));
if(type != null && overrides.contains(type+".Cooldown")) {
if(playerData.getTradingCooldowns().containsKey(type)) {
if(System.currentTimeMillis() >= playerData.getTradingCooldowns().get(type) + Cooldown.parseTime(overrides.getString(type+".Cooldown"))) {
playerData.getTradingCooldowns().remove(type);
} else {
maxUses = 0;
}
}
}
}
}
if(maxUses < 0) maxUses = uses; if(maxUses < 0) maxUses = uses;
if(disabled) maxUses = 0; if(disabled) maxUses = 0;
return maxUses; return maxUses;
@ -220,15 +221,4 @@ public class PlayerListener implements Listener {
ingredient.setMaterialId("minecraft:"+item.getString("Material", ingredient.getMaterialId()).replace("minecraft:","")); ingredient.setMaterialId("minecraft:"+item.getString("Material", ingredient.getMaterialId()).replace("minecraft:",""));
ingredient.setAmount(item.getInt("Amount", ingredient.getAmount())); ingredient.setAmount(item.getInt("Amount", ingredient.getAmount()));
} }
/**
* @param player The player to get the data container for
* @return The data container for the given player
*/
private PlayerData getPlayerData(final Player player) {
if(playerData.containsKey(player) && playerData.get(player) != null) return playerData.get(player);
final PlayerData pd = new PlayerData(player);
playerData.put(player, pd);
return pd;
}
} }

View File

@ -25,7 +25,7 @@ public class Settings {
*/ */
public boolean fetchBoolean(final RecipeWrapper recipe, String key, boolean defaultValue) { public boolean fetchBoolean(final RecipeWrapper recipe, String key, boolean defaultValue) {
boolean global = instance.getCfg().getBoolean(key, defaultValue); boolean global = instance.getCfg().getBoolean(key, defaultValue);
final ConfigurationSection override = getOverride(recipe); final ConfigurationSection override = getOverride(recipe.getItemStack("buy"), recipe.getItemStack("sell"));
if(override != null) return override.getBoolean(key, global); if(override != null) return override.getBoolean(key, global);
return global; return global;
} }
@ -38,7 +38,7 @@ public class Settings {
*/ */
public int fetchInt(final RecipeWrapper recipe, String key, int defaultValue) { public int fetchInt(final RecipeWrapper recipe, String key, int defaultValue) {
int global = instance.getCfg().getInt(key, defaultValue); int global = instance.getCfg().getInt(key, defaultValue);
final ConfigurationSection override = getOverride(recipe); final ConfigurationSection override = getOverride(recipe.getItemStack("buy"), recipe.getItemStack("sell"));
if(override != null) return override.getInt(key, global); if(override != null) return override.getInt(key, global);
return global; return global;
} }
@ -51,20 +51,51 @@ public class Settings {
*/ */
public double fetchDouble(final RecipeWrapper recipe, String key, double defaultValue) { public double fetchDouble(final RecipeWrapper recipe, String key, double defaultValue) {
double global = instance.getCfg().getDouble(key, defaultValue); double global = instance.getCfg().getDouble(key, defaultValue);
final ConfigurationSection override = getOverride(recipe); final ConfigurationSection override = getOverride(recipe.getItemStack("buy"), recipe.getItemStack("sell"));
if(override != null) return override.getDouble(key, global); if(override != null) return override.getDouble(key, global);
return global; return global;
} }
/** /**
* @param recipe The wrapped recipe to fetch any overrides for * @param result The itemstack for the recipe's result
* @param ingredient1 The itemstack for the recipe's first ingredient
* @param ingredient2 The itemstack for the recipe's second ingredient
* @return The matched type of the item, if any
*/
public String getType(final ItemStack result, final ItemStack ingredient1, final ItemStack ingredient2) {
final String resultType = result.getType().name().toLowerCase();
final String ingredient1Type = ingredient1.getType().name().toLowerCase();
final String ingredient2Type = ingredient2.getType().name().toLowerCase();
if(result.getType() == Material.ENCHANTED_BOOK) {
final EnchantmentStorageMeta meta = (EnchantmentStorageMeta) result.getItemMeta();
if(meta == null) return null;
for(Enchantment key : meta.getStoredEnchants().keySet()) {
if (key != null) {
final String itemType = key.getKey().getKey() +"_"+meta.getStoredEnchantLevel(key);
if(getItem(ingredient1, result, itemType) != null) return itemType;
}
}
return null;
}
final ItemStack ingredient = (ingredient1.getType() == Material.AIR ? ingredient2 : ingredient1);
if(getItem(ingredient, result, resultType) != null) return resultType;
if(getItem(ingredient, result, ingredient1Type) != null) return ingredient1Type;
if(getItem(ingredient, result, ingredient2Type) != null) return ingredient2Type;
return null;
}
/**
* @param buy The first ingredient of the recipe
* @param sell The result of the recipe
* @return The corresponding override config section for the recipe, if it exists, or null * @return The corresponding override config section for the recipe, if it exists, or null
*/ */
public ConfigurationSection getOverride(final RecipeWrapper recipe) { public ConfigurationSection getOverride(final ItemStack buy, ItemStack sell) {
final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides"); final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides");
if(overrides != null) { if(overrides != null) {
for(final String override : overrides.getKeys(false)) { for(final String override : overrides.getKeys(false)) {
final ConfigurationSection item = this.getItem(recipe, override); final ConfigurationSection item = this.getItem(buy, sell, override);
if(item != null) return item; if(item != null) return item;
} }
} }
@ -72,17 +103,18 @@ public class Settings {
} }
/** /**
* @param recipe The wrapped recipe to fetch any overrides for * @param buy The first ingredient of the recipe
* @param sell The result of the recipe
* @param key The key where the override settings are stored in config.yml * @param key The key where the override settings are stored in config.yml
* @return The corresponding override config section for the recipe, if it exists, or null * @return The corresponding override config section for the recipe, if it exists, or null
*/ */
public ConfigurationSection getItem(final RecipeWrapper recipe, final String key) { public ConfigurationSection getItem(final ItemStack buy, final ItemStack sell, final String key) {
final ConfigurationSection item = instance.getCfg().getConfigurationSection("Overrides."+key); final ConfigurationSection item = instance.getCfg().getConfigurationSection("Overrides."+key);
if(item == null) return null; if(item == null) return null;
if(!key.contains("_")) { if(!key.contains("_")) {
//Return the item if the item name is valid //Return the item if the item name is valid
if(this.verify(recipe, Material.matchMaterial(key))) return item; if(this.verify(buy, sell, Material.matchMaterial(key))) return item;
return null; return null;
} }
@ -90,15 +122,15 @@ public class Settings {
try { try {
//Return the enchanted book item if there's a number in the item name //Return the enchanted book item if there's a number in the item name
final int level = Integer.parseInt(words[words.length-1]); final int level = Integer.parseInt(words[words.length-1]);
if(recipe.getSellItemStack().getType() == Material.ENCHANTED_BOOK) { if(sell.getType() == Material.ENCHANTED_BOOK) {
final EnchantmentStorageMeta meta = (EnchantmentStorageMeta) recipe.getSellItemStack().getItemMeta(); final EnchantmentStorageMeta meta = (EnchantmentStorageMeta) sell.getItemMeta();
final Enchantment enchantment = EnchantmentWrapper.getByKey(NamespacedKey.minecraft(key.substring(0, key.lastIndexOf("_")))); final Enchantment enchantment = EnchantmentWrapper.getByKey(NamespacedKey.minecraft(key.substring(0, key.lastIndexOf("_"))));
if (meta == null || enchantment == null) return null; if (meta == null || enchantment == null) return null;
if (meta.hasStoredEnchant(enchantment) && meta.getStoredEnchantLevel(enchantment) == level) return item; if (meta.hasStoredEnchant(enchantment) && meta.getStoredEnchantLevel(enchantment) == level) return item;
} }
} catch(NumberFormatException e) { } catch(NumberFormatException e) {
//Return the item if the item name is valid //Return the item if the item name is valid
if(this.verify(recipe, Material.matchMaterial(key))) if(this.verify(buy, sell, Material.matchMaterial(key)))
return item; return item;
return null; return null;
} catch(Exception e2) { } catch(Exception e2) {
@ -109,11 +141,12 @@ public class Settings {
} }
/** /**
* @param recipe The wrapped recipe to match with the override setting * @param buy The first ingredient of the recipe
* @param sell The result of the recipe
* @param material The material to compare the recipe against * @param material The material to compare the recipe against
* @return True if a recipe matches an override section, false otherwise * @return True if a recipe matches an override section, false otherwise
*/ */
private boolean verify(final RecipeWrapper recipe, final Material material) { private boolean verify(final ItemStack buy, final ItemStack sell, final Material material) {
return ((recipe.getSellItemStack().getType() == material) || (recipe.getBuyItemStack().getType() == material)); return ((buy.getType() == material) || (sell.getType() == material));
} }
} }

View File

@ -71,9 +71,6 @@ public class RecipeWrapper {
/** @return The maximum number of times a player can make a trade before the villager restocks */ /** @return The maximum number of times a player can make a trade before the villager restocks */
public int getMaxUses() { return recipe.getInteger("maxUses"); } public int getMaxUses() { return recipe.getInteger("maxUses"); }
/** @return The ItemStack representation of the first ingredient */ /** @return The ItemStack representation of an ingredient or the result */
public ItemStack getBuyItemStack() { return recipe.getItemStack("buy"); } public ItemStack getItemStack(final String key) { return recipe.getItemStack(key); }
/** @return The ItemStack representation of the result */
public ItemStack getSellItemStack() { return recipe.getItemStack("sell"); }
} }

View File

@ -8,6 +8,17 @@
# This helps me keep track of what server versions are being used. Please leave this set to true. # This helps me keep track of what server versions are being used. Please leave this set to true.
bStats: true bStats: true
# Database connection settings
database:
mysql: false
host: 127.0.0.1
port: 3306
database: villagertradelimiter
username: root
password: root
encoding: utf8
useSSL: false
# Add world names for worlds that you want to completely disable ALL villager trading. Set to [] to disable this feature. # Add world names for worlds that you want to completely disable ALL villager trading. Set to [] to disable this feature.
DisableTrading: DisableTrading:
- world_nether - world_nether
@ -17,7 +28,7 @@ DisableTrading:
# * Set to -1 to disable this feature and keep vanilla behavior. # * Set to -1 to disable this feature and keep vanilla behavior.
# * Set to a number between 0 and 5 to set the maximum HotV effect level players can have # * Set to a number between 0 and 5 to set the maximum HotV effect level players can have
# For more information, see https://minecraft.fandom.com/wiki/Hero_of_the_Village#Price_decrement # For more information, see https://minecraft.fandom.com/wiki/Hero_of_the_Village#Price_decrement
MaxHeroLevel: 1 MaxHeroLevel: -1
# The maximum discount (%) you can get from trading/healing zombie villagers. This limits reputation-based price decreases. # The maximum discount (%) you can get from trading/healing zombie villagers. This limits reputation-based price decreases.
# * Set to -1.0 to disable this feature and keep vanilla behavior # * Set to -1.0 to disable this feature and keep vanilla behavior
@ -55,12 +66,13 @@ Overrides:
MaxDiscount: -1.0 MaxDiscount: -1.0
MaxDemand: 60 MaxDemand: 60
MaxUses: 2 MaxUses: 2
Cooldown: 7d
Item1: Item1:
Material: "book" Material: "book"
Amount: 64 Amount: 64
Item2: Item2:
Material: "ink_sac" Material: "ink_sac"
Amount: 64 Amount: 48
Result: Result:
Material: "name_tag" Material: "name_tag"
Amount: 2 Amount: 2