From e708187b33e257c459096382fa9cef04c48a9407 Mon Sep 17 00:00:00 2001 From: PretzelJohn <58197328+PretzelJohn@users.noreply.github.com> Date: Tue, 11 Jan 2022 04:22:52 -0500 Subject: [PATCH] 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 --- pom.xml | 12 ++ .../VillagerTradeLimiter.java | 42 +++++-- .../villagertradelimiter/data/Cooldown.java | 33 +++++ .../villagertradelimiter/data/PlayerData.java | 17 +-- .../database/Database.java | 65 ++++++++++ .../database/DatabaseManager.java | 96 +++++++++++++++ .../villagertradelimiter/database/MySQL.java | 35 ++++++ .../villagertradelimiter/database/SQLite.java | 25 ++++ .../villagertradelimiter/lib/Callback.java | 2 +- .../dev/villagertradelimiter/lib/Util.java | 79 ++++++++++-- .../listeners/InventoryListener.java | 114 ++++++++++++++++++ .../listeners/PlayerListener.java | 84 ++++++------- .../settings/Settings.java | 63 +++++++--- .../wrappers/RecipeWrapper.java | 7 +- src/main/resources/config.yml | 16 ++- 15 files changed, 590 insertions(+), 100 deletions(-) create mode 100644 src/com/pretzel/dev/villagertradelimiter/data/Cooldown.java create mode 100644 src/com/pretzel/dev/villagertradelimiter/database/Database.java create mode 100644 src/com/pretzel/dev/villagertradelimiter/database/DatabaseManager.java create mode 100644 src/com/pretzel/dev/villagertradelimiter/database/MySQL.java create mode 100644 src/com/pretzel/dev/villagertradelimiter/database/SQLite.java create mode 100644 src/com/pretzel/dev/villagertradelimiter/listeners/InventoryListener.java diff --git a/pom.xml b/pom.xml index 7f5fe98..56b0d7f 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,18 @@ 1.18.1-R0.1-SNAPSHOT provided + + mysql + mysql-connector-java + 8.0.27 + provided + + + org.xerial + sqlite-jdbc + 3.36.0.3 + provided + de.tr7zw functional-annotations diff --git a/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java b/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java index a8892c7..19bf9e7 100644 --- a/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java +++ b/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java @@ -2,11 +2,15 @@ package com.pretzel.dev.villagertradelimiter; import com.pretzel.dev.villagertradelimiter.commands.CommandManager; 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.lib.Metrics; import com.pretzel.dev.villagertradelimiter.lib.Util; import com.pretzel.dev.villagertradelimiter.listeners.PlayerListener; import com.pretzel.dev.villagertradelimiter.settings.Lang; +import com.pretzel.dev.villagertradelimiter.settings.Settings; import org.bukkit.ChatColor; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; @@ -25,13 +29,16 @@ public class VillagerTradeLimiter extends JavaPlugin { private FileConfiguration cfg; private Lang lang; private CommandManager commandManager; + private DatabaseManager databaseManager; private PlayerListener playerListener; + private HashMap playerData; - //Initial plugin load/unload + /** Initial plugin load/unload */ public void onEnable() { //Initialize instance variables this.cfg = null; this.commandManager = new CommandManager(this); + this.playerData = new HashMap<>(); //Copy default settings & load settings this.getConfig().options().copyDefaults(); @@ -40,7 +47,6 @@ public class VillagerTradeLimiter extends JavaPlugin { this.loadBStats(); //Register commands and listeners - this.playerListener = new PlayerListener(this); this.registerCommands(); this.registerListeners(); @@ -48,7 +54,15 @@ public class VillagerTradeLimiter extends JavaPlugin { 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() { final String mainPath = this.getDataFolder().getPath()+"/"; final File file = new File(mainPath, "config.yml"); @@ -59,35 +73,45 @@ public class VillagerTradeLimiter extends JavaPlugin { } this.cfg = YamlConfiguration.loadConfiguration(file); 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() { if(this.cfg.getBoolean("bStats", true)) { new Metrics(this, BSTATS_ID); } } - //Registers plugin commands + /** Registers plugin commands */ private void registerCommands() { final CommandBase cmd = this.commandManager.getCommands(); this.getCommand("villagertradelimiter").setExecutor(cmd); this.getCommand("villagertradelimiter").setTabCompleter(cmd); } - //Registers plugin listeners + /** Registers plugin listeners */ 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(new InventoryListener(this, settings), this); } // ------------------------- Getters ------------------------- - //Returns the settings from config.yml + /** Returns the settings from config.yml */ 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); } - //Returns this plugin's player listener + /** Returns this plugin's player listener */ public PlayerListener getPlayerListener() { return this.playerListener; } + + /** Returns a player's data container */ + public HashMap getPlayerData() { return this.playerData; } } diff --git a/src/com/pretzel/dev/villagertradelimiter/data/Cooldown.java b/src/com/pretzel/dev/villagertradelimiter/data/Cooldown.java new file mode 100644 index 0000000..c876d03 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/data/Cooldown.java @@ -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; + } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/data/PlayerData.java b/src/com/pretzel/dev/villagertradelimiter/data/PlayerData.java index 4834634..937f852 100644 --- a/src/com/pretzel/dev/villagertradelimiter/data/PlayerData.java +++ b/src/com/pretzel/dev/villagertradelimiter/data/PlayerData.java @@ -1,22 +1,23 @@ package com.pretzel.dev.villagertradelimiter.data; import com.pretzel.dev.villagertradelimiter.wrappers.VillagerWrapper; -import org.bukkit.entity.Player; + +import java.util.HashMap; public class PlayerData { - private final Player player; + private final HashMap tradingCooldowns; private VillagerWrapper tradingVillager; - public PlayerData(final Player player) { - this.player = player; + public PlayerData() { + this.tradingCooldowns = new HashMap<>(); this.tradingVillager = null; } - /** @param tradingVillager The villager that this player is currently trading with */ - public void setTradingVillager(VillagerWrapper tradingVillager) { this.tradingVillager = tradingVillager; } + /** @return The map of items to timestamps for the player's trading history */ + public HashMap getTradingCooldowns() { return this.tradingCooldowns; } - /** @return The player that this data is for */ - public Player getPlayer() { return this.player; } + /** @param tradingVillager The villager that this player is currently trading with */ + public void setTradingVillager(final VillagerWrapper tradingVillager) { this.tradingVillager = tradingVillager; } /** @return The villager that this player is currently trading with */ public VillagerWrapper getTradingVillager() { return this.tradingVillager; } diff --git a/src/com/pretzel/dev/villagertradelimiter/database/Database.java b/src/com/pretzel/dev/villagertradelimiter/database/Database.java new file mode 100644 index 0000000..4a509d9 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/database/Database.java @@ -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 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 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> callback) { + Bukkit.getScheduler().runTaskAsynchronously(this.instance, () -> { + final ArrayList 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(); +} \ No newline at end of file diff --git a/src/com/pretzel/dev/villagertradelimiter/database/DatabaseManager.java b/src/com/pretzel/dev/villagertradelimiter/database/DatabaseManager.java new file mode 100644 index 0000000..f0e11b9 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/database/DatabaseManager.java @@ -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); + } +} + diff --git a/src/com/pretzel/dev/villagertradelimiter/database/MySQL.java b/src/com/pretzel/dev/villagertradelimiter/database/MySQL.java new file mode 100644 index 0000000..5a83ae0 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/database/MySQL.java @@ -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; } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/database/SQLite.java b/src/com/pretzel/dev/villagertradelimiter/database/SQLite.java new file mode 100644 index 0000000..fbb62c6 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/database/SQLite.java @@ -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; } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java b/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java index bc5828b..84a6eeb 100644 --- a/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java +++ b/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java @@ -6,5 +6,5 @@ public interface Callback { * @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 */ - void call(T result, String[] args); + void call(T result, String... args); } diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/Util.java b/src/com/pretzel/dev/villagertradelimiter/lib/Util.java index 4631bf8..1d99b39 100644 --- a/src/com/pretzel/dev/villagertradelimiter/lib/Util.java +++ b/src/com/pretzel/dev/villagertradelimiter/lib/Util.java @@ -14,28 +14,49 @@ import org.bukkit.entity.Villager; import net.md_5.bungee.api.ChatColor; public class Util { - //Sends a message to the sender of a command - public static void sendMsg(String msg, Player p) { - if(p == null) consoleMsg(msg); - else p.sendMessage(msg); + /** + * Sends a message to the sender of a command + * @param msg The message to send + * @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 + * @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) { 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) { 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) { if(in == null) return null; 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) { String error = e.toString(); for(StackTraceElement x : e.getStackTrace()) { @@ -44,26 +65,47 @@ public class Util { 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) { 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) { 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 = ""; - for(int a : arr) { res += a+""; } + for(int a : arr) { res += a+separator; } 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) { String out = ""; - BufferedReader br = null; + BufferedReader br; try { br = new BufferedReader(reader); String line; @@ -76,6 +118,12 @@ public class Util { } 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) { try { 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) { - BufferedWriter bw = null; + BufferedWriter bw; try { bw = new BufferedWriter(new FileWriter(file)); bw.write(out); diff --git a/src/com/pretzel/dev/villagertradelimiter/listeners/InventoryListener.java b/src/com/pretzel/dev/villagertradelimiter/listeners/InventoryListener.java new file mode 100644 index 0000000..e3c2614 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/listeners/InventoryListener.java @@ -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; + } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java b/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java index e98ce48..f1dcac1 100644 --- a/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java +++ b/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java @@ -1,6 +1,7 @@ package com.pretzel.dev.villagertradelimiter.listeners; 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 com.pretzel.dev.villagertradelimiter.settings.Settings; @@ -11,31 +12,28 @@ 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.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryPickupItemEvent; -import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import java.util.HashMap; import java.util.List; public class PlayerListener implements Listener { private final VillagerTradeLimiter instance; private final Settings settings; - private final HashMap 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.settings = new Settings(instance); - this.playerData = new HashMap<>(); + this.settings = settings; } /** Handles when a player begins trading with a villager */ @EventHandler - public void onPlayerBeginTrading(PlayerInteractEntityEvent event) { + public void onPlayerBeginTrading(final PlayerInteractEntityEvent event) { if(!(event.getRightClicked() instanceof Villager)) return; final Villager villager = (Villager)event.getRightClicked(); if(Util.isNPC(villager)) return; //Skips NPCs @@ -44,11 +42,13 @@ public class PlayerListener implements Listener { //DisableTrading feature if(instance.getCfg().isBoolean("DisableTrading")) { + //If all trading is disabled if(instance.getCfg().getBoolean("DisableTrading", false)) { event.setCancelled(true); return; } } else { + //If trading in the world the player is in is disabled final List disabledWorlds = instance.getCfg().getStringList("DisableTrading"); final String world = event.getPlayer().getWorld().getName(); 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); final Player player = event.getPlayer(); + if(!instance.getPlayerData().containsKey(player.getUniqueId())) { + instance.getPlayerData().put(player.getUniqueId(), new PlayerData()); + } 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) * @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); 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 String version = instance.getServer().getClass().getPackage().getName(); 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)); //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) { setIngredient(override.getConfigurationSection("Item1"), recipe.getIngredient1()); setIngredient(override.getConfigurationSection("Item2"), recipe.getIngredient2()); @@ -116,19 +106,13 @@ public class PlayerListener implements Listener { } //Set the maximum number of uses (trades/day) - recipe.setMaxUses(getMaxUses(recipe)); + recipe.setMaxUses(getMaxUses(recipe, other)); } //Open the villager's trading menu - getPlayerData(player).setTradingVillager(villagerWrapper); player.openMerchant(villager, false); } - @EventHandler - public void onPickupItem(InventoryPickupItemEvent event) { - Util.consoleMsg("Picked up!"); - } - /** * @param recipe The recipe to get the base price for * @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 * @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 maxUses = settings.fetchInt(recipe, "MaxUses", -1); 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(disabled) maxUses = 0; return maxUses; @@ -220,15 +221,4 @@ public class PlayerListener implements Listener { ingredient.setMaterialId("minecraft:"+item.getString("Material", ingredient.getMaterialId()).replace("minecraft:","")); 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; - } } diff --git a/src/com/pretzel/dev/villagertradelimiter/settings/Settings.java b/src/com/pretzel/dev/villagertradelimiter/settings/Settings.java index a4e9b4a..b462f8c 100644 --- a/src/com/pretzel/dev/villagertradelimiter/settings/Settings.java +++ b/src/com/pretzel/dev/villagertradelimiter/settings/Settings.java @@ -25,7 +25,7 @@ public class Settings { */ public boolean fetchBoolean(final RecipeWrapper recipe, String key, boolean 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); return global; } @@ -38,7 +38,7 @@ public class Settings { */ public int fetchInt(final RecipeWrapper recipe, String key, int 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); return global; } @@ -51,20 +51,51 @@ public class Settings { */ public double fetchDouble(final RecipeWrapper recipe, String key, double 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); 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 */ - public ConfigurationSection getOverride(final RecipeWrapper recipe) { + public ConfigurationSection getOverride(final ItemStack buy, ItemStack sell) { final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides"); if(overrides != null) { 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; } } @@ -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 * @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); if(item == null) return null; if(!key.contains("_")) { //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; } @@ -90,15 +122,15 @@ public class Settings { try { //Return the enchanted book item if there's a number in the item name final int level = Integer.parseInt(words[words.length-1]); - if(recipe.getSellItemStack().getType() == Material.ENCHANTED_BOOK) { - final EnchantmentStorageMeta meta = (EnchantmentStorageMeta) recipe.getSellItemStack().getItemMeta(); + if(sell.getType() == Material.ENCHANTED_BOOK) { + final EnchantmentStorageMeta meta = (EnchantmentStorageMeta) sell.getItemMeta(); final Enchantment enchantment = EnchantmentWrapper.getByKey(NamespacedKey.minecraft(key.substring(0, key.lastIndexOf("_")))); if (meta == null || enchantment == null) return null; if (meta.hasStoredEnchant(enchantment) && meta.getStoredEnchantLevel(enchantment) == level) return item; } } catch(NumberFormatException e) { //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 null; } 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 * @return True if a recipe matches an override section, false otherwise */ - private boolean verify(final RecipeWrapper recipe, final Material material) { - return ((recipe.getSellItemStack().getType() == material) || (recipe.getBuyItemStack().getType() == material)); + private boolean verify(final ItemStack buy, final ItemStack sell, final Material material) { + return ((buy.getType() == material) || (sell.getType() == material)); } } diff --git a/src/com/pretzel/dev/villagertradelimiter/wrappers/RecipeWrapper.java b/src/com/pretzel/dev/villagertradelimiter/wrappers/RecipeWrapper.java index 851758b..435a1a1 100644 --- a/src/com/pretzel/dev/villagertradelimiter/wrappers/RecipeWrapper.java +++ b/src/com/pretzel/dev/villagertradelimiter/wrappers/RecipeWrapper.java @@ -71,9 +71,6 @@ public class RecipeWrapper { /** @return The maximum number of times a player can make a trade before the villager restocks */ public int getMaxUses() { return recipe.getInteger("maxUses"); } - /** @return The ItemStack representation of the first ingredient */ - public ItemStack getBuyItemStack() { return recipe.getItemStack("buy"); } - - /** @return The ItemStack representation of the result */ - public ItemStack getSellItemStack() { return recipe.getItemStack("sell"); } + /** @return The ItemStack representation of an ingredient or the result */ + public ItemStack getItemStack(final String key) { return recipe.getItemStack(key); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index e2a88f2..41acb3e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -8,6 +8,17 @@ # This helps me keep track of what server versions are being used. Please leave this set to 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. DisableTrading: - world_nether @@ -17,7 +28,7 @@ DisableTrading: # * 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 # 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. # * Set to -1.0 to disable this feature and keep vanilla behavior @@ -55,12 +66,13 @@ Overrides: MaxDiscount: -1.0 MaxDemand: 60 MaxUses: 2 + Cooldown: 7d Item1: Material: "book" Amount: 64 Item2: Material: "ink_sac" - Amount: 64 + Amount: 48 Result: Material: "name_tag" Amount: 2