- * Check out https://bStats.org/ to learn more about bStats!
- *
- * This class is modified by tr7zw to work when the api is shaded into other peoples plugins.
- */
-public class ApiMetricsLite {
-
- private static final String PLUGINNAME = "ItemNBTAPI"; // DO NOT CHANGE THE NAME! else it won't link the data on bStats
-
- // The version of this bStats class
- public static final int B_STATS_VERSION = 1;
-
- // The version of the NBT-Api bStats
- public static final int NBT_BSTATS_VERSION = 1;
-
- // The url to which the data is sent
- private static final String URL = "https://bStats.org/submitData/bukkit";
-
- // Is bStats enabled on this server?
- private boolean enabled;
-
- // Should failed requests be logged?
- private static boolean logFailedRequests;
-
- // Should the sent data be logged?
- private static boolean logSentData;
-
- // Should the response text be logged?
- private static boolean logResponseStatusText;
-
- // The uuid of the server
- private static String serverUUID;
-
- // The plugin
- private Plugin plugin;
-
- /**
- * Class constructor.
- *
- */
- public ApiMetricsLite() {
-
- // The register method just uses any enabled plugin it can find to register. This *shouldn't* cause any problems, since the plugin isn't used any other way.
- // Register our service
- for(Plugin plug : Bukkit.getPluginManager().getPlugins()) {
- plugin = plug;
- if(plugin != null)
- break;
- }
- if(plugin == null) {
- return;// Didn't find any plugin that could work
- }
-
- // Get the config file
- File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
- File configFile = new File(bStatsFolder, "config.yml");
- YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
-
- // Check if the config file exists
- if (!config.isSet("serverUuid")) {
-
- // Add default values
- config.addDefault("enabled", true);
- // Every server gets it's unique random id.
- config.addDefault("serverUuid", UUID.randomUUID().toString());
- // Should failed request be logged?
- config.addDefault("logFailedRequests", false);
- // Should the sent data be logged?
- config.addDefault("logSentData", false);
- // Should the response text be logged?
- config.addDefault("logResponseStatusText", false);
-
- // Inform the server owners about bStats
- config.options().header(
- "bStats collects some data for plugin authors like how many servers are using their plugins.\n" +
- "To honor their work, you should not disable it.\n" +
- "This has nearly no effect on the server performance!\n" +
- "Check out https://bStats.org/ to learn more :)"
- ).copyDefaults(true);
- try {
- config.save(configFile);
- } catch (IOException ignored) { }
- }
-
- // Load the data
- serverUUID = config.getString("serverUuid");
- logFailedRequests = config.getBoolean("logFailedRequests", false);
- enabled = config.getBoolean("enabled", true);
- logSentData = config.getBoolean("logSentData", false);
- logResponseStatusText = config.getBoolean("logResponseStatusText", false);
- if (enabled) {
- boolean found = false;
- // Search for all other bStats Metrics classes to see if we are the first one
- for (Class> service : Bukkit.getServicesManager().getKnownServices()) {
- try {
- service.getField("NBT_BSTATS_VERSION"); // Create only one instance of the nbt-api bstats.
- return;
- } catch (NoSuchFieldException ignored) { }
- try {
- service.getField("B_STATS_VERSION"); // Our identifier :)
- found = true; // We aren't the first
- break;
- } catch (NoSuchFieldException ignored) { }
- }
- boolean fFound = found;
- // Register our service
- if(Bukkit.isPrimaryThread()){
- Bukkit.getServicesManager().register(ApiMetricsLite.class, this, plugin, ServicePriority.Normal);
- if (!fFound) {
- getLogger().info("[NBTAPI] Using the plugin '" + plugin.getName() + "' to create a bStats instance!");
- // We are the first!
- startSubmitting();
- }
- }else{
- Bukkit.getScheduler().runTask(plugin, () -> {
- Bukkit.getServicesManager().register(ApiMetricsLite.class, this, plugin, ServicePriority.Normal);
- if (!fFound) {
- getLogger().info("[NBTAPI] Using the plugin '" + plugin.getName() + "' to create a bStats instance!");
- // We are the first!
- startSubmitting();
- }
- });
- }
- }
- }
-
- /**
- * Checks if bStats is enabled.
- *
- * @return Whether bStats is enabled or not.
- */
- public boolean isEnabled() {
- return enabled;
- }
-
- /**
- * Starts the Scheduler which submits our data every 30 minutes.
- */
- private void startSubmitting() {
- final Timer timer = new Timer(true); // We use a timer cause the Bukkit scheduler is affected by server lags
- timer.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- if (!plugin.isEnabled()) { // Plugin was disabled
- timer.cancel();
- return;
- }
- // Nevertheless we want our code to run in the Bukkit main thread, so we have to use the Bukkit scheduler
- // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;)
- Bukkit.getScheduler().runTask(plugin, () -> submitData());
- }
- }, 1000l * 60l * 5l, 1000l * 60l * 30l);
- // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start
- // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted!
- // WARNING: Just don't do it!
- }
-
- /**
- * Gets the plugin specific data.
- * This method is called using Reflection.
- *
- * @return The plugin specific data.
- */
- public JsonObject getPluginData() {
- JsonObject data = new JsonObject();
-
- data.addProperty("pluginName", PLUGINNAME); // Append the name of the plugin
- data.addProperty("pluginVersion", MinecraftVersion.VERSION); // Append the version of the plugin
- data.add("customCharts", new JsonArray());
-
- return data;
- }
-
- /**
- * Gets the server specific data.
- *
- * @return The server specific data.
- */
- private JsonObject getServerData() {
- // Minecraft specific data
- int playerAmount;
- try {
- // Around MC 1.8 the return type was changed to a collection from an array,
- // This fixes java.lang.NoSuchMethodError: org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection;
- Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers");
- playerAmount = onlinePlayersMethod.getReturnType().equals(Collection.class)
- ? ((Collection>) onlinePlayersMethod.invoke(Bukkit.getServer())).size()
- : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length;
- } catch (Exception e) {
- playerAmount = Bukkit.getOnlinePlayers().size(); // Just use the new method if the Reflection failed
- }
- int onlineMode = Bukkit.getOnlineMode() ? 1 : 0;
- String bukkitVersion = Bukkit.getVersion();
- String bukkitName = Bukkit.getName();
-
- // OS/Java specific data
- String javaVersion = System.getProperty("java.version");
- String osName = System.getProperty("os.name");
- String osArch = System.getProperty("os.arch");
- String osVersion = System.getProperty("os.version");
- int coreCount = Runtime.getRuntime().availableProcessors();
-
- JsonObject data = new JsonObject();
-
- data.addProperty("serverUUID", serverUUID);
-
- data.addProperty("playerAmount", playerAmount);
- data.addProperty("onlineMode", onlineMode);
- data.addProperty("bukkitVersion", bukkitVersion);
- data.addProperty("bukkitName", bukkitName);
-
- data.addProperty("javaVersion", javaVersion);
- data.addProperty("osName", osName);
- data.addProperty("osArch", osArch);
- data.addProperty("osVersion", osVersion);
- data.addProperty("coreCount", coreCount);
-
- return data;
- }
-
- /**
- * Collects the data and sends it afterwards.
- */
- private void submitData() {
- final JsonObject data = getServerData();
-
- JsonArray pluginData = new JsonArray();
- // Search for all other bStats Metrics classes to get their plugin data
- for (Class> service : Bukkit.getServicesManager().getKnownServices()) {
- try {
- service.getField("B_STATS_VERSION"); // Our identifier :)
-
- for (RegisteredServiceProvider> provider : Bukkit.getServicesManager().getRegistrations(service)) {
- try {
- Object plugin = provider.getService().getMethod("getPluginData").invoke(provider.getProvider());
- if (plugin instanceof JsonObject) {
- pluginData.add((JsonObject) plugin);
- } else { // old bstats version compatibility
- try {
- Class> jsonObjectJsonSimple = Class.forName("org.json.simple.JSONObject");
- if (plugin.getClass().isAssignableFrom(jsonObjectJsonSimple)) {
- Method jsonStringGetter = jsonObjectJsonSimple.getDeclaredMethod("toJSONString");
- jsonStringGetter.setAccessible(true);
- String jsonString = (String) jsonStringGetter.invoke(plugin);
- JsonObject object = new JsonParser().parse(jsonString).getAsJsonObject();
- pluginData.add(object);
- }
- } catch (ClassNotFoundException e) {
- // minecraft version 1.14+
- if (logFailedRequests) {
- getLogger().log(Level.WARNING, "[NBTAPI][BSTATS] Encountered exception while posting request!", e);
- // Not using the plugins logger since the plugin isn't the plugin containing the NBT-Api most of the time
- //this.plugin.getLogger().log(Level.SEVERE, "Encountered unexpected exception ", e);
- }
- continue; // continue looping since we cannot do any other thing.
- }
- }
- } catch (NullPointerException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) {
- }
- }
- } catch (NoSuchFieldException ignored) { }
- }
-
- data.add("plugins", pluginData);
-
- // Create a new thread for the connection to the bStats server
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- // Send the data
- sendData(plugin, data);
- } catch (Exception e) {
- // Something went wrong! :(
- if (logFailedRequests) {
- getLogger().log(Level.WARNING, "[NBTAPI][BSTATS] Could not submit plugin stats of " + plugin.getName(), e);
- // Not using the plugins logger since the plugin isn't the plugin containing the NBT-Api most of the time
- //plugin.getLogger().log(Level.WARNING, "Could not submit plugin stats of " + plugin.getName(), e);
- }
- }
- }
- }).start();
- }
-
- /**
- * Sends the data to the bStats server.
- *
- * @param plugin Any plugin. It's just used to get a logger instance.
- * @param data The data to send.
- * @throws Exception If the request failed.
- */
- private static void sendData(Plugin plugin, JsonObject data) throws Exception {
- if (data == null) {
- throw new IllegalArgumentException("Data cannot be null!");
- }
- if (Bukkit.isPrimaryThread()) {
- throw new IllegalAccessException("This method must not be called from the main thread!");
- }
- if (logSentData) {
- System.out.println("[NBTAPI][BSTATS] Sending data to bStats: " + data.toString());
- // Not using the plugins logger since the plugin isn't the plugin containing the NBT-Api most of the time
- //plugin.getLogger().info("Sending data to bStats: " + data.toString());
- }
- HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection();
-
- // Compress the data to save bandwidth
- byte[] compressedData = compress(data.toString());
-
- // Add headers
- connection.setRequestMethod("POST");
- connection.addRequestProperty("Accept", "application/json");
- connection.addRequestProperty("Connection", "close");
- connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request
- connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
- connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format
- connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION);
-
- // Send data
- connection.setDoOutput(true);
- DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
- outputStream.write(compressedData);
- outputStream.flush();
- outputStream.close();
-
- InputStream inputStream = connection.getInputStream();
- BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
-
- StringBuilder builder = new StringBuilder();
- String line;
- while ((line = bufferedReader.readLine()) != null) {
- builder.append(line);
- }
- bufferedReader.close();
- if (logResponseStatusText) {
- getLogger().info("[NBTAPI][BSTATS] Sent data to bStats and received response: " + builder.toString());
- // Not using the plugins logger since the plugin isn't the plugin containing the NBT-Api most of the time
- //plugin.getLogger().info("Sent data to bStats and received response: " + builder.toString());
- }
- }
-
- /**
- * Gzips the given String.
- *
- * @param str The string to gzip.
- * @return The gzipped String.
- * @throws IOException If the compression failed.
- */
- private static byte[] compress(final String str) throws IOException {
- if (str == null) {
- return new byte[0];
- }
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
- gzip.write(str.getBytes(StandardCharsets.UTF_8));
- gzip.close();
- return outputStream.toByteArray();
- }
-
-}
diff --git a/src/com/pretzel/dev/villagertradelimiter/nms/utils/MinecraftVersion.java b/src/com/pretzel/dev/villagertradelimiter/nms/utils/MinecraftVersion.java
index f868455..aceec7a 100644
--- a/src/com/pretzel/dev/villagertradelimiter/nms/utils/MinecraftVersion.java
+++ b/src/com/pretzel/dev/villagertradelimiter/nms/utils/MinecraftVersion.java
@@ -112,13 +112,6 @@ public enum MinecraftVersion {
}
private static void init() {
- try {
- if (hasGsonSupport() && !bStatsDisabled)
- new ApiMetricsLite();
- } catch (Exception ex) {
- logger.log(Level.WARNING, "[NBTAPI] Error enabling Metrics!", ex);
- }
-
if (hasGsonSupport() && !updateCheckDisabled)
new Thread(() -> {
try {
diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/ConfigUpdater.java b/src/com/pretzel/dev/villagertradelimiter/settings/ConfigUpdater.java
similarity index 99%
rename from src/com/pretzel/dev/villagertradelimiter/lib/ConfigUpdater.java
rename to src/com/pretzel/dev/villagertradelimiter/settings/ConfigUpdater.java
index 914839d..3d904a9 100644
--- a/src/com/pretzel/dev/villagertradelimiter/lib/ConfigUpdater.java
+++ b/src/com/pretzel/dev/villagertradelimiter/settings/ConfigUpdater.java
@@ -1,4 +1,4 @@
-package com.pretzel.dev.villagertradelimiter.lib;
+package com.pretzel.dev.villagertradelimiter.settings;
import com.google.common.base.Preconditions;
import org.bukkit.configuration.ConfigurationSection;
diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/KeyBuilder.java b/src/com/pretzel/dev/villagertradelimiter/settings/KeyBuilder.java
similarity index 98%
rename from src/com/pretzel/dev/villagertradelimiter/lib/KeyBuilder.java
rename to src/com/pretzel/dev/villagertradelimiter/settings/KeyBuilder.java
index 93123ac..46e654f 100644
--- a/src/com/pretzel/dev/villagertradelimiter/lib/KeyBuilder.java
+++ b/src/com/pretzel/dev/villagertradelimiter/settings/KeyBuilder.java
@@ -1,4 +1,4 @@
-package com.pretzel.dev.villagertradelimiter.lib;
+package com.pretzel.dev.villagertradelimiter.settings;
import org.bukkit.configuration.file.FileConfiguration;
diff --git a/src/com/pretzel/dev/villagertradelimiter/settings/Lang.java b/src/com/pretzel/dev/villagertradelimiter/settings/Lang.java
new file mode 100644
index 0000000..9a68d0e
--- /dev/null
+++ b/src/com/pretzel/dev/villagertradelimiter/settings/Lang.java
@@ -0,0 +1,65 @@
+package com.pretzel.dev.villagertradelimiter.settings;
+
+import com.pretzel.dev.villagertradelimiter.lib.Util;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.Plugin;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+
+public class Lang {
+ private final FileConfiguration def;
+ private FileConfiguration cfg;
+
+ /**
+ * @param plugin The Bukkit/Spigot/Paper plugin instance
+ * @param reader The file reader for the default messages.yml file (located in the src/main/resources)
+ * @param path The file path for the active messages.yml file (located on the server in plugins/[plugin name])
+ */
+ public Lang(final Plugin plugin, final Reader reader, final String path) {
+ //Gets the default values, puts them in a temp file, and loads them as a FileConfiguration
+ String[] defLines = Util.readFile(reader);
+ String def = "";
+ if(defLines == null) defLines = new String[0];
+ for(String line : defLines) def += line+"\n";
+ final File defFile = new File(path, "temp.yml");
+ Util.writeFile(defFile, def);
+ this.def = YamlConfiguration.loadConfiguration(defFile);
+
+ //Gets the active values and loads them as a FileConfiguration
+ File file = new File(path,"messages.yml");
+ try {
+ if(file.createNewFile()) Util.writeFile(file, def);
+ } catch (Exception e) {
+ Util.errorMsg(e);
+ }
+
+ this.cfg = null;
+ try {
+ ConfigUpdater.update(plugin, "messages.yml", file);
+ } catch (IOException e) {
+ Util.errorMsg(e);
+ }
+ this.cfg = YamlConfiguration.loadConfiguration(file);
+ defFile.delete();
+ }
+
+ /**
+ * @param key The key (or path) of the section in messages.yml (e.g, common.reloaded)
+ * @return The String value in messages.yml that is mapped to the given key
+ */
+ public String get(final String key) {
+ return get(key, def.getString("help", ""));
+ }
+
+ /**
+ * @param key The key (or path) of the section in messages.yml (e.g, common.reloaded)
+ * @param def The default value to return if the key is not found
+ * @return The String value in messages.yml that is mapped to the given key, or the given default value if the key was not found
+ */
+ public String get(final String key, final String def) {
+ return Util.replaceColors(this.cfg.getString(key, def));
+ }
+}
\ No newline at end of file
diff --git a/src/com/pretzel/dev/villagertradelimiter/settings/Settings.java b/src/com/pretzel/dev/villagertradelimiter/settings/Settings.java
new file mode 100644
index 0000000..a4e9b4a
--- /dev/null
+++ b/src/com/pretzel/dev/villagertradelimiter/settings/Settings.java
@@ -0,0 +1,119 @@
+package com.pretzel.dev.villagertradelimiter.settings;
+
+import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter;
+import com.pretzel.dev.villagertradelimiter.lib.Util;
+import com.pretzel.dev.villagertradelimiter.wrappers.RecipeWrapper;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.enchantments.EnchantmentWrapper;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.EnchantmentStorageMeta;
+
+public class Settings {
+ private final VillagerTradeLimiter instance;
+
+ /** @param instance The instance of VillagerTradeLimiter.java */
+ public Settings(final VillagerTradeLimiter instance) { this.instance = instance; }
+
+ /**
+ * @param recipe The wrapped recipe to fetch any overrides for
+ * @param key The key where the fetched value is stored in config.yml (e.g, DisableTrading)
+ * @param defaultValue The default boolean value to use if the key does not exist
+ * @return A boolean value that has the most specific value possible between the global setting and the overrides settings
+ */
+ public boolean fetchBoolean(final RecipeWrapper recipe, String key, boolean defaultValue) {
+ boolean global = instance.getCfg().getBoolean(key, defaultValue);
+ final ConfigurationSection override = getOverride(recipe);
+ if(override != null) return override.getBoolean(key, global);
+ return global;
+ }
+
+ /**
+ * @param recipe The wrapped recipe to fetch any overrides for
+ * @param key The key where the fetched value is stored in config.yml (e.g, MaxDemand)
+ * @param defaultValue The default integer value to use if the key does not exist
+ * @return An integer value that has the most specific value possible between the global setting and the overrides settings
+ */
+ public int fetchInt(final RecipeWrapper recipe, String key, int defaultValue) {
+ int global = instance.getCfg().getInt(key, defaultValue);
+ final ConfigurationSection override = getOverride(recipe);
+ if(override != null) return override.getInt(key, global);
+ return global;
+ }
+
+ /**
+ * @param recipe The wrapped recipe to fetch any overrides for
+ * @param key The key where the fetched value is stored in config.yml (e.g, MaxDiscount)
+ * @param defaultValue The default double value to use if the key does not exist
+ * @return A double value that has the most specific value possible between the global setting and the overrides settings
+ */
+ public double fetchDouble(final RecipeWrapper recipe, String key, double defaultValue) {
+ double global = instance.getCfg().getDouble(key, defaultValue);
+ final ConfigurationSection override = getOverride(recipe);
+ if(override != null) return override.getDouble(key, global);
+ return global;
+ }
+
+ /**
+ * @param recipe The wrapped recipe to fetch any overrides for
+ * @return The corresponding override config section for the recipe, if it exists, or null
+ */
+ public ConfigurationSection getOverride(final RecipeWrapper recipe) {
+ final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides");
+ if(overrides != null) {
+ for(final String override : overrides.getKeys(false)) {
+ final ConfigurationSection item = this.getItem(recipe, override);
+ if(item != null) return item;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param recipe The wrapped recipe to fetch any overrides for
+ * @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) {
+ 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;
+ return null;
+ }
+
+ final String[] words = key.split("_");
+ 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();
+ 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)))
+ return item;
+ return null;
+ } catch(Exception e2) {
+ //Send an error message
+ Util.errorMsg(e2);
+ }
+ return null;
+ }
+
+ /**
+ * @param recipe The wrapped recipe to match with the override setting
+ * @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));
+ }
+}
diff --git a/src/com/pretzel/dev/villagertradelimiter/wrappers/GossipWrapper.java b/src/com/pretzel/dev/villagertradelimiter/wrappers/GossipWrapper.java
new file mode 100644
index 0000000..ed8ab6a
--- /dev/null
+++ b/src/com/pretzel/dev/villagertradelimiter/wrappers/GossipWrapper.java
@@ -0,0 +1,48 @@
+package com.pretzel.dev.villagertradelimiter.wrappers;
+
+import com.pretzel.dev.villagertradelimiter.lib.Util;
+import com.pretzel.dev.villagertradelimiter.nms.NBTCompound;
+
+public class GossipWrapper {
+ private final NBTCompound gossip;
+
+ public enum GossipType {
+ MAJOR_NEGATIVE(-5),
+ MINOR_NEGATIVE(-1),
+ TRADING(1),
+ MINOR_POSITIVE(1),
+ MAJOR_POSITIVE(5),
+ OTHER(0);
+
+ private final int weight;
+ GossipType(int weight) { this.weight = weight; }
+ int getWeight() { return this.weight; }
+ }
+
+ /** @param gossip The NBTCompound that contains the villager's NBT data of the gossip */
+ public GossipWrapper(final NBTCompound gossip) { this.gossip = gossip; }
+
+ /** @return The GossipType of this gossip: MAJOR_NEGATIVE, MINOR_NEGATIVE, TRADING, MINOR_POSITIVE, MAJOR_POSITIVE, or OTHER if not found */
+ public GossipType getType() {
+ try {
+ return GossipType.valueOf(gossip.getString("Type").toUpperCase());
+ } catch (IllegalArgumentException e) {
+ return GossipType.OTHER;
+ }
+ }
+
+ /**
+ * @param isOld Whether the server is older than 1.16 or not. Minecraft changed how UUID's are represented in 1.16
+ * @return A string representation of the target UUID, for use when matching the target UUID to a player's UUID
+ */
+ public String getTargetUUID(final boolean isOld) {
+ //BEFORE 1.16 (< 1.16)
+ if(isOld) return gossip.getLong("TargetMost")+";"+gossip.getLong("TargetLeast");
+
+ //AFTER 1.16 (>= 1.16)
+ return Util.intArrayToString(gossip.getIntArray("Target"));
+ }
+
+ /** @return The strength of this gossip, which is a value between 0 and: 25, 100, or 200, depending on the gossip type */
+ public int getValue() { return gossip.getInteger("Value"); }
+}
diff --git a/src/com/pretzel/dev/villagertradelimiter/wrappers/IngredientWrapper.java b/src/com/pretzel/dev/villagertradelimiter/wrappers/IngredientWrapper.java
new file mode 100644
index 0000000..e62d861
--- /dev/null
+++ b/src/com/pretzel/dev/villagertradelimiter/wrappers/IngredientWrapper.java
@@ -0,0 +1,35 @@
+package com.pretzel.dev.villagertradelimiter.wrappers;
+
+import com.pretzel.dev.villagertradelimiter.nms.NBTCompound;
+
+public class IngredientWrapper {
+ private final NBTCompound ingredient;
+ private final String materialId;
+ private final int amount;
+
+ /** @param ingredient The NBTCompound that contains the recipe's NBT data of the ingredient */
+ public IngredientWrapper(final NBTCompound ingredient) {
+ this.ingredient = ingredient;
+ this.materialId = getMaterialId();
+ this.amount = getAmount();
+ }
+
+ /** @return The ingredient's material id (e.g, minecraft:enchanted_book) */
+ public String getMaterialId() { return ingredient.getString("id"); }
+
+ /** @return The number of items in the ingredient stack, between 1 and 64 */
+ public int getAmount() { return ingredient.getByte("Count").intValue(); }
+
+
+ /** @param id The ingredient's material id (e.g, minecraft:enchanted_book) */
+ public void setMaterialId(final String id) { this.ingredient.setString("id", id); }
+
+ /** @param amount The number of items in the ingredient stack, which is clamped between 1 and 64 by this function */
+ public void setAmount(int amount) { this.ingredient.setByte("Count", (byte)Math.max(Math.min(amount, 64), 1)); }
+
+ /** Resets the material ID and the amount of this ingredient to default values */
+ public void reset() {
+ setMaterialId(this.materialId);
+ setAmount(this.amount);
+ }
+}
diff --git a/src/com/pretzel/dev/villagertradelimiter/wrappers/PlayerWrapper.java b/src/com/pretzel/dev/villagertradelimiter/wrappers/PlayerWrapper.java
new file mode 100644
index 0000000..2d2c52b
--- /dev/null
+++ b/src/com/pretzel/dev/villagertradelimiter/wrappers/PlayerWrapper.java
@@ -0,0 +1,39 @@
+package com.pretzel.dev.villagertradelimiter.wrappers;
+
+import com.pretzel.dev.villagertradelimiter.lib.Util;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+
+import java.util.UUID;
+
+public class PlayerWrapper {
+ private final OfflinePlayer player;
+
+ /** @param player The offline player that this wrapper wraps */
+ public PlayerWrapper(final OfflinePlayer player) { this.player = player; }
+
+ /** @return Whether this player is an NPC or not */
+ public boolean isNPC() { return (player.isOnline() && Util.isNPC((Player)player)); }
+
+ /**
+ * @param isOld Whether the server is older than 1.16 or not. Minecraft changed how UUID's are represented in 1.16
+ * @return A string representation of the player's UUID, for use when matching the player's UUID to a gossip's target UUID
+ */
+ public String getUUID(final boolean isOld) {
+ final UUID uuid = player.getUniqueId();
+
+ //BEFORE 1.16 (< 1.16)
+ if(isOld) return uuid.getMostSignificantBits()+";"+uuid.getLeastSignificantBits();
+
+ //AFTER 1.16 (>= 1.16)
+ final String uuidString = uuid.toString().replace("-", "");
+ int[] intArray = new int[4];
+ for(int i = 0; i < 4; i++) {
+ intArray[i] = (int)Long.parseLong(uuidString.substring(8*i, 8*(i+1)), 16);
+ }
+ return Util.intArrayToString(intArray);
+ }
+
+ /** @return The regular, online player of this wrapper's offline player, or null if the player is not online */
+ public Player getPlayer() { return player.getPlayer(); }
+}
diff --git a/src/com/pretzel/dev/villagertradelimiter/wrappers/RecipeWrapper.java b/src/com/pretzel/dev/villagertradelimiter/wrappers/RecipeWrapper.java
new file mode 100644
index 0000000..851758b
--- /dev/null
+++ b/src/com/pretzel/dev/villagertradelimiter/wrappers/RecipeWrapper.java
@@ -0,0 +1,79 @@
+package com.pretzel.dev.villagertradelimiter.wrappers;
+
+import com.pretzel.dev.villagertradelimiter.nms.NBTCompound;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.Arrays;
+
+public class RecipeWrapper {
+ //A list of all the items with a default MaxUses of 12 and 3, respectively
+ private static final Material[] MAX_USES_12 = new Material[]{Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, Material.IRON_INGOT, Material.BELL, Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, Material.LAVA_BUCKET, Material.DIAMOND, Material.SHIELD, Material.RABBIT_STEW, Material.DRIED_KELP_BLOCK, Material.SWEET_BERRIES, Material.MAP, Material.FILLED_MAP, Material.COMPASS, Material.ITEM_FRAME, Material.GLOBE_BANNER_PATTERN, Material.WHITE_BANNER, Material.LIGHT_GRAY_BANNER, Material.GRAY_BANNER, Material.BLACK_BANNER, Material.BROWN_BANNER, Material.ORANGE_BANNER, Material.YELLOW_BANNER, Material.LIME_BANNER, Material.GREEN_BANNER, Material.CYAN_BANNER, Material.BLUE_BANNER, Material.LIGHT_BLUE_BANNER, Material.PURPLE_BANNER, Material.MAGENTA_BANNER, Material.PINK_BANNER, Material.RED_BANNER, Material.WHITE_BED, Material.LIGHT_GRAY_BED, Material.GRAY_BED, Material.BLACK_BED, Material.BROWN_BED, Material.ORANGE_BED, Material.YELLOW_BED, Material.LIME_BED, Material.GREEN_BED, Material.CYAN_BED, Material.BLUE_BED, Material.LIGHT_BLUE_BED, Material.PURPLE_BED, Material.MAGENTA_BED, Material.PINK_BED, Material.RED_BED, Material.REDSTONE, Material.GOLD_INGOT, Material.LAPIS_LAZULI, Material.RABBIT_FOOT, Material.GLOWSTONE, Material.SCUTE, Material.GLASS_BOTTLE, Material.ENDER_PEARL, Material.NETHER_WART, Material.EXPERIENCE_BOTTLE, Material.PUMPKIN, Material.PUMPKIN_PIE, Material.MELON, Material.COOKIE, Material.CAKE, Material.SUSPICIOUS_STEW, Material.GOLDEN_CARROT, Material.GLISTERING_MELON_SLICE, Material.CAMPFIRE, Material.TROPICAL_FISH, Material.PUFFERFISH, Material.BIRCH_BOAT, Material.ACACIA_BOAT, Material.OAK_BOAT, Material.DARK_OAK_BOAT, Material.SPRUCE_BOAT, Material.JUNGLE_BOAT, Material.ARROW, Material.FLINT, Material.STRING, Material.TRIPWIRE_HOOK, Material.TIPPED_ARROW, Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, Material.LEATHER, Material.RABBIT_HIDE, Material.LEATHER_HORSE_ARMOR, Material.SADDLE, Material.BOOK, Material.ENCHANTED_BOOK, Material.BOOKSHELF, Material.INK_SAC, Material.GLASS, Material.WRITABLE_BOOK, Material.CLOCK, Material.NAME_TAG, Material.QUARTZ, Material.QUARTZ_PILLAR, Material.QUARTZ_BLOCK, Material.TERRACOTTA, Material.WHITE_TERRACOTTA, Material.LIGHT_GRAY_TERRACOTTA, Material.GRAY_TERRACOTTA, Material.BLACK_TERRACOTTA, Material.BROWN_TERRACOTTA, Material.ORANGE_TERRACOTTA, Material.YELLOW_TERRACOTTA, Material.LIME_TERRACOTTA, Material.GREEN_TERRACOTTA, Material.CYAN_TERRACOTTA, Material.BLUE_TERRACOTTA, Material.LIGHT_BLUE_TERRACOTTA, Material.PURPLE_TERRACOTTA, Material.MAGENTA_TERRACOTTA, Material.PINK_TERRACOTTA, Material.RED_TERRACOTTA, Material.WHITE_GLAZED_TERRACOTTA, Material.LIGHT_GRAY_GLAZED_TERRACOTTA, Material.GRAY_GLAZED_TERRACOTTA, Material.BLACK_GLAZED_TERRACOTTA, Material.BROWN_GLAZED_TERRACOTTA, Material.ORANGE_GLAZED_TERRACOTTA, Material.YELLOW_GLAZED_TERRACOTTA, Material.LIME_GLAZED_TERRACOTTA, Material.GREEN_GLAZED_TERRACOTTA, Material.CYAN_GLAZED_TERRACOTTA, Material.BLUE_GLAZED_TERRACOTTA, Material.LIGHT_BLUE_GLAZED_TERRACOTTA, Material.PURPLE_GLAZED_TERRACOTTA, Material.MAGENTA_GLAZED_TERRACOTTA, Material.PINK_GLAZED_TERRACOTTA, Material.RED_GLAZED_TERRACOTTA, Material.SHEARS, Material.PAINTING, Material.STONE_AXE, Material.STONE_SHOVEL, Material.STONE_PICKAXE, Material.STONE_HOE};
+ private static final Material[] MAX_USES_3 = new Material[]{Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, Material.DIAMOND_SWORD, Material.DIAMOND_AXE, Material.DIAMOND_SHOVEL, Material.DIAMOND_PICKAXE, Material.DIAMOND_HOE, Material.IRON_SWORD, Material.IRON_AXE, Material.IRON_SHOVEL, Material.IRON_PICKAXE, Material.FISHING_ROD, Material.BOW, Material.CROSSBOW};
+
+ private final NBTCompound recipe;
+ private final IngredientWrapper ingredient1;
+ private final IngredientWrapper ingredient2;
+ private final IngredientWrapper result;
+ private final int specialPrice;
+
+ /** @param recipe The NBTCompound that contains the villager's NBT data of the recipe */
+ public RecipeWrapper(final NBTCompound recipe) {
+ this.recipe = recipe;
+ this.ingredient1 = new IngredientWrapper(recipe.getCompound("buy"));
+ this.ingredient2 = new IngredientWrapper(recipe.getCompound("buyB"));
+ this.result = new IngredientWrapper(recipe.getCompound("sell"));
+ this.specialPrice = getSpecialPrice();
+ }
+
+ /** @param specialPrice The discount, which is added to the base price. A negative value will decrease the price, and a positive value will increase the price. */
+ public void setSpecialPrice(int specialPrice) { recipe.setInteger("specialPrice", specialPrice); }
+
+ /** @param maxUses The maximum number of times a player can make a trade before the villager restocks */
+ public void setMaxUses(int maxUses) { recipe.setInteger("maxUses", maxUses); }
+
+ /** Resets the recipe back to its default state */
+ public void reset() {
+ this.setSpecialPrice(this.specialPrice);
+ this.ingredient1.reset();
+ this.ingredient2.reset();
+ this.result.reset();
+
+ int maxUses = 16;
+ Material buyMaterial = recipe.getItemStack("buy").getType();
+ Material sellMaterial = recipe.getItemStack("sell").getType();
+ if(Arrays.asList(MAX_USES_12).contains(buyMaterial) || Arrays.asList(MAX_USES_12).contains(sellMaterial)) {
+ maxUses = 12;
+ } else if(Arrays.asList(MAX_USES_3).contains(buyMaterial) || Arrays.asList(MAX_USES_3).contains(sellMaterial)) {
+ maxUses = 3;
+ }
+ setMaxUses(maxUses);
+ }
+
+ /** @return The wrapper for the first ingredient */
+ public IngredientWrapper getIngredient1() { return ingredient1; }
+
+ /** @return The wrapper for the second ingredient */
+ public IngredientWrapper getIngredient2() { return ingredient2; }
+
+ /** @return The wrapper for the result */
+ public IngredientWrapper getResult() { return result; }
+
+ /** @return The demand for this recipe (increases the price when above 0) */
+ public int getDemand() { return recipe.getInteger("demand"); }
+
+ /** @return The price multiplier for this recipe (controls how strongly gossips, demand, etc. affect the price) */
+ public float getPriceMultiplier() { return recipe.getFloat("priceMultiplier"); }
+
+ /** @return The discount, which is added to the base price. A negative value will decrease the price, and a positive value will increase the price. */
+ public int getSpecialPrice() { return recipe.getInteger("specialPrice"); }
+
+ /** @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"); }
+}
diff --git a/src/com/pretzel/dev/villagertradelimiter/wrappers/VillagerWrapper.java b/src/com/pretzel/dev/villagertradelimiter/wrappers/VillagerWrapper.java
new file mode 100644
index 0000000..f774b76
--- /dev/null
+++ b/src/com/pretzel/dev/villagertradelimiter/wrappers/VillagerWrapper.java
@@ -0,0 +1,87 @@
+package com.pretzel.dev.villagertradelimiter.wrappers;
+
+import com.pretzel.dev.villagertradelimiter.nms.NBTCompound;
+import com.pretzel.dev.villagertradelimiter.nms.NBTCompoundList;
+import com.pretzel.dev.villagertradelimiter.nms.NBTEntity;
+import org.bukkit.entity.Villager;
+import org.bukkit.inventory.ItemStack;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class VillagerWrapper {
+ private final Villager villager;
+ private final NBTEntity entity;
+ private final ItemStack[] contents;
+
+ /** @param villager The Villager to store in this wrapper */
+ public VillagerWrapper(final Villager villager) {
+ this.villager = villager;
+ this.entity = new NBTEntity(villager);
+ this.contents = new ItemStack[villager.getInventory().getContents().length];
+ for(int i = 0; i < this.contents.length; i++) {
+ ItemStack item = villager.getInventory().getItem(i);
+ this.contents[i] = (item == null ? null : item.clone());
+ }
+ }
+
+ /** @return a list of wrapped recipes for the villager */
+ public List