commit 6eb4eb782f30e773cfce2aae06ddc52f23e5ec45 Author: PretzelJohn Date: Sun Jun 27 08:42:57 2021 -0400 Version 1.2.0: Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2249a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/bin/ +/target/ +bin +*.class +/.settings/ +.project +.classpath +*.iml +/.idea/ +/.git/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9eb366 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +

VillagerTradeLimiter (VTL)

+
by PretzelJohn
+ +

Description:

+

This Minecraft plugin limits the villager trade deals that players can get when they cure a zombie villager.

+
+ +

Commands:

+ + + + + + + + + + + + + + + + +
CommandAliasDescription
/villagertradelimiter/vtlshows a help message
/villagertradelimiter reload/vtl reloadreloads config.yml

+ +

Permissions:

+ + + + + + + + + + + + + + + + + + + + + +
PermissionDescriptionDefault User(s)
villagertradelimiter.*Allows players to use /vtl and /vtl reloadOP
villagertradelimiter.useAllows players to use /vtlOP
villagertradelimiter.reloadAllows players to reload config.yml and messages.ymlOP

+ +

Config:

+ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..af3de63 --- /dev/null +++ b/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + com.pretzel.dev + VillagerTradeLimiter + 1.2.0 + + + 1.8 + + + + src + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + + + + + + spigotmc-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + + + + org.spigotmc + spigot + 1.17-R0.1-SNAPSHOT + + + org.spigotmc + spigot-api + 1.16.5-R0.1-SNAPSHOT + provided + + + \ No newline at end of file diff --git a/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java b/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java new file mode 100644 index 0000000..10a7043 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java @@ -0,0 +1,89 @@ +package com.pretzel.dev.villagertradelimiter; + +import com.pretzel.dev.villagertradelimiter.lib.CommandBase; +import com.pretzel.dev.villagertradelimiter.lib.ConfigUpdater; +import com.pretzel.dev.villagertradelimiter.lib.Metrics; +import com.pretzel.dev.villagertradelimiter.lib.Util; +import com.pretzel.dev.villagertradelimiter.listeners.PlayerListener; +import org.bukkit.ChatColor; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; + +public class VillagerTradeLimiter extends JavaPlugin { + public static final String PLUGIN_NAME = "VillagerTradeLimiter"; + public static final String PREFIX = ChatColor.GOLD+"["+PLUGIN_NAME+"] "; + + //Settings + private FileConfiguration cfg; + + //Initial plugin load/unload + public void onEnable() { + //Initialize instance variables + this.cfg = null; + + //Copy default settings & load settings + this.getConfig().options().copyDefaults(); + this.saveDefaultConfig(); + this.loadSettings(); + this.loadBStats(); + + //Register commands and listeners + this.registerCommands(); + this.registerListeners(); + + //Send enabled message + Util.consoleMsg(PREFIX+PLUGIN_NAME+" is running!"); + } + + //Loads or reloads config.yml settings + public void loadSettings() { + //Load config.yml + final String mainPath = this.getDataFolder().getPath()+"/"; + final File file = new File(mainPath, "config.yml"); + ConfigUpdater updater = new ConfigUpdater(this.getTextResource("config.yml"), file); + this.cfg = updater.updateConfig(file, PREFIX); + } + + private void loadBStats() { + if(this.cfg.getBoolean("bStats", true)) new Metrics(this, 9829); + } + + //Registers plugin commands + private void registerCommands() { + final String reloaded = Util.replaceColors("&eVillagerTradeLimiter &ahas been reloaded!"); + final CommandBase vtl = new CommandBase("villagertradelimiter", "villagertradelimiter.use", p -> this.help(p)); + vtl.addSub(new CommandBase("reload", "villagertradelimiter.reload", p -> { + loadSettings(); + p.sendMessage(reloaded); + })); + this.getCommand("villagertradelimiter").setExecutor(vtl); + this.getCommand("villagertradelimiter").setTabCompleter(vtl); + } + + //Registers plugin listeners + private void registerListeners() { + this.getServer().getPluginManager().registerEvents(new PlayerListener(this), this); + } + + // ------------------------- Commands ------------------------- + private void help(final Player p) { + if(p != null) { + if(!p.hasPermission("villagertradelimiter.use") && !p.hasPermission("villagertradelimiter.*")) return; + p.sendMessage(ChatColor.GREEN+"VillagerTradeLimiter commands:"); + p.sendMessage(ChatColor.AQUA+"/vtl "+ChatColor.WHITE+"- shows this help message"); + Util.sendIfPermitted("villagertradelimiter.reload", ChatColor.AQUA+"/vtl reload "+ChatColor.WHITE+"- reloads config.yml", p); + } else { + Util.consoleMsg(ChatColor.GREEN+"VillagerTradeLimiter commands:"); + Util.consoleMsg(ChatColor.AQUA+"/vtl "+ChatColor.WHITE+"- shows this help message"); + Util.consoleMsg(ChatColor.AQUA+"/vtl reload "+ChatColor.WHITE+"- reloads config.yml"); + } + } + + // ------------------------- Getters ------------------------- + //Returns the settings from config.yml + public FileConfiguration getCfg() { return this.cfg; } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java b/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java new file mode 100644 index 0000000..a39b2f6 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java @@ -0,0 +1,5 @@ +package com.pretzel.dev.villagertradelimiter.lib; + +public interface Callback { + void call(T result); +} diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/CommandBase.java b/src/com/pretzel/dev/villagertradelimiter/lib/CommandBase.java new file mode 100644 index 0000000..7ed1966 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/lib/CommandBase.java @@ -0,0 +1,78 @@ +package com.pretzel.dev.villagertradelimiter.lib; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +public class CommandBase implements CommandExecutor, TabCompleter { + private final String name; + private final String permission; + private final Callback callback; + private final ArrayList subs; + + public CommandBase(String name, String permission, Callback callback) { + this.name = name; + this.permission = permission; + this.callback = callback; + this.subs = new ArrayList<>(); + } + + public CommandBase addSub(CommandBase command) { + this.subs.add(command); + return command; + } + + @Override + public boolean onCommand(final CommandSender sender, final Command command, final String alias, final String[] args) { + final Player player = (sender instanceof Player ? (Player)sender : null); + if(player != null && !player.hasPermission(this.permission) && !this.permission.isEmpty()) return false; + + if(args.length == 0) { + this.callback.call(player); + return true; + } + + final String[] args2 = getCopy(args); + for(CommandBase cmd : subs) + if(cmd.getName().equalsIgnoreCase(args[0]) && cmd.onCommand(sender, command, alias, args2)) + return true; + + sender.sendMessage(Util.replaceColors("&fUnknown command. Type \"/help\" for help.")); + return true; + } + + @Override + public List onTabComplete(final CommandSender sender, final Command command, final String alias, final String[] args) { + final Player player = (sender instanceof Player ? (Player)sender : null); + if(player == null) return null; + + final List list = new ArrayList<>(); + if(args.length == 0) return null; + if(args.length == 1) { + for(CommandBase cmd : subs) + if(player.hasPermission(cmd.getPermission())) + list.add(cmd.getName()); + } else { + final String[] args2 = getCopy(args); + for(CommandBase cmd : subs) { + List list2 = cmd.onTabComplete(sender, command, alias, args2); + if(list2 != null) list.addAll(list2); + } + } + return list; + } + + private static String[] getCopy(final String[] args) { + String[] res = new String[args.length-1]; + System.arraycopy(args, 1, res, 0, res.length); + return res; + } + + public String getName() { return this.name; } + public String getPermission() { return this.permission; } +} \ No newline at end of file diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/ConfigUpdater.java b/src/com/pretzel/dev/villagertradelimiter/lib/ConfigUpdater.java new file mode 100644 index 0000000..a6ec04e --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/lib/ConfigUpdater.java @@ -0,0 +1,84 @@ +package com.pretzel.dev.villagertradelimiter.lib; + +import java.io.File; +import java.io.FileReader; +import java.io.Reader; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +public class ConfigUpdater { + private final String[] cfgDefault; + private String[] cfgActive; + + public ConfigUpdater(final Reader def, final File active) { + this.cfgDefault = Util.readFile(def); + this.cfgActive = this.cfgDefault; + try { + this.cfgActive = Util.readFile(new FileReader(active)); + } catch (Exception e) { + Util.errorMsg(e); + } + } + + private String getVersion(String[] cfg) { + for(String x : cfg) + if(x.startsWith("#") && x.endsWith("#") && x.contains("Version: ")) + return x.split(": ")[1].replace("#", "").trim(); + return "0"; + } + + public FileConfiguration updateConfig(File file, String prefix) { + final FileConfiguration cfg = (FileConfiguration)YamlConfiguration.loadConfiguration(file); + if(this.isUpdated()) return cfg; + Util.consoleMsg(prefix+"Updating config.yml..."); + + String out = ""; + for(int i = 0; i < cfgDefault.length; i++) { + String line = cfgDefault[i]; + if(line.startsWith("#") || line.replace(" ", "").isEmpty()) { + if(!line.startsWith(" ")) out += line+"\n"; + } else if(!line.startsWith(" ")) { + if(line.contains(": ")) { + out += matchActive(line.split(": ")[0], line)+"\n"; + } else if(line.contains(":")) { + String set = matchActive(line, ""); + if(set.contains("none")) { + out += set+"\n"; + continue; + } + out += line+"\n"; + boolean found = false; + for(int j = 0; j < cfgActive.length; j++) { + String line2 = cfgActive[j]; + if(line2.startsWith(" ") && !line2.replace(" ", "").isEmpty()) { + out += line2+"\n"; + found = true; + } + } + if(found == false) { + while(i < cfgDefault.length-1) { + i++; + String line2 = cfgDefault[i]; + out += line2+"\n"; + if(!line2.startsWith(" ")) break; + } + } + } + } + } + Util.writeFile(file, out+"\n"); + return (FileConfiguration)YamlConfiguration.loadConfiguration(file); + } + + public boolean isUpdated() { + return getVersion(cfgActive).contains(getVersion(cfgDefault)); + } + + private String matchActive(String start, String def) { + for(String x : cfgActive) + if(x.startsWith(start)) + return x; + return def; + } +} \ No newline at end of file diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/Metrics.java b/src/com/pretzel/dev/villagertradelimiter/lib/Metrics.java new file mode 100644 index 0000000..ab690f0 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/lib/Metrics.java @@ -0,0 +1,848 @@ +package com.pretzel.dev.villagertradelimiter.lib; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HttpsURLConnection; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +public class Metrics { + + private final Plugin plugin; + + private final MetricsBase metricsBase; + + /** + * Creates a new Metrics instance. + * + * @param plugin Your plugin instance. + * @param serviceId The id of the service. It can be found at What is my plugin id? + */ + public Metrics(JavaPlugin plugin, int serviceId) { + this.plugin = plugin; + // Get the config file + File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); + File configFile = new File(bStatsFolder, "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + if (!config.isSet("serverUuid")) { + config.addDefault("enabled", true); + config.addDefault("serverUuid", UUID.randomUUID().toString()); + config.addDefault("logFailedRequests", false); + config.addDefault("logSentData", false); + config.addDefault("logResponseStatusText", false); + // Inform the server owners about bStats + config + .options() + .header( + "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" + + "many people use their plugin and their total player count. It's recommended to keep bStats\n" + + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" + + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" + + "anonymous.") + .copyDefaults(true); + try { + config.save(configFile); + } catch (IOException ignored) { + } + } + // Load the data + boolean enabled = config.getBoolean("enabled", true); + String serverUUID = config.getString("serverUuid"); + boolean logErrors = config.getBoolean("logFailedRequests", false); + boolean logSentData = config.getBoolean("logSentData", false); + boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); + metricsBase = + new MetricsBase( + "bukkit", + serverUUID, + serviceId, + enabled, + this::appendPlatformData, + this::appendServiceData, + submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), + plugin::isEnabled, + (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), + (message) -> this.plugin.getLogger().log(Level.INFO, message), + logErrors, + logSentData, + logResponseStatusText); + } + + /** + * Adds a custom chart. + * + * @param chart The chart to add. + */ + public void addCustomChart(CustomChart chart) { + metricsBase.addCustomChart(chart); + } + + private void appendPlatformData(JsonObjectBuilder builder) { + builder.appendField("playerAmount", getPlayerAmount()); + builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); + builder.appendField("bukkitVersion", Bukkit.getVersion()); + builder.appendField("bukkitName", Bukkit.getName()); + builder.appendField("javaVersion", System.getProperty("java.version")); + builder.appendField("osName", System.getProperty("os.name")); + builder.appendField("osArch", System.getProperty("os.arch")); + builder.appendField("osVersion", System.getProperty("os.version")); + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); + } + + private void appendServiceData(JsonObjectBuilder builder) { + builder.appendField("pluginVersion", plugin.getDescription().getVersion()); + } + + private int getPlayerAmount() { + try { + // Around MC 1.8 the return type was changed from an array to a collection, + // This fixes java.lang.NoSuchMethodError: + // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; + Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); + return onlinePlayersMethod.getReturnType().equals(Collection.class) + ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() + : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; + } catch (Exception e) { + // Just use the new method if the reflection failed + return Bukkit.getOnlinePlayers().size(); + } + } + + public static class MetricsBase { + + /** The version of the Metrics class. */ + public static final String METRICS_VERSION = "2.2.1"; + + private static final ScheduledExecutorService scheduler = + Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics")); + + private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; + + private final String platform; + + private final String serverUuid; + + private final int serviceId; + + private final Consumer appendPlatformDataConsumer; + + private final Consumer appendServiceDataConsumer; + + private final Consumer submitTaskConsumer; + + private final Supplier checkServiceEnabledSupplier; + + private final BiConsumer errorLogger; + + private final Consumer infoLogger; + + private final boolean logErrors; + + private final boolean logSentData; + + private final boolean logResponseStatusText; + + private final Set customCharts = new HashSet<>(); + + private final boolean enabled; + + /** + * Creates a new MetricsBase class instance. + * + * @param platform The platform of the service. + * @param serviceId The id of the service. + * @param serverUuid The server uuid. + * @param enabled Whether or not data sending is enabled. + * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all platform-specific data. + * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and + * appends all service-specific data. + * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be + * used to delegate the data collection to a another thread to prevent errors caused by + * concurrency. Can be {@code null}. + * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. + * @param errorLogger A consumer that accepts log message and an error. + * @param infoLogger A consumer that accepts info log messages. + * @param logErrors Whether or not errors should be logged. + * @param logSentData Whether or not the sent data should be logged. + * @param logResponseStatusText Whether or not the response status text should be logged. + */ + public MetricsBase( + String platform, + String serverUuid, + int serviceId, + boolean enabled, + Consumer appendPlatformDataConsumer, + Consumer appendServiceDataConsumer, + Consumer submitTaskConsumer, + Supplier checkServiceEnabledSupplier, + BiConsumer errorLogger, + Consumer infoLogger, + boolean logErrors, + boolean logSentData, + boolean logResponseStatusText) { + this.platform = platform; + this.serverUuid = serverUuid; + this.serviceId = serviceId; + this.enabled = enabled; + this.appendPlatformDataConsumer = appendPlatformDataConsumer; + this.appendServiceDataConsumer = appendServiceDataConsumer; + this.submitTaskConsumer = submitTaskConsumer; + this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; + this.errorLogger = errorLogger; + this.infoLogger = infoLogger; + this.logErrors = logErrors; + this.logSentData = logSentData; + this.logResponseStatusText = logResponseStatusText; + checkRelocation(); + if (enabled) { + startSubmitting(); + } + } + + public void addCustomChart(CustomChart chart) { + this.customCharts.add(chart); + } + + private void startSubmitting() { + final Runnable submitTask = + () -> { + if (!enabled || !checkServiceEnabledSupplier.get()) { + // Submitting data or service is disabled + scheduler.shutdown(); + return; + } + if (submitTaskConsumer != null) { + submitTaskConsumer.accept(this::submitData); + } else { + this.submitData(); + } + }; + // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution + // of requests on the + // bStats backend. To circumvent this problem, we introduce some randomness into the initial + // and second delay. + // WARNING: You must not modify and part of this Metrics class, including the submit delay or + // frequency! + // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! + long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); + long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); + scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate( + submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); + } + + private void submitData() { + final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); + appendPlatformDataConsumer.accept(baseJsonBuilder); + final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); + appendServiceDataConsumer.accept(serviceJsonBuilder); + JsonObjectBuilder.JsonObject[] chartData = + customCharts.stream() + .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) + .filter(Objects::nonNull) + .toArray(JsonObjectBuilder.JsonObject[]::new); + serviceJsonBuilder.appendField("id", serviceId); + serviceJsonBuilder.appendField("customCharts", chartData); + baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); + baseJsonBuilder.appendField("serverUUID", serverUuid); + baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); + JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); + scheduler.execute( + () -> { + try { + // Send the data + sendData(data); + } catch (Exception e) { + // Something went wrong! :( + if (logErrors) { + errorLogger.accept("Could not submit bStats metrics data", e); + } + } + }); + } + + private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { + if (logSentData) { + infoLogger.accept("Sent bStats metrics data: " + data.toString()); + } + String url = String.format(REPORT_URL, platform); + HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); + // Compress the data to save bandwidth + byte[] compressedData = compress(data.toString()); + connection.setRequestMethod("POST"); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.addRequestProperty("Content-Encoding", "gzip"); + connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", "Metrics-Service/1"); + connection.setDoOutput(true); + try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { + outputStream.write(compressedData); + } + StringBuilder builder = new StringBuilder(); + try (BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + builder.append(line); + } + } + if (logResponseStatusText) { + infoLogger.accept("Sent data to bStats and received response: " + builder); + } + } + + /** Checks that the class was properly relocated. */ + private void checkRelocation() { + // You can use the property to disable the check in your test environment + if (System.getProperty("bstats.relocatecheck") == null + || !System.getProperty("bstats.relocatecheck").equals("false")) { + // Maven's Relocate is clever and changes strings, too. So we have to use this little + // "trick" ... :D + final String defaultPackage = + new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); + final String examplePackage = + new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); + // We want to make sure no one just copy & pastes the example and uses the wrong package + // names + if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) + || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { + throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); + } + } + } + + /** + * Gzips the given string. + * + * @param str The string to gzip. + * @return The gzipped string. + */ + private static byte[] compress(final String str) throws IOException { + if (str == null) { + return null; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + } + return outputStream.toByteArray(); + } + } + + public static class AdvancedBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue().length == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class SimpleBarChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimpleBarChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + for (Map.Entry entry : map.entrySet()) { + valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class MultiLineChart extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public MultiLineChart(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public static class AdvancedPie extends CustomChart { + + private final Callable> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public AdvancedPie(String chartId, Callable> callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean allSkipped = true; + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() == 0) { + // Skip this invalid + continue; + } + allSkipped = false; + valuesBuilder.appendField(entry.getKey(), entry.getValue()); + } + if (allSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + public abstract static class CustomChart { + + private final String chartId; + + protected CustomChart(String chartId) { + if (chartId == null) { + throw new IllegalArgumentException("chartId must not be null"); + } + this.chartId = chartId; + } + + public JsonObjectBuilder.JsonObject getRequestJsonObject( + BiConsumer errorLogger, boolean logErrors) { + JsonObjectBuilder builder = new JsonObjectBuilder(); + builder.appendField("chartId", chartId); + try { + JsonObjectBuilder.JsonObject data = getChartData(); + if (data == null) { + // If the data is null we don't send the chart. + return null; + } + builder.appendField("data", data); + } catch (Throwable t) { + if (logErrors) { + errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); + } + return null; + } + return builder.build(); + } + + protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; + } + + public static class SingleLineChart extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SingleLineChart(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + int value = callable.call(); + if (value == 0) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + public static class SimplePie extends CustomChart { + + private final Callable callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public SimplePie(String chartId, Callable callable) { + super(chartId); + this.callable = callable; + } + + @Override + protected JsonObjectBuilder.JsonObject getChartData() throws Exception { + String value = callable.call(); + if (value == null || value.isEmpty()) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("value", value).build(); + } + } + + public static class DrilldownPie extends CustomChart { + + private final Callable>> callable; + + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */ + public DrilldownPie(String chartId, Callable>> callable) { + super(chartId); + this.callable = callable; + } + + @Override + public JsonObjectBuilder.JsonObject getChartData() throws Exception { + JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); + Map> map = callable.call(); + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null; + } + boolean reallyAllSkipped = true; + for (Map.Entry> entryValues : map.entrySet()) { + JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); + boolean allSkipped = true; + for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { + valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); + allSkipped = false; + } + if (!allSkipped) { + reallyAllSkipped = false; + valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); + } + } + if (reallyAllSkipped) { + // Null = skip the chart + return null; + } + return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); + } + } + + /** + * An extremely simple JSON builder. + * + *

While this class is neither feature-rich nor the most performant one, it's sufficient enough + * for its use-case. + */ + public static class JsonObjectBuilder { + + private StringBuilder builder = new StringBuilder(); + + private boolean hasAtLeastOneField = false; + + public JsonObjectBuilder() { + builder.append("{"); + } + + /** + * Appends a null field to the JSON. + * + * @param key The key of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendNull(String key) { + appendFieldUnescaped(key, "null"); + return this; + } + + /** + * Appends a string field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String value) { + if (value == null) { + throw new IllegalArgumentException("JSON value must not be null"); + } + appendFieldUnescaped(key, "\"" + escape(value) + "\""); + return this; + } + + /** + * Appends an integer field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int value) { + appendFieldUnescaped(key, String.valueOf(value)); + return this; + } + + /** + * Appends an object to the JSON. + * + * @param key The key of the field. + * @param object The object. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject object) { + if (object == null) { + throw new IllegalArgumentException("JSON object must not be null"); + } + appendFieldUnescaped(key, object.toString()); + return this; + } + + /** + * Appends a string array to the JSON. + * + * @param key The key of the field. + * @param values The string array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, String[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values) + .map(value -> "\"" + escape(value) + "\"") + .collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an integer array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, int[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends an object array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + public JsonObjectBuilder appendField(String key, JsonObject[] values) { + if (values == null) { + throw new IllegalArgumentException("JSON values must not be null"); + } + String escapedValues = + Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); + appendFieldUnescaped(key, "[" + escapedValues + "]"); + return this; + } + + /** + * Appends a field to the object. + * + * @param key The key of the field. + * @param escapedValue The escaped value of the field. + */ + private void appendFieldUnescaped(String key, String escapedValue) { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + if (key == null) { + throw new IllegalArgumentException("JSON key must not be null"); + } + if (hasAtLeastOneField) { + builder.append(","); + } + builder.append("\"").append(escape(key)).append("\":").append(escapedValue); + hasAtLeastOneField = true; + } + + /** + * Builds the JSON string and invalidates this builder. + * + * @return The built JSON string. + */ + public JsonObject build() { + if (builder == null) { + throw new IllegalStateException("JSON has already been built"); + } + JsonObject object = new JsonObject(builder.append("}").toString()); + builder = null; + return object; + } + + /** + * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. + * + *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. + * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). + * + * @param value The value to escape. + * @return The escaped value. + */ + private static String escape(String value) { + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '"') { + builder.append("\\\""); + } else if (c == '\\') { + builder.append("\\\\"); + } else if (c <= '\u000F') { + builder.append("\\u000").append(Integer.toHexString(c)); + } else if (c <= '\u001F') { + builder.append("\\u00").append(Integer.toHexString(c)); + } else { + builder.append(c); + } + } + return builder.toString(); + } + + /** + * A super simple representation of a JSON object. + * + *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not + * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, + * JsonObject)}. + */ + public static class JsonObject { + + private final String value; + + private JsonObject(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/Util.java b/src/com/pretzel/dev/villagertradelimiter/lib/Util.java new file mode 100644 index 0000000..9d53837 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/lib/Util.java @@ -0,0 +1,151 @@ +package com.pretzel.dev.villagertradelimiter.lib; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.entity.Villager; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; + +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 a player if they have permission + public static void sendIfPermitted(String perm, String msg, Player player) { + if(player.hasPermission(perm)) player.sendMessage(msg); + } + + //Sends a message to the console + public static void consoleMsg(String msg) { + if(msg != null) Bukkit.getServer().getConsoleSender().sendMessage(msg); + } + + public static String replaceColors(String in) { + if(in == null) return null; + return in.replace("&", "\u00A7"); + } + + //Sends an error message to the console + public static void errorMsg(Exception e) { + String error = e.toString(); + for(StackTraceElement x : e.getStackTrace()) { + error += "\n\t"+x.toString(); + } + consoleMsg(ChatColor.RED+"ERROR: "+error); + } + + //Returns whether a player is a Citizens NPC or not + public static boolean isNPC(Player player) { + return player.hasMetadata("NPC"); + } + + //Returns whether a villager is a Citizens NPC or not + public static boolean isNPC(Villager villager) { + return villager.hasMetadata("NPC"); + } + + //Converts an int array to a string + public static String intArrayToString(int[] arr) { + String res = ""; + for(int a : arr) { res += a+""; } + return res; + } + + public static String[] readFile(Reader reader) { + String out = ""; + BufferedReader br = null; + try { + br = new BufferedReader(reader); + String line; + while((line = br.readLine()) != null) + out += line+"\n"; + br.close(); + return out.split("\n"); + } catch(Exception e) { + errorMsg(e); + } + return null; + } + public static String[] readFile(File file) { + try { + return readFile(new FileReader(file)); + } catch(Exception e) { + errorMsg(e); + return null; + } + } + + public static void writeFile(File file, String out) { + BufferedWriter bw = null; + try { + bw = new BufferedWriter(new FileWriter(file)); + bw.write(out); + bw.close(); + } catch(Exception e) { + errorMsg(e); + } + } + + public static final String stacksToBase64(final ItemStack[] contents) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); + + dataOutput.writeInt(contents.length); + for (ItemStack stack : contents) dataOutput.writeObject(stack); + dataOutput.close(); + return Base64Coder.encodeLines(outputStream.toByteArray()).replace("\n", "").replace("\r", ""); + } catch (Exception e) { + throw new IllegalStateException("Unable to save item stacks.", e); + } + } + + public static final ItemStack[] stacksFromBase64(final String data) { + if (data == null || Base64Coder.decodeLines(data).equals(null)) + return new ItemStack[]{}; + + ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data)); + BukkitObjectInputStream dataInput = null; + ItemStack[] stacks = null; + + try { + dataInput = new BukkitObjectInputStream(inputStream); + stacks = new ItemStack[dataInput.readInt()]; + } catch (IOException e) { + Util.errorMsg(e); + } + + for (int i = 0; i < stacks.length; i++) { + try { + stacks[i] = (ItemStack) dataInput.readObject(); + } catch (IOException | ClassNotFoundException e) { + try { dataInput.close(); } + catch (IOException ignored) {} + Util.errorMsg(e); + return null; + } + } + + try { dataInput.close(); } + catch (IOException ignored) {} + + return stacks; + } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java b/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java new file mode 100644 index 0000000..1813287 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java @@ -0,0 +1,196 @@ +package com.pretzel.dev.villagertradelimiter.listeners; + +import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter; +import com.pretzel.dev.villagertradelimiter.lib.Util; +import com.pretzel.dev.villagertradelimiter.nms.*; +import org.bukkit.Bukkit; +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.entity.Player; +import org.bukkit.entity.Villager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.inventory.MerchantRecipe; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.List; + +public class PlayerListener implements Listener { + private static final Material[] MATERIALS = new Material[] { Material.IRON_HELMET, Material.IRON_CHESTPLATE, Material.IRON_LEGGINGS, Material.IRON_BOOTS, Material.BELL, Material.CHAINMAIL_HELMET, Material.CHAINMAIL_CHESTPLATE, Material.CHAINMAIL_LEGGINGS, Material.CHAINMAIL_BOOTS, Material.SHIELD, Material.DIAMOND_HELMET, Material.DIAMOND_CHESTPLATE, Material.DIAMOND_LEGGINGS, Material.DIAMOND_BOOTS, Material.FILLED_MAP, Material.FISHING_ROD, Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE, Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS, Material.LEATHER_HORSE_ARMOR, Material.SADDLE, Material.ENCHANTED_BOOK, Material.STONE_AXE, Material.STONE_SHOVEL, Material.STONE_PICKAXE, Material.STONE_HOE, Material.IRON_AXE, Material.IRON_SHOVEL, Material.IRON_PICKAXE, Material.DIAMOND_AXE, Material.DIAMOND_SHOVEL, Material.DIAMOND_PICKAXE, Material.DIAMOND_HOE, Material.IRON_SWORD, Material.DIAMOND_SWORD }; + + private VillagerTradeLimiter instance; + private NMS nms; + + public PlayerListener(VillagerTradeLimiter instance) { + this.instance = instance; + this.nms = new NMS(Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]); + } + + @EventHandler + public void onPlayerInteract(PlayerInteractEntityEvent event) { + if(!(event.getRightClicked() instanceof Villager)) return; + final Villager villager = (Villager)event.getRightClicked(); + if(Util.isNPC(villager)) return; //Skips NPCs + if(villager.getProfession() == Villager.Profession.NONE || villager.getProfession() == Villager.Profession.NITWIT) return; //Skips non-trading villagers + if(villager.getRecipeCount() == 0) return; //Skips non-trading villagers + if(instance.getCfg().getBoolean("DisableTrading", false)) { + event.setCancelled(true); + return; + } + + final Player player = event.getPlayer(); + if(Util.isNPC(player)) return; //Skips NPCs + this.hotv(player); + this.maxDiscount(villager, player); + this.maxDemand(villager); + } + + private void hotv(final Player player) { + final PotionEffectType effect = PotionEffectType.HERO_OF_THE_VILLAGE; + if(!player.hasPotionEffect(effect)) return; //Skips when player doesn't have HotV + + final int maxHeroLevel = instance.getCfg().getInt("MaxHeroLevel", 1); + if(maxHeroLevel == 0) player.removePotionEffect(effect); + if(maxHeroLevel <= 0) return; //Skips when disabled in config.yml + + final PotionEffect pot = player.getPotionEffect(effect); + if(pot.getAmplifier() > maxHeroLevel-1) { + player.removePotionEffect(effect); + player.addPotionEffect(new PotionEffect(effect, pot.getDuration(), maxHeroLevel-1)); + } + } + + private void maxDiscount(final Villager villager, final Player player) { + final List recipes = villager.getRecipes(); + int a = 0, b = 0, c = 0, d = 0, e = 0; + + final NBTContainer vnbt = new NBTContainer(this.nms, villager); + final NBTTagList gossips = new NBTTagList(this.nms, vnbt.getTag().get("Gossips")); + final NBTContainer pnbt = new NBTContainer(this.nms, player); + final String puuid = Util.intArrayToString(pnbt.getTag().getIntArray("UUID")); + + for (int i = 0; i < gossips.size(); ++i) { + final NBTTagCompound gossip = gossips.getCompound(i); + final String type = gossip.getString("Type"); + final String tuuid = Util.intArrayToString(gossip.getIntArray("Target")); + final int value = gossip.getInt("Value"); + if (tuuid.equals(puuid)) { + switch(type) { + case "trading": c = value; break; + case "minor_positive": b = value; break; + case "minor_negative": d = value; break; + case "major_positive": a = value; break; + case "major_negative": e = value; break; + default: break; + } + } + } + final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides"); + for (final MerchantRecipe recipe : recipes) { + final int x = recipe.getIngredients().get(0).getAmount(); + final float p0 = this.getPriceMultiplier(recipe); + final int w = 5 * a + b + c - d - 5 * e; + final float y = x - p0 * w; + double maxDiscount = instance.getCfg().getDouble("MaxDiscount", 0.3); + if(overrides != null) { + for (final String k : overrides.getKeys(false)) { + final ConfigurationSection item = this.getItem(recipe, k); + if (item != null) { + maxDiscount = item.getDouble("MaxDiscount", maxDiscount); + break; + } + } + } + if(maxDiscount >= 0.0 && maxDiscount <= 1.0) { + if(y < x * (1.0 - maxDiscount) && y != x) { + recipe.setPriceMultiplier(x * (float)maxDiscount / w); + } else { + recipe.setPriceMultiplier(p0); + } + } else { + recipe.setPriceMultiplier(p0); + } + } + } + + private void maxDemand(final Villager villager) { + List recipes = villager.getRecipes(); + final NBTContainer vnbt = new NBTContainer(this.nms, villager); + final NBTTagCompound vtag = vnbt.getTag(); + final NBTTagList recipes2 = new NBTTagList(this.nms, vtag.getCompound("Offers").get("Recipes")); + + final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides"); + for(int i = 0; i < recipes2.size(); ++i) { + final NBTTagCompound recipe2 = recipes2.getCompound(i); + final int demand = recipe2.getInt("demand"); + int maxDemand = instance.getCfg().getInt("MaxDemand", -1); + if(overrides != null) { + for(final String k : overrides.getKeys(false)) { + final ConfigurationSection item = this.getItem(recipes.get(i), k); + if(item != null) { + maxDemand = item.getInt("MaxDemand", maxDemand); + break; + } + } + } + if(maxDemand >= 0 && demand > maxDemand) { + recipe2.setInt("demand", maxDemand); + } + } + villager.getInventory().clear(); + vnbt.saveTag(villager, vtag); + } + + private float getPriceMultiplier(final MerchantRecipe recipe) { + float p = 0.05f; + final Material type = recipe.getResult().getType(); + for(int length = MATERIALS.length, i = 0; i < length; ++i) { + if(type == MATERIALS[i]) { + p = 0.2f; + break; + } + } + return p; + } + + private ConfigurationSection getItem(final MerchantRecipe recipe, final String k) { + final ConfigurationSection item = instance.getCfg().getConfigurationSection("Overrides."+k); + if(item == null) return null; + + if(!k.contains("_")) { + //Return the item if the item name is valid + if(this.verify(recipe, Material.matchMaterial(k))) return item; + return null; + } + + final String[] words = k.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.getResult().getType() != Material.ENCHANTED_BOOK) return null; + final EnchantmentStorageMeta meta = (EnchantmentStorageMeta)recipe.getResult().getItemMeta(); + final Enchantment enchantment = EnchantmentWrapper.getByKey(NamespacedKey.minecraft(k.substring(0, k.lastIndexOf("_")))); + if(meta.hasStoredEnchant(enchantment) && meta.getStoredEnchantLevel(enchantment) == level) return item; + return null; + } catch(NumberFormatException e) { + //Return the item if the item name is valid + if(this.verify(recipe, Material.matchMaterial(k))) return item; + return null; + } catch(Exception e2) { + //Send an error message + Util.errorMsg(e2); + return null; + } + } + + //Verifies that an item exists in the villager's trade + private boolean verify(final MerchantRecipe recipe, final Material material) { + return ((recipe.getResult().getType() == material) || (recipe.getIngredients().get(0).getType() == material)); + } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/nms/CraftEntity.java b/src/com/pretzel/dev/villagertradelimiter/nms/CraftEntity.java new file mode 100644 index 0000000..a14f61a --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/nms/CraftEntity.java @@ -0,0 +1,23 @@ +package com.pretzel.dev.villagertradelimiter.nms; + +import com.pretzel.dev.villagertradelimiter.lib.Util; +import org.bukkit.entity.Entity; + +public class CraftEntity { + private final NMS nms; + private final Class c; + + public CraftEntity(final NMS nms) { + this.nms = nms; + this.c = nms.getCraftBukkitClass("entity.CraftEntity"); + } + + public Object getHandle(final Entity entity) { + try { + return nms.getMethod(this.c, "getHandle").invoke(this.c.cast(entity)); + } catch (Exception e) { + Util.errorMsg(e); + return null; + } + } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/nms/NBTContainer.java b/src/com/pretzel/dev/villagertradelimiter/nms/NBTContainer.java new file mode 100644 index 0000000..9cebdfe --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/nms/NBTContainer.java @@ -0,0 +1,44 @@ +package com.pretzel.dev.villagertradelimiter.nms; + +import com.pretzel.dev.villagertradelimiter.lib.Util; +import org.bukkit.entity.Entity; + +public class NBTContainer { + private final NMS nms; + private final Entity entity; + private final NBTTagCompound tag; + + public NBTContainer(final NMS nms, final Entity entity) { + this.nms = nms; + this.entity = entity; + this.tag = this.loadTag(); + } + + public NBTTagCompound loadTag() { + final CraftEntity craftEntity = new CraftEntity(nms); + final NMSEntity nmsEntity = new NMSEntity(nms); + final Class tgc; + if(nms.getVersion().compareTo("1.17_R1") < 0) + tgc = nms.getNMSClass("server."+nms.getVersion()+".NBTTagCompound"); + else + tgc = nms.getNMSClass("nbt.NBTTagCompound"); + try { + final NBTTagCompound tag = new NBTTagCompound(nms, tgc.getDeclaredConstructor().newInstance()); + nmsEntity.save(craftEntity.getHandle(this.entity), tag); + return tag; + } catch (Exception e) { + Util.errorMsg(e); + return null; + } + } + + public void saveTag(final Entity entity, final NBTTagCompound tag) { + final CraftEntity craftEntity = new CraftEntity(nms); + final NMSEntity nmsEntity = new NMSEntity(nms); + nmsEntity.load(craftEntity.getHandle(entity), tag); + } + + public NBTTagCompound getTag() { + return this.tag; + } +} \ No newline at end of file diff --git a/src/com/pretzel/dev/villagertradelimiter/nms/NBTTagCompound.java b/src/com/pretzel/dev/villagertradelimiter/nms/NBTTagCompound.java new file mode 100644 index 0000000..a8a8d73 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/nms/NBTTagCompound.java @@ -0,0 +1,71 @@ +package com.pretzel.dev.villagertradelimiter.nms; + +import com.pretzel.dev.villagertradelimiter.lib.Util; + +public class NBTTagCompound { + private final NMS nms; + private final Class c; + private final Object self; + + public NBTTagCompound(final NMS nms, final Object self) { + this.nms = nms; + this.self = self; + this.c = self.getClass(); + } + + public Object get(final String key) { + try { + return this.nms.getMethod(this.c, "get", String.class).invoke(this.self, key); + } catch (Exception e) { + Util.errorMsg(e); + return null; + } + } + + public String getString(final String key) { + try { + return (String)this.nms.getMethod(this.c, "getString", String.class).invoke(this.self, key); + } catch (Exception e) { + Util.errorMsg(e); + return null; + } + } + + public int getInt(final String key) { + try { + return (int)this.nms.getMethod(this.c, "getInt", String.class).invoke(this.self, key); + } catch (Exception e) { + Util.errorMsg(e); + return Integer.MIN_VALUE; + } + } + + public int[] getIntArray(final String key) { + try { + return (int[])this.nms.getMethod(this.c, "getIntArray", String.class).invoke(this.self, key); + } catch (Exception e) { + Util.errorMsg(e); + return null; + } + } + + public NBTTagCompound getCompound(final String key) { + try { + return new NBTTagCompound(this.nms, this.nms.getMethod(this.self.getClass(), "getCompound", String.class).invoke(this.self, key)); + } catch (Exception e) { + Util.errorMsg(e); + return null; + } + } + + public void setInt(final String key, final int value) { + try { + this.nms.getMethod(this.c, "setInt", String.class, Integer.TYPE).invoke(this.self, key, value); + } catch (Exception e) { + Util.errorMsg(e); + } + } + + public Object getSelf() { return this.self; } + public Class getC() { return this.c; } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/nms/NBTTagList.java b/src/com/pretzel/dev/villagertradelimiter/nms/NBTTagList.java new file mode 100644 index 0000000..39b62bd --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/nms/NBTTagList.java @@ -0,0 +1,41 @@ +package com.pretzel.dev.villagertradelimiter.nms; + +import com.pretzel.dev.villagertradelimiter.lib.Util; + +public class NBTTagList +{ + private final NMS nms; + private final Object self; + private final Class c; + + public NBTTagList(final NMS nms, final Object self) { + this.nms = nms; + this.self = self; + if(nms.getVersion().compareTo("1.17_R1") < 0) + this.c = nms.getNMSClass("server."+nms.getVersion()+".NBTTagList"); + else + this.c = nms.getNMSClass("nbt.NBTTagList"); + } + + public NBTTagCompound getCompound(final int index) { + try { + return new NBTTagCompound(this.nms, this.nms.getMethod(this.c, "getCompound", Integer.TYPE).invoke(this.self, index)); + } catch (Exception e) { + Util.errorMsg(e); + return null; + } + } + + public int size() { + try { + return (int)this.nms.getMethod(this.c, "size").invoke(this.self, new Object[0]); + } catch (Exception e) { + Util.errorMsg(e); + return -1; + } + } + + public Object getSelf() { + return this.self; + } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/nms/NMS.java b/src/com/pretzel/dev/villagertradelimiter/nms/NMS.java new file mode 100644 index 0000000..8235a34 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/nms/NMS.java @@ -0,0 +1,81 @@ +package com.pretzel.dev.villagertradelimiter.nms; + +import com.pretzel.dev.villagertradelimiter.lib.Util; +import java.lang.reflect.Method; +import java.util.HashMap; + +public class NMS +{ + private final HashMap> nmsClasses; + private final HashMap> bukkitClasses; + private final HashMap methods; + private final String version; + + public NMS(final String version) { + this.nmsClasses = new HashMap<>(); + this.bukkitClasses = new HashMap<>(); + this.methods = new HashMap<>(); + this.version = version; + } + + public Class getNMSClass(final String name) { + if(this.nmsClasses.containsKey(name)) { + return this.nmsClasses.get(name); + } + + try { + Class c = Class.forName("net.minecraft."+name); + this.nmsClasses.put(name, c); + return c; + } catch (Exception e) { + Util.errorMsg(e); + return this.nmsClasses.put(name, null); + } + } + + public Class getCraftBukkitClass(final String name) { + if(this.bukkitClasses.containsKey(name)) { + return this.bukkitClasses.get(name); + } + + try { + Class c = Class.forName("org.bukkit.craftbukkit." + this.version + "." + name); + this.bukkitClasses.put(name, c); + return c; + } catch (Exception e) { + Util.errorMsg(e); + return this.bukkitClasses.put(name, null); + } + } + + public Method getMethod(final Class invoker, final String name) throws NoSuchMethodException { + return this.getMethod(invoker, name, null, null); + } + + public Method getMethod(final Class invoker, final String name, final Class type) throws NoSuchMethodException { + return this.getMethod(invoker, name, type, null); + } + + public Method getMethod(final Class invoker, final String name, final Class type, final Class type2) throws NoSuchMethodException { + if(this.methods.containsKey(name) && this.methods.get(name).getDeclaringClass().equals(invoker)) { + return this.methods.get(name); + } + Method method; + try { + if(type2 != null) { + method = invoker.getMethod(name, type, type2); + } else if(type != null) { + method = invoker.getMethod(name, type); + } else { + method = invoker.getMethod(name); + } + } catch (Exception e) { + Util.errorMsg(e); + return this.methods.put(name, null); + } + this.methods.put(name, method); + return method; + } + + public String getVersion() { return this.version; } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/nms/NMSEntity.java b/src/com/pretzel/dev/villagertradelimiter/nms/NMSEntity.java new file mode 100644 index 0000000..d37481e --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/nms/NMSEntity.java @@ -0,0 +1,38 @@ +package com.pretzel.dev.villagertradelimiter.nms; + +import com.pretzel.dev.villagertradelimiter.lib.Util; + +public class NMSEntity { + private final NMS nms; + private final Class c; + + public NMSEntity(final NMS nms) { + this.nms = nms; + if(nms.getVersion().compareTo("1.17_R1") < 0) + this.c = nms.getNMSClass("server."+nms.getVersion()+".Entity"); + else + this.c = nms.getNMSClass("world.entity.Entity"); + } + + public void save(final Object nmsEntity, final NBTTagCompound tag) { + try { + nms.getMethod(this.c, "save", tag.getC()).invoke(nmsEntity, tag.getSelf()); + } catch (Exception e) { + Util.errorMsg(e); + } + } + + public void load(final Object nmsEntity, final NBTTagCompound tag) { + try { + nms.getMethod(this.c, "load", tag.getC()).invoke(nmsEntity, tag.getSelf()); + } catch (NoSuchMethodException e) { + try { + nms.getMethod(this.c, "f", tag.getC()).invoke(nmsEntity, tag.getSelf()); + } catch (Exception e2) { + Util.errorMsg(e2); + } + } catch (Exception e3) { + Util.errorMsg(e3); + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..74514c4 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,50 @@ +#---------------------------------------------------------------------------------# +# VTL ~ VillagerTradeLimiter # +# Version: 1.2.0 # +# By: PretzelJohn # +#---------------------------------------------------------------------------------# + + +#-------------------------------- GLOBAL SETTINGS --------------------------------# +# This helps me keep track of what server versions are being used. Please leave this set to true. +bStats: true + +# Set this to true if you want to completely disable ALL villager trading. +DisableTrading: false + +# The maximum level of the "Hero of the Village" (HotV) effect that a player can have. This limits HotV price decreases. +# * 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 +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 +# * Set to a number between 0.0 and 1.0 to set the maximum discount a player can get. (NOTE: 30% = 0.3) +MaxDiscount: 0.3 + +# The maximum demand for all items. This limits demand-based price increases. +# * Set to -1 to disable this feature and keep vanilla behavior +# * Set to 0 or higher to set the maximum demand for all items +# WARNING: The previous demand cannot be recovered if it was higher than the MaxDemand. +MaxDemand: -1 + + +#-------------------------------- PER-ITEM SETTINGS --------------------------------# +# Override the global settings for individual items. To disable, set like this --> Overrides: none +# To enable, add items below! +# * Enchanted books must follow the format: enchantment_name_level (ex: mending_1) +# * All other items must follow the format: item_name (ex: stone_bricks) +# For each item you add, you can override MaxDiscount and/or MaxDemand. +Overrides: + mending_1: + MaxDiscount: 0.1 + MaxDemand: 36 + depth_strider_3: + MaxDiscount: 0.6 + name_tag: + MaxDiscount: -1.0 + MaxDemand: 60 + clock: + MaxDemand: 12 + paper: + MaxDiscount: 0.1 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..c081021 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,25 @@ +name: VillagerTradeLimiter +author: PretzelJohn +main: com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter +version: 1.2.0 +api-version: 1.14 + +commands: + villagertradelimiter: + description: Base command for VTL + usage: / + permission: villagertradelimiter.use + aliases: vtl +permissions: + villagertradelimiter.*: + description: Gives access to all commands. + children: + villagertradelimiter.use: true + villagertradelimiter.reload: true + default: op + villagertradelimiter.use: + description: Allows players to use VillagerTradeLimiter. + default: op + villagertradelimiter.reload: + description: Allows players to reload config.yml. + default: op \ No newline at end of file