mirror of
https://github.com/PretzelJohn/VillagerTradeLimiter.git
synced 2024-11-13 05:54:23 +01:00
Version 1.5.0-pre1:
* Added /vtl see <player> command to see the trade prices of another player * Added /vtl invsee command to see the inventory of a villager * Added customizable help, success, error messages * Added result slot options, so people can change the currency and amount of the resulting item from a trade * Massive code rewrite for better sustainability and reliability * Fixed villager inventory dupe bug: food in a villager's inventory would multiply 16+ times each time a player traded with the villager * Smoothed out hero of the village, so the player never loses their original potion effect when trading with villagers TO-DO: * Add long-term restock cooldown * Add enchantment, custom model data, names, and lores to ingredient & result settings? * GUI Editor
This commit is contained in:
parent
446e677336
commit
5110befd30
2
pom.xml
2
pom.xml
@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.pretzel.dev</groupId>
|
||||
<artifactId>VillagerTradeLimiter</artifactId>
|
||||
<version>1.4.4</version>
|
||||
<version>1.5.0</version>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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<Player> callback;
|
||||
private final ArrayList<CommandBase> 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<Player> 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<String> getPlayerList() {
|
||||
final List<String> 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; }
|
||||
}
|
@ -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 <player> 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;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
package com.pretzel.dev.villagertradelimiter.lib;
|
||||
|
||||
public interface Callback<T> {
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
@ -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<Player, PlayerData> 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<String> disabledWorlds = instance.getCfg().getStringList("DisableTrading");
|
||||
for(String world : disabledWorlds) {
|
||||
if(event.getPlayer().getWorld().getName().equals(world)) {
|
||||
final List<String> 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<RecipeWrapper> 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;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
@ -1,4 +1,4 @@
|
||||
package com.pretzel.dev.villagertradelimiter.lib;
|
||||
package com.pretzel.dev.villagertradelimiter.settings;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
65
src/com/pretzel/dev/villagertradelimiter/settings/Lang.java
Normal file
65
src/com/pretzel/dev/villagertradelimiter/settings/Lang.java
Normal file
@ -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));
|
||||
}
|
||||
}
|
119
src/com/pretzel/dev/villagertradelimiter/settings/Settings.java
Normal file
119
src/com/pretzel/dev/villagertradelimiter/settings/Settings.java
Normal file
@ -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));
|
||||
}
|
||||
}
|
@ -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"); }
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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(); }
|
||||
}
|
@ -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"); }
|
||||
}
|
@ -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<RecipeWrapper> getRecipes() {
|
||||
final List<RecipeWrapper> 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<GossipWrapper> getGossips() {
|
||||
final List<GossipWrapper> 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<GossipWrapper> 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);
|
||||
}
|
||||
}
|
@ -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:
|
||||
|
19
src/main/resources/messages.yml
Normal file
19
src/main/resources/messages.yml
Normal file
@ -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 <player>;&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 <player> 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."
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user