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