diff --git a/pom.xml b/pom.xml index 8c61cce..7f5fe98 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.pretzel.dev VillagerTradeLimiter - 1.4.4 + 1.5.0 1.8 diff --git a/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java b/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java index 6226445..a8892c7 100644 --- a/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java +++ b/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java @@ -1,31 +1,37 @@ package com.pretzel.dev.villagertradelimiter; -import com.pretzel.dev.villagertradelimiter.lib.CommandBase; -import com.pretzel.dev.villagertradelimiter.lib.ConfigUpdater; +import com.pretzel.dev.villagertradelimiter.commands.CommandManager; +import com.pretzel.dev.villagertradelimiter.commands.CommandBase; +import com.pretzel.dev.villagertradelimiter.settings.ConfigUpdater; import com.pretzel.dev.villagertradelimiter.lib.Metrics; import com.pretzel.dev.villagertradelimiter.lib.Util; import com.pretzel.dev.villagertradelimiter.listeners.PlayerListener; +import com.pretzel.dev.villagertradelimiter.settings.Lang; import 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; import java.io.IOException; -import java.util.Collections; +import java.util.*; public class VillagerTradeLimiter extends JavaPlugin { public static final String PLUGIN_NAME = "VillagerTradeLimiter"; public static final String PREFIX = ChatColor.GOLD+"["+PLUGIN_NAME+"] "; + private static final int BSTATS_ID = 9829; //Settings private FileConfiguration cfg; + private Lang lang; + private CommandManager commandManager; + private PlayerListener playerListener; //Initial plugin load/unload public void onEnable() { //Initialize instance variables this.cfg = null; + this.commandManager = new CommandManager(this); //Copy default settings & load settings this.getConfig().options().copyDefaults(); @@ -34,6 +40,7 @@ public class VillagerTradeLimiter extends JavaPlugin { this.loadBStats(); //Register commands and listeners + this.playerListener = new PlayerListener(this); this.registerCommands(); this.registerListeners(); @@ -41,9 +48,8 @@ public class VillagerTradeLimiter extends JavaPlugin { Util.consoleMsg(PREFIX+PLUGIN_NAME+" is running!"); } - //Loads or reloads config.yml settings + //Loads or reloads config.yml and messages.yml public void loadSettings() { - //Load config.yml final String mainPath = this.getDataFolder().getPath()+"/"; final File file = new File(mainPath, "config.yml"); try { @@ -52,44 +58,36 @@ public class VillagerTradeLimiter extends JavaPlugin { Util.errorMsg(e); } this.cfg = YamlConfiguration.loadConfiguration(file); + this.lang = new Lang(this, this.getTextResource("messages.yml"), mainPath); } + //Load and initialize the bStats class with the plugin id private void loadBStats() { - if(this.cfg.getBoolean("bStats", true)) new Metrics(this, 9829); + if(this.cfg.getBoolean("bStats", true)) { + new Metrics(this, BSTATS_ID); + } } //Registers plugin commands private void registerCommands() { - final String reloaded = Util.replaceColors("&eVillagerTradeLimiter &ahas been reloaded!"); - final CommandBase vtl = new CommandBase("villagertradelimiter", "villagertradelimiter.use", (p,args) -> this.help(p)); - vtl.addSub(new CommandBase("reload", "villagertradelimiter.reload", (p,args) -> { - loadSettings(); - Util.sendMsg(reloaded, p); - })); - this.getCommand("villagertradelimiter").setExecutor(vtl); - this.getCommand("villagertradelimiter").setTabCompleter(vtl); + final CommandBase cmd = this.commandManager.getCommands(); + this.getCommand("villagertradelimiter").setExecutor(cmd); + this.getCommand("villagertradelimiter").setTabCompleter(cmd); } //Registers plugin listeners private void registerListeners() { - this.getServer().getPluginManager().registerEvents(new PlayerListener(this), this); + this.getServer().getPluginManager().registerEvents(this.playerListener, 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; } + + //Returns a language setting from messages.yml + public String getLang(final String path) { return this.lang.get(path); } + + //Returns this plugin's player listener + public PlayerListener getPlayerListener() { return this.playerListener; } } diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/CommandBase.java b/src/com/pretzel/dev/villagertradelimiter/commands/CommandBase.java similarity index 80% rename from src/com/pretzel/dev/villagertradelimiter/lib/CommandBase.java rename to src/com/pretzel/dev/villagertradelimiter/commands/CommandBase.java index b641d06..e3579de 100644 --- a/src/com/pretzel/dev/villagertradelimiter/lib/CommandBase.java +++ b/src/com/pretzel/dev/villagertradelimiter/commands/CommandBase.java @@ -1,5 +1,7 @@ -package com.pretzel.dev.villagertradelimiter.lib; +package com.pretzel.dev.villagertradelimiter.commands; +import com.pretzel.dev.villagertradelimiter.lib.Callback; +import com.pretzel.dev.villagertradelimiter.lib.Util; import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -16,6 +18,11 @@ public class CommandBase implements CommandExecutor, TabCompleter { private final Callback callback; private final ArrayList subs; + /** + * @param name The name of the command + * @param permission The permission required to use the command + * @param callback The callback that is called when the command is executed + */ public CommandBase(String name, String permission, Callback callback) { this.name = name; this.permission = permission; @@ -23,6 +30,10 @@ public class CommandBase implements CommandExecutor, TabCompleter { this.subs = new ArrayList<>(); } + /** + * @param command The child command to add + * @return The given child command + */ public CommandBase addSub(CommandBase command) { this.subs.add(command); return command; @@ -69,12 +80,17 @@ public class CommandBase implements CommandExecutor, TabCompleter { return list; } + /** + * @param args The arguments to be copied + * @return The copied arguments + */ private static String[] getCopy(final String[] args) { String[] res = new String[args.length-1]; System.arraycopy(args, 1, res, 0, res.length); return res; } + /** @return The current online player list */ private static List getPlayerList() { final List players = new ArrayList<>(); for(Player p : Bukkit.getOnlinePlayers()) @@ -82,6 +98,9 @@ public class CommandBase implements CommandExecutor, TabCompleter { return players; } + /** @return The name of this command */ public String getName() { return this.name; } + + /** @return The permission required to use this command */ public String getPermission() { return this.permission; } } \ No newline at end of file diff --git a/src/com/pretzel/dev/villagertradelimiter/commands/CommandManager.java b/src/com/pretzel/dev/villagertradelimiter/commands/CommandManager.java new file mode 100644 index 0000000..0ba67da --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/commands/CommandManager.java @@ -0,0 +1,154 @@ +package com.pretzel.dev.villagertradelimiter.commands; + +import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter; +import com.pretzel.dev.villagertradelimiter.lib.Util; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.*; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Villager; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.util.Vector; + +import java.util.Arrays; + +public class CommandManager { + private final VillagerTradeLimiter instance; + + /** @param instance The instance of VillagerTradeLimiter.java */ + public CommandManager(final VillagerTradeLimiter instance) { + this.instance = instance; + } + + /** @return The root command node, to be registered by the plugin */ + public CommandBase getCommands() { + //Adds the /vtl command + final CommandBase cmd = new CommandBase("villagertradelimiter", "villagertradelimiter.use", (p, args) -> showHelp(p, "help")); + + //Adds the /vtl reload command + cmd.addSub(new CommandBase("reload", "villagertradelimiter.reload", (p,args) -> { + //Reload the config and lang + instance.loadSettings(); + Util.sendMsg(instance.getLang("common.reloaded"), p); + })); + + //Adds the /vtl see command + cmd.addSub(new CommandBase("see", "villagertradelimiter.see", (p,args) -> { + //Check if the command was issued via console + if(p == null) { + Util.sendMsg(instance.getLang("common.noconsole"), p); + return; + } + + //Checks if there are enough arguments + if(args.length < 1) { + Util.sendMsg(instance.getLang("common.noargs"), p); + return; + } + + //Get the closest villager. If a nearby villager wasn't found, send the player an error message + Entity closestEntity = getClosestEntity(p); + if(closestEntity == null) { + Util.sendMsg(instance.getLang("see.novillager"), p); + return; + } + + //Gets the other player by name, using the first argument of the command + OfflinePlayer otherPlayer = Bukkit.getOfflinePlayer(args[0]); + if(!otherPlayer.isOnline() && !otherPlayer.hasPlayedBefore()) { + Util.sendMsg(instance.getLang("see.noplayer").replace("{player}", args[0]), p); + return; + } + + //Open the other player's trade view for the calling player + Util.sendMsg(instance.getLang("see.success").replace("{player}", args[0]), p); + instance.getPlayerListener().see((Villager)closestEntity, p, otherPlayer); + })); + + //Adds the /vtl invsee command + cmd.addSub(new CommandBase("invsee", "villagertradelimiter.invsee", (p, args) -> { + //Check if the command was issued via console + if(p == null) { + Util.sendMsg(instance.getLang("common.noconsole"), p); + return; + } + + //Get the closest villager. If a nearby villager wasn't found, send the player an error message + Entity closestEntity = getClosestEntity(p); + if(closestEntity == null) { + Util.sendMsg(instance.getLang("see.novillager"), p); + return; + } + + //Open the villager's inventory view for the calling player + final Villager closestVillager = (Villager)closestEntity; + final Inventory inventory = Bukkit.createInventory(null, 9, "Villager Inventory"); + for(ItemStack item : closestVillager.getInventory().getContents()) { + if(item == null) continue; + inventory.addItem(item.clone()); + } + inventory.setItem(8, getBarrier()); + p.openInventory(inventory); + })); + return cmd; + } + + /** + * @param player The player to get the closest entity for + * @return The closest entity to the player, that the player is looking at + */ + private Entity getClosestEntity(final Player player) { + Entity closestEntity = null; + double closestDistance = Double.MAX_VALUE; + for(Entity entity : player.getNearbyEntities(10, 10, 10)) { + if(entity instanceof Villager) { + Location eye = player.getEyeLocation(); + Vector toEntity = ((Villager) entity).getEyeLocation().toVector().subtract(eye.toVector()); + double dot = toEntity.normalize().dot(eye.getDirection()); + double distance = eye.distance(((Villager)entity).getEyeLocation()); + if(dot > 0.99D && distance < closestDistance) { + closestEntity = entity; + closestDistance = distance; + } + } + } + return closestEntity; + } + + /** + * Sends an interactive help message to a player via chat + * @param p The player to show the help message to + * @param key The key of the help message to show (in messages.yml) + */ + public void showHelp(final Player p, final String key) { + for(String line : instance.getLang(key).split("\n")) { + int i = line.indexOf("]"); + + final String[] tokens = line.substring(i+1).split(";"); + if(p == null) Util.consoleMsg(tokens[0]); + else { + final TextComponent text = new TextComponent(tokens[0]); + if(tokens.length > 1) text.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(tokens[0]+"\n"+tokens[1]))); + text.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, ChatColor.stripColor(tokens[0]))); + if(p.hasPermission(line.substring(1, i))) p.spigot().sendMessage(text); + } + } + } + + /** @return A custom barrier block to show players villagers only have 8 inventory slots */ + private ItemStack getBarrier() { + ItemStack barrier = new ItemStack(Material.BARRIER, 1); + ItemMeta meta = barrier.getItemMeta(); + if(meta != null) { + meta.setDisplayName(ChatColor.RED+"N/A"); + meta.setLore(Arrays.asList(ChatColor.GRAY+"Villagers only have", ChatColor.GRAY+"8 inventory slots!")); + } + barrier.setItemMeta(meta); + return barrier; + } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/data/PlayerData.java b/src/com/pretzel/dev/villagertradelimiter/data/PlayerData.java new file mode 100644 index 0000000..4834634 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/data/PlayerData.java @@ -0,0 +1,23 @@ +package com.pretzel.dev.villagertradelimiter.data; + +import com.pretzel.dev.villagertradelimiter.wrappers.VillagerWrapper; +import org.bukkit.entity.Player; + +public class PlayerData { + private final Player player; + private VillagerWrapper tradingVillager; + + public PlayerData(final Player player) { + this.player = player; + this.tradingVillager = null; + } + + /** @param tradingVillager The villager that this player is currently trading with */ + public void setTradingVillager(VillagerWrapper tradingVillager) { this.tradingVillager = tradingVillager; } + + /** @return The player that this data is for */ + public Player getPlayer() { return this.player; } + + /** @return The villager that this player is currently trading with */ + public VillagerWrapper getTradingVillager() { return this.tradingVillager; } +} diff --git a/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java b/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java index 53ffdfc..bc5828b 100644 --- a/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java +++ b/src/com/pretzel/dev/villagertradelimiter/lib/Callback.java @@ -1,5 +1,10 @@ package com.pretzel.dev.villagertradelimiter.lib; public interface Callback { + /** + * Callback function + * @param result Any type of result to be passed into the callback function + * @param args Any extra arguments to be passed into the callback function + */ void call(T result, String[] args); } diff --git a/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java b/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java index 32d1e42..e98ce48 100644 --- a/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java +++ b/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java @@ -1,39 +1,41 @@ package com.pretzel.dev.villagertradelimiter.listeners; import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter; +import com.pretzel.dev.villagertradelimiter.data.PlayerData; import com.pretzel.dev.villagertradelimiter.lib.Util; -import com.pretzel.dev.villagertradelimiter.nms.*; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; +import com.pretzel.dev.villagertradelimiter.settings.Settings; +import com.pretzel.dev.villagertradelimiter.wrappers.*; +import org.bukkit.OfflinePlayer; 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.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryPickupItemEvent; +import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerInteractEntityEvent; -import org.bukkit.inventory.meta.EnchantmentStorageMeta; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import java.util.Arrays; +import java.util.HashMap; 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.DIAMOND_SWORD}; - 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 VillagerTradeLimiter instance; + private final Settings settings; + private final HashMap playerData; + /** @param instance The instance of VillagerTradeLimiter.java */ public PlayerListener(VillagerTradeLimiter instance) { this.instance = instance; + this.settings = new Settings(instance); + this.playerData = new HashMap<>(); } - //Handles villager trading event + /** Handles when a player begins trading with a villager */ @EventHandler - public void onPlayerInteract(PlayerInteractEntityEvent event) { + public void onPlayerBeginTrading(PlayerInteractEntityEvent event) { if(!(event.getRightClicked() instanceof Villager)) return; final Villager villager = (Villager)event.getRightClicked(); if(Util.isNPC(villager)) return; //Skips NPCs @@ -47,214 +49,186 @@ public class PlayerListener implements Listener { return; } } else { - List disabledWorlds = instance.getCfg().getStringList("DisableTrading"); - for(String world : disabledWorlds) { - if(event.getPlayer().getWorld().getName().equals(world)) { + final List disabledWorlds = instance.getCfg().getStringList("DisableTrading"); + final String world = event.getPlayer().getWorld().getName(); + for(String disabledWorld : disabledWorlds) { + if(world.equals(disabledWorld)) { event.setCancelled(true); return; } } } + event.setCancelled(true); final Player player = event.getPlayer(); - if(Util.isNPC(player)) return; //Skips NPCs - this.hotv(player); - this.maxDiscount(villager, player); - this.maxDemand(villager); + this.see(villager, player, player); } - //Hero of the Village effect limiter feature - 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 + /** Handles when a player stops trading with a villager */ + @EventHandler + public void onPlayerStopTrading(final InventoryCloseEvent event) { + //Don't do anything unless the player is actually finished trading with a villager + if(event.getInventory().getType() != InventoryType.MERCHANT) return; + if(!(event.getPlayer() instanceof Player)) return; + final Player player = (Player)event.getPlayer(); + if(Util.isNPC(player)) return; + if(getPlayerData(player).getTradingVillager() == null) return; - 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 == null) return; - if(pot.getAmplifier() > maxHeroLevel-1) { - player.removePotionEffect(effect); - player.addPotionEffect(new PotionEffect(effect, pot.getDuration(), maxHeroLevel-1)); - } + //Reset the villager's NBT data when a player is finished trading + final VillagerWrapper villager = playerData.get(player).getTradingVillager(); + getPlayerData(player).setTradingVillager(null); + if(villager == null) return; + villager.reset(); } - //Sets an ingredient for a trade - private void setIngredient(ConfigurationSection item, NBTCompound recipe, String nbtKey, String itemKey) { - if(!item.contains(itemKey) || recipe.getCompound(nbtKey).getString("id").equals("minecraft:air")) return; - if(item.contains(itemKey+".Material")) { - recipe.getCompound(nbtKey).setString("id", "minecraft:" + item.getString(itemKey+".Material")); - } - if(item.contains(itemKey+".Amount")) { - int cost = item.getInt(itemKey+".Amount"); - cost = Math.min(cost, 64); - cost = Math.max(cost, 1); - recipe.getCompound(nbtKey).setInteger("Count", cost); + /** + * Opens the villager's trading menu, with the adjusted trades of another player (or the same player) + * @param villager The villager whose trades you want to see + * @param player The player who calls the command, or the player that has begun trading + * @param other The other player to view trades for, or the player that has just begun trading + */ + public void see(final Villager villager, final Player player, final OfflinePlayer other) { + //Wraps the villager and player into wrapper classes + final VillagerWrapper villagerWrapper = new VillagerWrapper(villager); + final PlayerWrapper otherWrapper = new PlayerWrapper(other); + if(Util.isNPC(villager) || Util.isNPC(player) || otherWrapper.isNPC()) return; //Skips NPCs + + //Checks if the version is old, before the 1.16 UUID changes + String version = instance.getServer().getClass().getPackage().getName(); + boolean isOld = version.contains("1_13_") || version.contains("1_14_") || version.contains("1_15_"); + + //Calculates the player's total reputation and Hero of the Village discount + int totalReputation = villagerWrapper.getTotalReputation(villagerWrapper, otherWrapper, isOld); + double hotvDiscount = getHotvDiscount(otherWrapper); + + //Adjusts the recipe prices, MaxUses, and ingredients + final List recipes = villagerWrapper.getRecipes(); + for(RecipeWrapper recipe : recipes) { + //Set the special price (discount) + recipe.setSpecialPrice(getDiscount(recipe, totalReputation, hotvDiscount)); + + //Set ingredient materials and amounts + final ConfigurationSection override = settings.getOverride(recipe); + if(override != null) { + setIngredient(override.getConfigurationSection("Item1"), recipe.getIngredient1()); + setIngredient(override.getConfigurationSection("Item2"), recipe.getIngredient2()); + setIngredient(override.getConfigurationSection("Result"), recipe.getResult()); + } + + //Set the maximum number of uses (trades/day) + recipe.setMaxUses(getMaxUses(recipe)); } + + //Open the villager's trading menu + getPlayerData(player).setTradingVillager(villagerWrapper); + player.openMerchant(villager, false); } - //MaxDiscount feature - limits the lowest discounted price to a % of the base price - private void maxDiscount(final Villager villager, final Player player) { - int majorPositiveValue = 0, minorPositiveValue = 0, tradingValue = 0, minorNegativeValue = 0, majorNegativeValue = 0; - - NBTEntity nbtEntity = new NBTEntity(villager); - final NBTEntity playerNBT = new NBTEntity(player); - final String playerUUID = Util.intArrayToString(playerNBT.getIntArray("UUID")); - if(nbtEntity.hasKey("Gossips")) { - NBTCompoundList gossips = nbtEntity.getCompoundList("Gossips"); - for(NBTCompound gossip : gossips) { - final String type = gossip.getString("Type"); - final String targetUUID = Util.intArrayToString(gossip.getIntArray("Target")); - final int value = gossip.getInteger("Value"); - if(targetUUID.equals(playerUUID)) { - switch (type) { - case "trading": tradingValue = value; break; - case "minor_positive": minorPositiveValue = value; break; - case "minor_negative": minorNegativeValue = value; break; - case "major_positive": majorPositiveValue = value; break; - case "major_negative": majorNegativeValue = value; break; - default: break; - } - } - } - } - - final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides"); - final NBTEntity villagerNBT = new NBTEntity(villager); - NBTCompoundList recipes = villagerNBT.getCompound("Offers").getCompoundList("Recipes"); - for(final NBTCompound recipe : recipes) { - final int ingredientAmount = recipe.getCompound("buy").getInteger("Count"); - final float priceMultiplier = this.getPriceMultiplier(recipe); - final int valueModifier = (5 * majorPositiveValue) + minorPositiveValue + tradingValue - minorNegativeValue - (5 * majorNegativeValue); - final float finalValue = ingredientAmount - priceMultiplier * valueModifier; - - boolean disabled = false; - double maxDiscount = instance.getCfg().getDouble("MaxDiscount", 0.3); - int maxUses = instance.getCfg().getInt("MaxUses", -1); - if (overrides != null) { - for (final String override : overrides.getKeys(false)) { - final ConfigurationSection item = this.getItem(recipe, override); - if (item != null) { - //Set whether trade is disabled and max discount - disabled = item.getBoolean("Disabled", false); - maxDiscount = item.getDouble("MaxDiscount", maxDiscount); - maxUses = item.getInt("MaxUses", maxUses); - - //Set 1st and 2nd ingredients - setIngredient(item, recipe, "buy", "Item1"); - setIngredient(item, recipe, "buyB", "Item2"); - break; - } - } - } - - //Set max uses - if(maxUses >= 0) { - recipe.setInteger("maxUses", maxUses); - } else { - if(disabled) recipe.setInteger("maxUses", 0); - else { - int uses = 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)) { - uses = 12; - } else if (Arrays.asList(MAX_USES_3).contains(buyMaterial) || Arrays.asList(MAX_USES_3).contains(sellMaterial)) { - uses = 3; - } - recipe.setInteger("maxUses", uses); - } - } - - //Set max discount - if (maxDiscount >= 0.0 && maxDiscount <= 1.0) { - if (finalValue < ingredientAmount * (1.0 - maxDiscount) && finalValue != ingredientAmount) { - recipe.setFloat("priceMultiplier", ingredientAmount * (float) maxDiscount / valueModifier); - } else { - recipe.setFloat("priceMultiplier", priceMultiplier); - } - } else if(maxDiscount > 1.0) { - recipe.setFloat("priceMultiplier", priceMultiplier * (float) maxDiscount); - } else { - recipe.setFloat("priceMultiplier", priceMultiplier); - } - } + @EventHandler + public void onPickupItem(InventoryPickupItemEvent event) { + Util.consoleMsg("Picked up!"); } - //MaxDemand feature - limits demand-based price increases - private void maxDemand(final Villager villager) { - final NBTEntity villagerNBT = new NBTEntity(villager); - final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides"); - if (villagerNBT.hasKey("Offers")) { - NBTCompoundList recipes = villagerNBT.getCompound("Offers").getCompoundList("Recipes"); - for (NBTCompound recipe : recipes) { - final int demand = recipe.getInteger("demand"); - int maxDemand = instance.getCfg().getInt("MaxDemand", -1); - if (overrides != null) { - for (String override : overrides.getKeys(false)) { - final ConfigurationSection item = this.getItem(recipe, override); - if(item != null) { - maxDemand = item.getInt("MaxDemand", maxDemand); - break; - } - } - } - if(maxDemand >= 0 && demand > maxDemand) { - recipe.setInteger("demand", maxDemand); - } - } - } + /** + * @param recipe The recipe to get the base price for + * @return The initial price of a recipe/trade, before any discounts are applied + */ + private int getBasePrice(final RecipeWrapper recipe) { + int basePrice = recipe.getIngredient1().getAmount(); + basePrice = settings.fetchInt(recipe, "Item1.Amount", basePrice); + return Math.min(Math.max(basePrice, 1), 64); } - //Returns the price multiplier for a given trade - private float getPriceMultiplier(final NBTCompound recipe) { - float p = 0.05f; - final Material type = recipe.getItemStack("sell").getType(); - for(int length = MATERIALS.length, i = 0; i < length; ++i) { - if(type == MATERIALS[i]) { - p = 0.2f; - break; - } - } - return p; + /** + * @param recipe The recipe to get the demand for + * @return The current value of the demand for the given recipe + */ + private int getDemand(final RecipeWrapper recipe) { + int demand = recipe.getDemand(); + int maxDemand = settings.fetchInt(recipe, "MaxDemand", -1); + if(maxDemand >= 0 && demand > maxDemand) return maxDemand; + return demand; } - //Returns the configured settings for a trade - private ConfigurationSection getItem(final NBTCompound recipe, final String k) { - final ConfigurationSection item = instance.getCfg().getConfigurationSection("Overrides."+k); - if(item == null) return null; + /** + * @param recipe The recipe to get the discount for + * @param totalReputation The player's total reputation from a villager's gossips + * @param hotvDiscount The total discount from the Hero of the Village effect + * @return The total discount for the recipe, which is added to the base price to get the final price + */ + private int getDiscount(final RecipeWrapper recipe, int totalReputation, double hotvDiscount) { + int basePrice = getBasePrice(recipe); + int demand = getDemand(recipe); + float priceMultiplier = recipe.getPriceMultiplier(); + int discount = -(int)(totalReputation * priceMultiplier) - (int)(hotvDiscount * basePrice) + Math.max(0, (int)(demand * priceMultiplier * basePrice)); - 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.getItemStack("sell").getType() == Material.ENCHANTED_BOOK) { - final EnchantmentStorageMeta meta = (EnchantmentStorageMeta) recipe.getItemStack("sell").getItemMeta(); - final Enchantment enchantment = EnchantmentWrapper.getByKey(NamespacedKey.minecraft(k.substring(0, k.lastIndexOf("_")))); - if (meta == null || enchantment == null) return null; - if (meta.hasStoredEnchant(enchantment) && meta.getStoredEnchantLevel(enchantment) == level) return item; + double maxDiscount = settings.fetchDouble(recipe, "MaxDiscount", 0.3); + if(maxDiscount >= 0.0 && maxDiscount <= 1.0) { + if(basePrice + discount < basePrice * (1.0 - maxDiscount)) { + discount = -(int)(basePrice * maxDiscount); } - } 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); + } else if(maxDiscount > 1.0) { + //TODO: Allow for better fine-tuning + discount = (int)(discount * maxDiscount); } - return null; + return discount; } - //Verifies that an item exists in the villager's trade - private boolean verify(final NBTCompound recipe, final Material material) { - return ((recipe.getItemStack("sell").getType() == material) || (recipe.getItemStack("buy").getType() == material)); + /** + * @param recipe The recipe to get the MaxUses for + * @return The current maximum number of times a player can make a trade before the villager restocks + */ + private int getMaxUses(final RecipeWrapper recipe) { + int uses = recipe.getMaxUses(); + int maxUses = settings.fetchInt(recipe, "MaxUses", -1); + boolean disabled = settings.fetchBoolean(recipe, "Disabled", false); + + if(maxUses < 0) maxUses = uses; + if(disabled) maxUses = 0; + return maxUses; + } + + /** + * @param playerWrapper The wrapped player to check the hotv effect for + * @return The Hero of the Village discount factor, adjusted by config + */ + private double getHotvDiscount(final PlayerWrapper playerWrapper) { + final Player player = playerWrapper.getPlayer(); + if(player == null) return 0.0; + + final PotionEffectType effectType = PotionEffectType.HERO_OF_THE_VILLAGE; + if(!player.hasPotionEffect(effectType)) return 0.0; + + final PotionEffect effect = player.getPotionEffect(effectType); + if(effect == null) return 0.0; + + int heroLevel = effect.getAmplifier()+1; + final int maxHeroLevel = instance.getCfg().getInt("MaxHeroLevel", -1); + if(maxHeroLevel == 0 || heroLevel == 0) return 0.0; + if(maxHeroLevel > 0 && heroLevel > maxHeroLevel) { + heroLevel = maxHeroLevel; + } + return 0.0625*(heroLevel-1) + 0.3; + } + + /** + * @param item The config section that contains the settings for Item1, Item2, or Result items in the trade + * @param ingredient The respective ingredient to change, based on config.yml + */ + private void setIngredient(final ConfigurationSection item, final IngredientWrapper ingredient) { + if(item == null) return; + ingredient.setMaterialId("minecraft:"+item.getString("Material", ingredient.getMaterialId()).replace("minecraft:","")); + ingredient.setAmount(item.getInt("Amount", ingredient.getAmount())); + } + + /** + * @param player The player to get the data container for + * @return The data container for the given player + */ + private PlayerData getPlayerData(final Player player) { + if(playerData.containsKey(player) && playerData.get(player) != null) return playerData.get(player); + final PlayerData pd = new PlayerData(player); + playerData.put(player, pd); + return pd; } } diff --git a/src/com/pretzel/dev/villagertradelimiter/nms/utils/ApiMetricsLite.java b/src/com/pretzel/dev/villagertradelimiter/nms/utils/ApiMetricsLite.java deleted file mode 100644 index 3ddfa97..0000000 --- a/src/com/pretzel/dev/villagertradelimiter/nms/utils/ApiMetricsLite.java +++ /dev/null @@ -1,388 +0,0 @@ -package com.pretzel.dev.villagertradelimiter.nms.utils; - -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import org.bukkit.Bukkit; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.RegisteredServiceProvider; -import org.bukkit.plugin.ServicePriority; - -import javax.net.ssl.HttpsURLConnection; -import java.io.*; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.Timer; -import java.util.TimerTask; -import java.util.UUID; -import java.util.logging.Level; -import java.util.zip.GZIPOutputStream; - -import static com.pretzel.dev.villagertradelimiter.nms.utils.MinecraftVersion.getLogger; - -/** - * bStats collects some data for plugin authors. - *

- * 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 getRecipes() { + final List recipes = new ArrayList<>(); + + //Add the recipes from the villager's NBT data into a list of wrapped recipes + final NBTCompoundList nbtRecipes = entity.getCompound("Offers").getCompoundList("Recipes"); + for(NBTCompound nbtRecipe : nbtRecipes) { + recipes.add(new RecipeWrapper(nbtRecipe)); + } + return recipes; + } + + /** @return A list of wrapped gossips for the villager */ + private List getGossips() { + final List gossips = new ArrayList<>(); + if(!entity.hasKey("Gossips")) return gossips; + + //Add the gossips from the villager's NBT data into a list of wrapped gossips + final NBTCompoundList nbtGossips = entity.getCompoundList("Gossips"); + for(NBTCompound nbtGossip : nbtGossips) { + gossips.add(new GossipWrapper(nbtGossip)); + } + return gossips; + } + + /** + * @param villager The wrapped villager that contains the gossips + * @param player The wrapped player that the gossips are about + * @param isOld Whether the server is older than 1.16 or not. Minecraft changed how UUID's are represented in 1.16 + * @return the total reputation (from gossips) for a player + */ + public int getTotalReputation(@NonNull final VillagerWrapper villager, @NonNull final PlayerWrapper player, final boolean isOld) { + int totalReputation = 0; + + final String playerUUID = player.getUUID(isOld); + final List gossips = villager.getGossips(); + for(GossipWrapper gossip : gossips) { + final GossipWrapper.GossipType type = gossip.getType(); + if(type == null || type == GossipWrapper.GossipType.OTHER) continue; + + final String targetUUID = gossip.getTargetUUID(isOld); + if(targetUUID.equals(playerUUID)) { + totalReputation += gossip.getValue() * type.getWeight(); + } + } + return totalReputation; + } + + /** Resets the villager's NBT data to default */ + public void reset() { + //Reset the recipes back to their default ingredients, MaxUses, and discounts + for(RecipeWrapper recipe : this.getRecipes()) { + recipe.reset(); + } + + this.villager.getInventory().clear(); + this.villager.getInventory().setContents(this.contents); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3d9f1ce..e2a88f2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -16,6 +16,7 @@ DisableTrading: # 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 +# For more information, see https://minecraft.fandom.com/wiki/Hero_of_the_Village#Price_decrement MaxHeroLevel: 1 # The maximum discount (%) you can get from trading/healing zombie villagers. This limits reputation-based price decreases. @@ -28,12 +29,13 @@ MaxDiscount: 0.3 # * 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. +# For more information, see https://minecraft.fandom.com/wiki/Trading#Economics MaxDemand: -1 # The maximum number of times a player can make any trade before a villager is out of stock. # * Set to -1 to disable this feature and keep vanilla behavior # * Set to 0 or higher to change the maximum number of uses for all items -# For default vanilla settings, see https://minecraft.fandom.com/el/wiki/Trading#Java_Edition +# For more information, see https://minecraft.fandom.com/el/wiki/Trading#Java_Edition MaxUses: -1 @@ -52,13 +54,16 @@ Overrides: name_tag: MaxDiscount: -1.0 MaxDemand: 60 + MaxUses: 2 Item1: - Material: "paper" + Material: "book" Amount: 64 Item2: Material: "ink_sac" - Amount: 32 - MaxUses: 64 + Amount: 64 + Result: + Material: "name_tag" + Amount: 2 clock: MaxDemand: 12 paper: diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml new file mode 100644 index 0000000..fc3a760 --- /dev/null +++ b/src/main/resources/messages.yml @@ -0,0 +1,19 @@ +# Command help messages (description on hover). Format: [Permission]Usage;Description +help: |- + [villagertradelimiter.use]&a----- VTL Commands ----- + [villagertradelimiter.use]&b/vtl;&fshows this help message + [villagertradelimiter.reload]&b/vtl reload;&freloads config.yml + [villagertradelimiter.see]&b/vtl see ;&fshows the adjusted trades for another player + +# Common messages: +common: + reloaded: "&eVillagerTradeLimiter (VTL) &ahas been reloaded!" + noconsole: "&cYou cannot use this command from the console." + noargs: "&cNot enough arguments! For command usage, see /vtl" + +# Messages for the /vtl see command: +see: + success: "&aShowing the adjusted trades for &b{player}&a..." + noplayer: "&cInvalid player &b{player}&c! Please use a valid player's name." + novillager: "&cInvalid entity! Please look at the villager you want to check." + diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4ad0909..a68935b 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: VillagerTradeLimiter author: PretzelJohn main: com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter -version: 1.4.4 +version: 1.5.0 api-version: 1.14 commands: @@ -16,10 +16,18 @@ permissions: children: villagertradelimiter.use: true villagertradelimiter.reload: true + villagertradelimiter.see: true + villagertradelimiter.invsee: true default: op villagertradelimiter.use: description: Allows players to use VillagerTradeLimiter. default: op villagertradelimiter.reload: description: Allows players to reload config.yml. + default: op + villagertradelimiter.see: + description: Allows players to see the trades for another player + default: op + villagertradelimiter.invsee: + description: Allows players to see inventory of a villager default: op \ No newline at end of file