diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/EventListener.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/EventListener.java index 807cf91..01dcc6d 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/EventListener.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/EventListener.java @@ -1,34 +1,16 @@ package io.github.mrcomputer1.smileyplayertrader; -import io.github.mrcomputer1.smileyplayertrader.gui.framework.GUIManager; -import io.github.mrcomputer1.smileyplayertrader.util.GeyserUtil; import io.github.mrcomputer1.smileyplayertrader.util.I18N; import io.github.mrcomputer1.smileyplayertrader.util.RegionUtil; -import io.github.mrcomputer1.smileyplayertrader.util.item.ItemUtil; import io.github.mrcomputer1.smileyplayertrader.util.database.statements.StatementHandler; -import io.github.mrcomputer1.smileyplayertrader.util.item.stocklocations.StockLocations; import io.github.mrcomputer1.smileyplayertrader.util.merchant.MerchantUtil; -import io.github.mrcomputer1.smileyplayertrader.versions.VersionSupport; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageByEntityEvent; -import org.bukkit.event.inventory.ClickType; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.event.player.PlayerInteractAtEntityEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.InventoryView; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.MerchantInventory; -import java.lang.reflect.InvocationTargetException; import java.sql.ResultSet; import java.sql.SQLException; @@ -93,13 +75,13 @@ public void onEntityTakeDamageByEntity(EntityDamageByEntityEvent e) { if(SmileyPlayerTrader.getInstance().getConfiguration().getAutoCombatLockEnabled()) { if (e.getDamager() instanceof Player) { Player player = (Player) e.getDamager(); - if(!RegionUtil.isAllowed(player)) + if(!RegionUtil.isAllowedOverall(player)) return; SmileyPlayerTrader.getInstance().getPlayerConfig().lockPlayer(player); } if (e.getEntity() instanceof Player){ Player player = (Player) e.getEntity(); - if(!RegionUtil.isAllowed(player)) + if(!RegionUtil.isAllowedOverall(player)) return; SmileyPlayerTrader.getInstance().getPlayerConfig().lockPlayer((Player) e.getEntity()); } diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/PlayerConfig.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/PlayerConfig.java index 47d31a1..d221644 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/PlayerConfig.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/PlayerConfig.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.HashMap; import java.util.Map; +import java.util.UUID; public class PlayerConfig { @@ -31,6 +32,7 @@ public Config clone() { private int LOCK_PERIOD = SmileyPlayerTrader.getInstance().getConfiguration().getAutoCombatLockLength() * 1000; private Map playerConfigs = new HashMap<>(); private Map playerCombatLock = new HashMap<>(); + private Map playerTradeCooldown = new HashMap<>(); public void loadPlayer(Player player){ try(ResultSet set = SmileyPlayerTrader.getInstance().getStatementHandler().get(StatementHandler.StatementType.LOAD_PLAYER_CONFIG, player.getUniqueId().toString())) { @@ -73,6 +75,7 @@ public Config getMutablePlayer(Player player){ public void unloadPlayer(Player player){ this.playerConfigs.remove(player.getName()); this.playerCombatLock.remove(player.getName()); + this.playerTradeCooldown.remove(player.getUniqueId()); } public void updatePlayer(Player player, Config config){ @@ -113,4 +116,29 @@ public void releasePlayerLock(Player player){ this.playerCombatLock.remove(player.getName()); } + public boolean isTradeOnCooldown(Player player) { + if (!SmileyPlayerTrader.getInstance().getConfiguration().getCooldownEnabled()) + return false; + if (player.hasPermission("smileyplayertrader.bypasscooldown")) + return false; + if (!playerTradeCooldown.containsKey(player.getUniqueId())) + return false; + return playerTradeCooldown.get(player.getUniqueId()) > System.currentTimeMillis(); + } + + public int getCooldownTimeRemaining(Player player) { + if (!playerTradeCooldown.containsKey(player.getUniqueId())) + return -1; + return (int) ((playerTradeCooldown.get(player.getUniqueId()) - System.currentTimeMillis()) / 1000); + } + + public void setTradeCooldown(Player player) { + if (!SmileyPlayerTrader.getInstance().getConfiguration().getCooldownEnabled()) + return; + if (player.hasPermission("smileyplayertrader.bypasscooldown")) + return; + playerTradeCooldown.put(player.getUniqueId(), + System.currentTimeMillis() + (SmileyPlayerTrader.getInstance().getConfiguration().getCooldownLength() * 1000L)); + } + } diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/SPTConfiguration.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/SPTConfiguration.java index 1c7446d..9cdcb88 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/SPTConfiguration.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/SPTConfiguration.java @@ -322,6 +322,12 @@ public boolean getDisableVaultOfflinePermissionChecking() { return this.config.getBoolean("disableVaultOfflinePermissionChecking", false); } + // Start On Trade Complete Commands + public List getOnTradeCompleteCommands() { + return this.config.getStringList("onTradeCompleteCommands"); + } + // End On Trade Complete Commands + // Start Auto Combat Lock public boolean getAutoCombatLockEnabled(){ return this.config.getBoolean("autoCombatLock.enabled", true); @@ -336,6 +342,17 @@ public boolean getAutoCombatLockNeverShowNotice(){ } // End Auto Combat Lock + // Start Cooldown + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean getCooldownEnabled() { + return this.config.getBoolean("cooldown.enabled", false); + } + + public int getCooldownLength() { + return this.config.getInt("cooldown.seconds", 10); + } + // End Cooldown + public List getPriceQuickSelection(){ List priceQuickSelection = SmileyPlayerTrader.getInstance().getConfig().getList("priceQuickSelection", new ArrayList<>()); List stacks = new ArrayList<>(); diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/TradeEventListener.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/TradeEventListener.java index b470429..bfeded0 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/TradeEventListener.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/TradeEventListener.java @@ -3,6 +3,8 @@ import io.github.mrcomputer1.smileyplayertrader.gui.framework.GUIManager; import io.github.mrcomputer1.smileyplayertrader.util.GeyserUtil; import io.github.mrcomputer1.smileyplayertrader.util.I18N; +import io.github.mrcomputer1.smileyplayertrader.util.RegionUtil; +import io.github.mrcomputer1.smileyplayertrader.util.TradeEventCommands; import io.github.mrcomputer1.smileyplayertrader.util.database.statements.StatementHandler; import io.github.mrcomputer1.smileyplayertrader.util.item.ItemUtil; import io.github.mrcomputer1.smileyplayertrader.util.item.stocklocations.StockLocations; @@ -41,10 +43,14 @@ public void onEntityRightClick(PlayerInteractAtEntityEvent e){ if(SmileyPlayerTrader.getInstance().getConfiguration().getDisableRightClickTrading()) return; - if(e.getRightClicked().hasMetadata("NPC")) // Citizens NPCs seem like players but they have different UUIDs and can't work. + if(e.getRightClicked().hasMetadata("NPC")) // Citizens NPCs seem like players, but they have different UUIDs and can't work. return; if(!e.getPlayer().hasPermission("smileyplayertrader.trade")) return; + if(!e.getPlayer().hasPermission("smileyplayertrader.trade.rightclick")) + return; + if(!RegionUtil.isAllowedRightClick(e.getPlayer()) || !RegionUtil.isAllowedOverall(e.getPlayer())) + return; PlayerConfig.Config playerConfig = SmileyPlayerTrader.getInstance().getPlayerConfig().getPlayer(e.getPlayer()); if(playerConfig == null) // If playerConfig is null, assume player is offline. @@ -196,6 +202,18 @@ private void processItemPurchase(InventoryClickEvent e, MerchantInventory mi, Of MerchantUtil.thankPurchaser(store, (Player) e.getWhoClicked()); SmileyPlayerTrader.getInstance().getStatementHandler().run(StatementHandler.StatementType.INCREMENT_PURCHASE_COUNT, productId); + // Execute trade commands + TradeEventCommands.executeCommands( + store, + (Player) e.getWhoClicked(), + mi.getSelectedRecipe().getResult(), + mi.getSelectedRecipe().getIngredients().get(0), + mi.getSelectedRecipe().getIngredients().size() >= 2 ? mi.getSelectedRecipe().getIngredients().get(1) : null + ); + + // Set cooldown (if enabled) + SmileyPlayerTrader.getInstance().getPlayerConfig().setTradeCooldown((Player) e.getWhoClicked()); + // If the store player is online, inform them of the purchase. if (store.isOnline()) store.getPlayer().sendMessage(I18N.translate("&a%0% just purchased %1%!", e.getWhoClicked().getName(), mi.getSelectedRecipe().getResult().getType())); @@ -241,6 +259,14 @@ private void onTradeSlotClick(InventoryClickEvent e) { MerchantInventory mi = (MerchantInventory) e.getInventory(); + // Check if on cooldown + if (SmileyPlayerTrader.getInstance().getPlayerConfig().isTradeOnCooldown((Player) e.getWhoClicked())) { + e.setCancelled(true); + GUIManager.sendErrorMessage(e.getWhoClicked(), I18N.translate("&cYou are currently on cooldown for %0% more seconds.", + SmileyPlayerTrader.getInstance().getPlayerConfig().getCooldownTimeRemaining((Player) e.getWhoClicked()))); + return; + } + // Get player and ensure player can be traded with. //noinspection deprecation OfflinePlayer store = Bukkit.getOfflinePlayer(e.getView().getTitle().replace(I18N.translate("&2Villager Store: "), "")); diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/command/TradeCommand.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/command/TradeCommand.java index 09998cd..1dd9fad 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/command/TradeCommand.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/command/TradeCommand.java @@ -1,6 +1,7 @@ package io.github.mrcomputer1.smileyplayertrader.command; import io.github.mrcomputer1.smileyplayertrader.util.I18N; +import io.github.mrcomputer1.smileyplayertrader.util.RegionUtil; import io.github.mrcomputer1.smileyplayertrader.util.merchant.MerchantUtil; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; @@ -20,6 +21,11 @@ public void onCommand(CommandSender sender, String[] args) { return; } + if(!RegionUtil.isAllowedRemote((Player) sender)) { + sender.sendMessage(I18N.translate("&cYou cannot trade here.")); + return; + } + if(args.length < 1){ sender.sendMessage(I18N.translate("&cBad Syntax! &f/spt trade ")); return; diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/RegionUtil.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/RegionUtil.java index 031f5e1..ce68c56 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/RegionUtil.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/RegionUtil.java @@ -17,11 +17,19 @@ public static void setup() { impl = new NullRegionImpl(); } - impl.registerFlag(); + impl.registerFlags(); } - public static boolean isAllowed(Player player) { - return impl.isAllowed(player); + public static boolean isAllowedOverall(Player player) { + return impl.isAllowedOverall(player); + } + + public static boolean isAllowedRightClick(Player player) { + return impl.isAllowedRightClick(player); + } + + public static boolean isAllowedRemote(Player player) { + return impl.isAllowedRemote(player); } } diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/TradeEventCommands.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/TradeEventCommands.java new file mode 100644 index 0000000000..654d38f --- /dev/null +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/TradeEventCommands.java @@ -0,0 +1,54 @@ +package io.github.mrcomputer1.smileyplayertrader.util; + +import io.github.mrcomputer1.smileyplayertrader.SmileyPlayerTrader; +import io.github.mrcomputer1.smileyplayertrader.versions.VersionSupport; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.List; + +public class TradeEventCommands { + + private static void executeCommand(String command, OfflinePlayer merchant, Player customer, ItemStack product, ItemStack cost, ItemStack cost2) { + boolean onlyIfNoCost2 = command.contains("%ONLY_IF_NO_COST2%"); + boolean onlyIfCost2 = command.contains("%COST2_TYPE%") || command.contains("%COST2_NAME%") || command.contains("%COST2_AMOUNT%"); + + if (cost2 == null && onlyIfCost2) // skip command if this command should only run if there is a cost2 and there isn't one. + return; + if (cost2 != null && onlyIfNoCost2) // skip command if this command should only run if there is no cost2 and there is one. + return; + + command = command.replace("%MERCHANT%", merchant.getName() == null ? "null" : merchant.getName()); + command = command.replace("%CUSTOMER%", customer.getName()); + command = command.replace("%PRODUCT_TYPE%", product.getType().name()); + String productItemName = VersionSupport.getPreferredItemName(product.getItemMeta()); + command = command.replace("%PRODUCT_NAME%", productItemName == null ? product.getType().toString() : productItemName); + command = command.replace("%PRODUCT_AMOUNT%", Integer.toString(product.getAmount())); + command = command.replace("%COST_TYPE%", cost.getType().name()); + String costItemName = VersionSupport.getPreferredItemName(cost.getItemMeta()); + command = command.replace("%COST_NAME%", cost.getType().name()); + command = command.replace("%COST_AMOUNT%", Integer.toString(cost.getAmount())); + command = command.replace("%ONLY_IF_NO_COST2%", ""); + + if (onlyIfCost2) { + command = command.replace("%COST2_TYPE%", cost2.getType().name()); + String cost2ItemName = VersionSupport.getPreferredItemName(cost2.getItemMeta()); + command = command.replace("%COST2_NAME%", cost2ItemName == null ? cost2.getType().toString() : cost2ItemName); + command = command.replace("%COST2_AMOUNT%", Integer.toString(cost2.getAmount())); + } + + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); + } + + public static void executeCommands(OfflinePlayer merchant, Player customer, ItemStack product, ItemStack cost, ItemStack cost2) { + List commands = SmileyPlayerTrader.getInstance().getConfiguration().getOnTradeCompleteCommands(); + + for (String command : commands) { + executeCommand(command, merchant, customer, product, cost, cost2); + } + } + +} diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/IRegionImpl.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/IRegionImpl.java index 854b13c..cd08538 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/IRegionImpl.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/IRegionImpl.java @@ -4,7 +4,9 @@ public interface IRegionImpl { - boolean isAllowed(Player player); - void registerFlag(); + boolean isAllowedOverall(Player player); + boolean isAllowedRightClick(Player player); + boolean isAllowedRemote(Player player); + void registerFlags(); } diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/NullRegionImpl.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/NullRegionImpl.java index dfe8114..5de889e 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/NullRegionImpl.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/NullRegionImpl.java @@ -5,12 +5,22 @@ public class NullRegionImpl implements IRegionImpl { @Override - public boolean isAllowed(Player player) { + public boolean isAllowedOverall(Player player) { return true; } @Override - public void registerFlag() { + public boolean isAllowedRightClick(Player player) { + return true; + } + + @Override + public boolean isAllowedRemote(Player player) { + return true; + } + + @Override + public void registerFlags() { } } diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/WorldGuardRegionImpl.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/WorldGuardRegionImpl.java index f4ea8cd..4dffdb2 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/WorldGuardRegionImpl.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/impl/region/WorldGuardRegionImpl.java @@ -13,10 +13,31 @@ public class WorldGuardRegionImpl implements IRegionImpl { - private StateFlag flag; + private StateFlag overallFlag; + private StateFlag rightClickFlag; + private StateFlag remoteFlag; @Override - public boolean isAllowed(Player player) { + public boolean isAllowedOverall(Player player) { + if (overallFlag == null) + return true; + + LocalPlayer localPlayer = WorldGuardPlugin.inst().wrapPlayer(player); + + // Bypass + if (WorldGuard.getInstance().getPlatform().getSessionManager().hasBypass(localPlayer, localPlayer.getWorld())) + return true; + + // Region + RegionQuery query = WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery(); + return query.testState(localPlayer.getLocation(), localPlayer, overallFlag); + } + + @Override + public boolean isAllowedRightClick(Player player) { + if (rightClickFlag == null) + return true; + LocalPlayer localPlayer = WorldGuardPlugin.inst().wrapPlayer(player); // Bypass @@ -25,25 +46,48 @@ public boolean isAllowed(Player player) { // Region RegionQuery query = WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery(); - return query.testState(localPlayer.getLocation(), localPlayer, flag); + return query.testState(localPlayer.getLocation(), localPlayer, rightClickFlag); } @Override - public void registerFlag() { + public boolean isAllowedRemote(Player player) { + if (remoteFlag == null) + return true; + + LocalPlayer localPlayer = WorldGuardPlugin.inst().wrapPlayer(player); + + // Bypass + if (WorldGuard.getInstance().getPlatform().getSessionManager().hasBypass(localPlayer, localPlayer.getWorld())) + return true; + + // Region + RegionQuery query = WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery(); + return query.testState(localPlayer.getLocation(), localPlayer, remoteFlag); + } + + private StateFlag registerStateFlag(String name) { FlagRegistry registry = WorldGuard.getInstance().getFlagRegistry(); try { - StateFlag flag = new StateFlag("smiley-player-trader", true); + StateFlag flag = new StateFlag(name, true); registry.register(flag); - this.flag = flag; + return flag; } catch (FlagConflictException e) { - Flag existing = registry.get("smiley-player-trader"); + Flag existing = registry.get(name); if (existing instanceof StateFlag) { - this.flag = (StateFlag) existing; + return (StateFlag) existing; } else { - SmileyPlayerTrader.getInstance().getLogger().warning("Failed to register WorldGuard flag due to incompatible conflicting flag from another plugin."); + SmileyPlayerTrader.getInstance().getLogger().warning("Failed to register WorldGuard flag '" + name + "' due to incompatible conflicting flag from another plugin."); + return null; } } } + @Override + public void registerFlags() { + this.overallFlag = registerStateFlag("smiley-player-trader"); + this.rightClickFlag = registerStateFlag("spt-right-click-trade"); + this.remoteFlag = registerStateFlag("spt-remote-trade"); + } + } diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/merchant/MerchantUtil.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/merchant/MerchantUtil.java index 1edc66b..48a7b3b 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/merchant/MerchantUtil.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/util/merchant/MerchantUtil.java @@ -130,7 +130,7 @@ public static void openMerchant(Player player, OfflinePlayer store, boolean unsu } } - if (!RegionUtil.isAllowed(player)) { + if (!RegionUtil.isAllowedOverall(player)) { if (unsuccessfulFeedback) player.sendMessage(I18N.translate("&cYou cannot trade here.")); return; diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/versions/MCVersion1_21_R7.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/versions/MCVersion1_21_R7.java new file mode 100644 index 0000000000..1da93ec --- /dev/null +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/versions/MCVersion1_21_R7.java @@ -0,0 +1,358 @@ +package io.github.mrcomputer1.smileyplayertrader.versions; + +import io.github.mrcomputer1.smileyplayertrader.util.merchant.MerchantRecipe; +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Merchant; +import org.bukkit.inventory.MerchantInventory; +import org.bukkit.inventory.meta.ItemMeta; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +public class MCVersion1_21_R7 implements IMCVersion { + // Sources: NMS = net.minecraft.server, OBC = org.bukkit.craftbukkit + // Source_ClassName_Static/Instance_MethodName_ArgTypes... + + private Object registryAccess; + private Object nbtOps; + private Object itemStackCodec; + + // NMS: net.minecraft.nbt.NbtAccounter -> NBTReadLimiter + private Method NMS_NbtAccounter_Static_unlimitedHeap_; + + // NMS: net.minecraft.nbt.NbtIo -> NBTCompressedStreamTools + private Method NMS_NbtIo_Static_readCompressed_InputStream_NbtAccounter; + private Method NMS_NbtIo_Static_writeCompressed_CompoundTag_OutputStream; + + // NMS: net.minecraft.world.item.ItemStack -> ItemStack + private Method NMS_ItemStack_Instance_isEmpty_; + + // NMS: com.mojang.serialization.Codec + private Method NMS_Codec_Instance_encodeStart_DynamicOps_Object; + private Method NMS_Codec_Instance_parse_DynamicOps_Tag; + + // NMS: net.minecraft.core.HolderLookup$Provider -> HolderLookup$a + private Method NMS_HolderLookup$Provider_Instance_createSerializationContext_DynamicOps; + + // NMS: com.mojang.serialisation.DataResult + private Method NMS_DataResult_Instance_getOrThrow_; + + // OBC: org.bukkit.craftbukkit._.inventory.CraftItemStack + private Method OBC_CraftItemStack_Static_asCraftMirror_ItemStack; + private Method OBC_CraftItemStack_Static_asNMSCopy_ItemStack; + + // OB: org.bukkit.inventory.MerchantRecipe + private Field OB_MerchantRecipe_Instance_recipe; + + // OBC: org.bukkit.craftbukkit._.inventory.CraftMerchant + private Method OBC_CraftMerchant_Instance_getMerchant_; + + // NMS: net.minecraft.world.item.trading.Merchant -> IMerchant + private Method NMS_Merchant_Instance_getOffers_; + + // NMS: net.minecraft.world.item.trading.MerchantOffer -> MerchantRecipe + private Method NMS_MerchantOffer_Instance_setSpecialPriceDiff_int; + private Method NMS_MerchantOffer_Instance_getSpecialPriceDiff_; + + // NMS: net.minecraft.world.item.trading.MerchantOffers -> MerchantRecipeList + private Method NMS_MerchantOffers_Instance_clear_; + private Method NMS_MerchantOffers_Instance_add_MerchantOffer; + + // OBC: org.bukkit.craftbukkit._.inventory.CraftMerchantRecipe + private Method OBC_CraftMerchantRecipe_Static_fromBukkit_MerchantRecipe; + private Method OBC_CraftMerchantRecipe_Instance_toMinecraft_; + + // OBC: org.bukkit.craftbukkit._.inventory.CraftInventoryMerchant + private Method OBC_CraftInventoryMerchant_Instance_getInventory_; + + // NMS: net.minecraft.world.inventory.MerchantContainer -> InventoryMerchant + private Method NMS_MerchantContainer_Instance_getActiveOffer_; + + // OB: org.bukkit.inventory.meta.ItemMeta + private Method OB_ItemMeta_Instance_hasItemName_; + private Method OB_ItemMeta_Instance_getItemName_; + + public MCVersion1_21_R7(World world){ + try { + /* + * Classes + */ + Class NMS_DynamicOps = Class.forName("com.mojang.serialization.DynamicOps"); + Class NMS_NbtOps = Class.forName("net.minecraft.nbt.DynamicOpsNBT"); + Class NMS_HolderLookup$Provider = Class.forName("net.minecraft.core.HolderLookup$a"); + Class NMS_Level = Class.forName("net.minecraft.world.level.World"); + Class NMS_CompoundTag = Class.forName("net.minecraft.nbt.NBTTagCompound"); + Class NMS_NbtAccounter = Class.forName("net.minecraft.nbt.NBTReadLimiter"); + Class NMS_NbtIo = Class.forName("net.minecraft.nbt.NBTCompressedStreamTools"); + Class NMS_ItemStack = Class.forName("net.minecraft.world.item.ItemStack"); + Class NMS_Codec = Class.forName("com.mojang.serialization.Codec"); + Class NMS_DataResult = Class.forName("com.mojang.serialization.DataResult"); + Class NMS_Merchant = Class.forName("net.minecraft.world.item.trading.IMerchant"); + Class NMS_MerchantOffer = Class.forName("net.minecraft.world.item.trading.MerchantRecipe"); + Class NMS_MerchantOffers = Class.forName("net.minecraft.world.item.trading.MerchantRecipeList"); + Class NMS_MerchantContainer = Class.forName("net.minecraft.world.inventory.InventoryMerchant"); + + Class OB_MerchantRecipe = Class.forName("org.bukkit.inventory.MerchantRecipe"); + Class OB_ItemMeta = Class.forName("org.bukkit.inventory.meta.ItemMeta"); + Class OBC_CraftWorld = Class.forName("org.bukkit.craftbukkit.v1_21_R7.CraftWorld"); + Class OBC_CraftItemStack = Class.forName("org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack"); + Class OBC_CraftMerchant = Class.forName("org.bukkit.craftbukkit.v1_21_R7.inventory.CraftMerchant"); + Class OBC_CraftMerchantRecipe = Class.forName("org.bukkit.craftbukkit.v1_21_R7.inventory.CraftMerchantRecipe"); + Class OBC_CraftInventoryMerchant = Class.forName("org.bukkit.craftbukkit.v1_21_R7.inventory.CraftInventoryMerchant"); + + /* + * Fields and Methods + */ + // net.minecraft.nbt.NbtOps + // Static Field: NMS: NbtOps.INSTANCE + Field NMS_NbtOps_Static_INSTANCE = NMS_NbtOps.getField("a"); + + // net.minecraft.core.HolderLookup$Provider + // Instance Method: NMS: HolderLookup$Provider.createSerializationContext(DynamicOps) + this.NMS_HolderLookup$Provider_Instance_createSerializationContext_DynamicOps = + NMS_HolderLookup$Provider.getMethod("a", NMS_DynamicOps); + + // org.bukkit.craftbukkit._.CraftWorld + // Instance Method: OBC: CraftWorld.getHandle() + Method OBC_CraftWorld_Instance_getHandle_ = OBC_CraftWorld.getMethod("getHandle"); + + // net.minecraft.world.level.Level + // Instance Method: NMS: Level.registryAccess() + Method NMS_Level_Instance_registryAccess_ = NMS_Level.getMethod("J_"); + + // net.minecraft.nbt.NbtAccounter + // Static Method: NMS: NbtAccounter.unlimitedHeap() + this.NMS_NbtAccounter_Static_unlimitedHeap_ = NMS_NbtAccounter.getMethod("c"); + + // net.minecraft.nbt.NbtIo + // Static Method: NMS: NbtIo.readCompressed(InputStream, NbtAccounter) + this.NMS_NbtIo_Static_readCompressed_InputStream_NbtAccounter = + NMS_NbtIo.getMethod("a", InputStream.class, NMS_NbtAccounter); + // Static Method: NMS: NbtIo.writeCompressed(CompoundTag, OutputStream) + this.NMS_NbtIo_Static_writeCompressed_CompoundTag_OutputStream = + NMS_NbtIo.getMethod("a", NMS_CompoundTag, OutputStream.class); + + // net.minecraft.world.item.ItemStack + // Static Field: NMS: ItemStack.CODEC + Field NMS_ItemStack_Static_CODEC = NMS_ItemStack.getField("b"); + // Instance Method: NMS: ItemStack.isEmpty() + this.NMS_ItemStack_Instance_isEmpty_ = NMS_ItemStack.getMethod("f"); + + // com.mojang.serialization.Codec + // Instance Method: NMS: Codec.encodeStart(DynamicOps, Object) + this.NMS_Codec_Instance_encodeStart_DynamicOps_Object = + NMS_Codec.getMethod("encodeStart", NMS_DynamicOps, Object.class); + // Instance Method: NMS: Codec.parse(DynamicOps, Tag/Object) + this.NMS_Codec_Instance_parse_DynamicOps_Tag = + NMS_Codec.getMethod("parse", NMS_DynamicOps, Object.class); + + // com.mojang.serialization.DataResult + // Instance Method: NMS: DataResult.getOrThrow() + this.NMS_DataResult_Instance_getOrThrow_ = NMS_DataResult.getMethod("getOrThrow"); + + // org.bukkit.craftbukkit._.inventory.CraftItemStack + // Static Method: OBC: CraftItemStack.asCraftMirror(NMS: ItemStack) + this.OBC_CraftItemStack_Static_asCraftMirror_ItemStack = + OBC_CraftItemStack.getMethod("asCraftMirror", NMS_ItemStack); + // Static Method: OBC: CraftItemStack.asNMSCopy(ItemStack) + this.OBC_CraftItemStack_Static_asNMSCopy_ItemStack = + OBC_CraftItemStack.getMethod("asNMSCopy", ItemStack.class); + + // org.bukkit.inventory.MerchantRecipe + // Instance Field: OB: MerchantRecipe.result + this.OB_MerchantRecipe_Instance_recipe = OB_MerchantRecipe.getDeclaredField("result"); + this.OB_MerchantRecipe_Instance_recipe.setAccessible(true); + + // org.bukkit.craftbukkit._.inventory.CraftMerchant + // Instance Method: OBC: CraftMerchant.getMerchant() + this.OBC_CraftMerchant_Instance_getMerchant_ = OBC_CraftMerchant.getMethod("getMerchant"); + + // net.minecraft.world.item.trading.Merchant + // Instance Method: NMS: Merchant.getOffers() + this.NMS_Merchant_Instance_getOffers_ = NMS_Merchant.getMethod("b"); + + // net.minecraft.world.item.trading.MerchantOffer + // Instance Method: NMS: MerchantOffer.setSpecialPriceDiff(int) + this.NMS_MerchantOffer_Instance_setSpecialPriceDiff_int = NMS_MerchantOffer.getMethod("b", int.class); + // Instance Method: NMS: MerchantOffer.getSpecialPriceDiff() + this.NMS_MerchantOffer_Instance_getSpecialPriceDiff_ = NMS_MerchantOffer.getMethod("o"); + + // net.minecraft.world.item.trading.MerchantOffers + // Instance Method: NMS: MerchantOffers.clear() + this.NMS_MerchantOffers_Instance_clear_ = NMS_MerchantOffers.getMethod("clear"); + // Instance Method: NMS: MerchantOffers.add(MerchantOffer) + this.NMS_MerchantOffers_Instance_add_MerchantOffer = + NMS_MerchantOffers.getMethod("add", Object.class); + + // org.bukkit.craftbukkit._.inventory.CraftMerchantRecipe + // Static Method: OBC: CraftMerchantRecipe.fromBukkit(MerchantRecipe) + this.OBC_CraftMerchantRecipe_Static_fromBukkit_MerchantRecipe = + OBC_CraftMerchantRecipe.getMethod("fromBukkit", org.bukkit.inventory.MerchantRecipe.class); + // Instance Method: OBC: CraftMerchantRecipe.toMinecraft() + this.OBC_CraftMerchantRecipe_Instance_toMinecraft_ = OBC_CraftMerchantRecipe.getMethod("toMinecraft"); + + // org.bukkit.craftbukkit._.inventory.CraftInventoryMerchant + // Instance Method: OBC: CraftInventoryMerchant.getInventory() + this.OBC_CraftInventoryMerchant_Instance_getInventory_ = + OBC_CraftInventoryMerchant.getMethod("getInventory"); + + // net.minecraft.world.inventory.MerchantContainer + // Instance Method: NMS: MerchantContainer.getActiveOffer() + this.NMS_MerchantContainer_Instance_getActiveOffer_ = NMS_MerchantContainer.getMethod("h"); + + // org.bukkit.inventory.meta.ItemMeta + // Instance Method: OB: ItemMeta.hasItemName() + //noinspection JavaReflectionMemberAccess + this.OB_ItemMeta_Instance_hasItemName_ = OB_ItemMeta.getMethod("hasItemName"); + //noinspection JavaReflectionMemberAccess + this.OB_ItemMeta_Instance_getItemName_ = OB_ItemMeta.getMethod("getItemName"); + + /* + * Constants + */ + // this.nbtOps = NbtOps.INSTANCE + this.nbtOps = NMS_NbtOps_Static_INSTANCE.get(null); + + // Object nmsWorld = CraftWorld.getHandle(world) + Object nmsWorld = OBC_CraftWorld_Instance_getHandle_.invoke(world); + + // this.registryAccess = nmsWorld.registryAccess() + this.registryAccess = NMS_Level_Instance_registryAccess_.invoke(nmsWorld); + + // this.itemStackCodec = ItemStack.CODEC + this.itemStackCodec = NMS_ItemStack_Static_CODEC.get(null); + } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + @Override + public ItemStack byteArrayToItemStack(byte[] array) throws InvocationTargetException { + try { + // NbtAccounter readLimiter = NbtAccounter.unlimitedHeap() + Object readLimiter = NMS_NbtAccounter_Static_unlimitedHeap_.invoke(null); + // CompoundTag tagCompound = NbtIo.readCompressed(new ByteArrayInputStream(array), readLimiter) + Object tagCompound = NMS_NbtIo_Static_readCompressed_InputStream_NbtAccounter.invoke(null, new ByteArrayInputStream(array), readLimiter); + + // DynamicOps dynamicOps = registryAccess.createSerializationContext(nbtOps) + Object dynamicOps = + NMS_HolderLookup$Provider_Instance_createSerializationContext_DynamicOps.invoke(registryAccess, nbtOps); + + // DataResult dataResult = itemStackCodec.parse(dynamicOps, tagCompound) + Object dataResult = + NMS_Codec_Instance_parse_DynamicOps_Tag.invoke(itemStackCodec, dynamicOps, tagCompound); + + // NMS:ItemStack nmsis = dataResult.getOrThrow() + Object nmsis = NMS_DataResult_Instance_getOrThrow_.invoke(dataResult); + // CraftItemStack cis = CraftItemStack.asCraftMirror(nmsis) + Object cis = OBC_CraftItemStack_Static_asCraftMirror_ItemStack.invoke(null, nmsis); + return (ItemStack)cis; + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public byte[] itemStackToByteArray(ItemStack itemStack) throws InvocationTargetException{ + try { + // NMS:ItemStack nmsis = CraftItemStack.asNMSCopy(itemStack) + Object nmsis = OBC_CraftItemStack_Static_asNMSCopy_ItemStack.invoke(null, itemStack); + + // if (nmsis.isEmpty()) + if ((boolean) NMS_ItemStack_Instance_isEmpty_.invoke(nmsis)) + throw new IllegalStateException("Empty items cannot be encoded."); + + // DynamicOps dynamicOps = registryAccess.createSerializationContext(nbtOps) + Object dynamicOps = + NMS_HolderLookup$Provider_Instance_createSerializationContext_DynamicOps.invoke(registryAccess, nbtOps); + + // DataResult dataResult = itemStackCodec.encodeStart(dynamicOps, nmsis) + Object dataResult = + NMS_Codec_Instance_encodeStart_DynamicOps_Object.invoke(itemStackCodec, dynamicOps, nmsis); + + // CompoundTag tagCompound = dataResult.getOrThrow() + Object tagCompound = NMS_DataResult_Instance_getOrThrow_.invoke(dataResult); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // NbtIo.writeCompressed(tagCompound, baos) + NMS_NbtIo_Static_writeCompressed_CompoundTag_OutputStream.invoke(null, tagCompound, baos); + return baos.toByteArray(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public void setRecipesOnMerchant(Merchant merchant, List recipes) throws InvocationTargetException { + try { + // Merchant im = CraftMerchant.getMerchant(merchant) + Object im = OBC_CraftMerchant_Instance_getMerchant_.invoke(merchant); + // MerchantOffers offers = im.getOffers() + Object offers = NMS_Merchant_Instance_getOffers_.invoke(im); + // offers.clear() + NMS_MerchantOffers_Instance_clear_.invoke(offers); + + for(MerchantRecipe recipe : recipes){ + // CraftMerchantRecipe cmr = CraftMerchantRecipe.fromBukkit(recipe) + Object cmr = OBC_CraftMerchantRecipe_Static_fromBukkit_MerchantRecipe.invoke(null, recipe); + // MerchantOffer mr = cmr.toMinecraft() + Object mr = OBC_CraftMerchantRecipe_Instance_toMinecraft_.invoke(cmr); + // mr.setSpecialPriceDiff(recipe.getSpecialPrice()) + NMS_MerchantOffer_Instance_setSpecialPriceDiff_int.invoke(mr, recipe.getSpecialPrice()); + // offers.add(mr) + NMS_MerchantOffers_Instance_add_MerchantOffer.invoke(offers, mr); + } + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + } + + @Override + public int getSpecialCountForRecipe(MerchantInventory inventory) throws InvocationTargetException{ + try { + // MerchantContainer im = inventory.getInventory() + Object im = OBC_CraftInventoryMerchant_Instance_getInventory_.invoke(inventory); + // MerchantOffer mr = im.getActiveOffer() + Object mr = NMS_MerchantContainer_Instance_getActiveOffer_.invoke(im); + // return mr.getSpecialPriceDiff() + return (int) NMS_MerchantOffer_Instance_getSpecialPriceDiff_.invoke(mr); + } catch (IllegalAccessException e) { + e.printStackTrace(); + return 0; + } + } + + @Override + public ItemStack getMerchantRecipeOriginalResult(org.bukkit.inventory.MerchantRecipe merchantRecipe) { + try { + // return (ItemStack) merchantRecipe.recipe + return (ItemStack) OB_MerchantRecipe_Instance_recipe.get(merchantRecipe); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getPreferredItemName(ItemMeta itemMeta) { + try { + if (itemMeta.hasDisplayName()) { + return itemMeta.getDisplayName(); + } else if ((boolean) OB_ItemMeta_Instance_hasItemName_.invoke(itemMeta)) { + return (String) OB_ItemMeta_Instance_getItemName_.invoke(itemMeta); + } else { + return null; + } + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/io/github/mrcomputer1/smileyplayertrader/versions/VersionSupport.java b/src/main/java/io/github/mrcomputer1/smileyplayertrader/versions/VersionSupport.java index e77c1f4..85b8696 100644 --- a/src/main/java/io/github/mrcomputer1/smileyplayertrader/versions/VersionSupport.java +++ b/src/main/java/io/github/mrcomputer1/smileyplayertrader/versions/VersionSupport.java @@ -154,11 +154,17 @@ public VersionSupportMeta(Callable isSupported, Supplier new MCVersion1_21_R5(Bukkit.getWorlds().get(0)) ); - // 1.21.9 + // 1.21.9 - 1.21.10 registerSupportedVersion( - () -> Pattern.compile("^1\\.21\\.(9|1[0-9]+)-").matcher(bukkitVersion).find(), + () -> Pattern.compile("^1\\.21\\.(9|10)-").matcher(bukkitVersion).find(), () -> new MCVersion1_21_R6(Bukkit.getWorlds().get(0)) ); + + // 1.21.11 + registerSupportedVersion( + () -> Pattern.compile("^1\\.21\\.(1[1-9])-").matcher(bukkitVersion).find(), + () -> new MCVersion1_21_R7(Bukkit.getWorlds().get(0)) + ); } public static void registerSupportedVersion(Callable isSupported, Supplier versionSupplier){ diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index cfcc877..66aae1c 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -112,6 +112,22 @@ purchaseCostComparison: strict # - * %ITEM_NAME% - The name of the item the player is selling. sendNotificationOnNewTrade: "false" +# Commands that are executed as the server when a player trades. +# Supported replacements are these: +# * %MERCHANT%: The name of the player with the trade. +# * %CUSTOMER%: The name of the player that traded. +# * %PRODUCT_TYPE%: The type of the item the player is selling. +# * %PRODUCT_NAME%: The name of the item the player is selling, or the type if no custom name. +# * %PRODUCT_AMOUNT%: The amount of the item that the player is selling. +# * %COST_TYPE%: The type of the primary cost item. +# * %COST_NAME%: The name of the primary cost item, or the type if no custom name. +# * %COST_AMOUNT%: The amount of the primary cost item. +# * %ONLY_IF_NO_COST2%: Makes the command only run if there is no secondary cost, replaced with nothing. +# * %COST2_TYPE%: The type of the secondary cost item. (If no secondary cost, the command isn't executed.) +# * %COST2_NAME%: The name of the secondary cost item, or the type if no custom name. (If no secondary cost, the command isn't executed.) +# * %COST2_AMOUNT%: The amount of the secondary cost item. (If no secondary cost, the command isn't executed.) +onTradeCompleteCommands: [] + # Configures if Vault is used to check permissions of offline players when attempting to trade with them. # Disable this if you have Vault but don't have a permission plugin or the one you have doesn't support offline players. # If disabled, the default permission value will be used instead. For "smileyplayertrader.merchant", this is "allow". @@ -123,6 +139,11 @@ autoCombatLock: combatLockLength: 30 # Combat lock length (in seconds) neverShowNotice: false # Enables/Disables the toggle notice (if this is false, the players can choose for themselves) +# Configures a cooldown period between trades +cooldown: + enabled: false # Enable/Disables cooldown (by default, it is disabled) + seconds: 10 # Cooldown time between trades (in seconds) + # Configure items in the quick selection on the Set Cost screen. You can have up to 6 items. priceQuickSelection: - type: EMERALD diff --git a/src/main/resources/languages/en_us.json b/src/main/resources/languages/en_us.json index 5bae493..4ed43af 100644 --- a/src/main/resources/languages/en_us.json +++ b/src/main/resources/languages/en_us.json @@ -1,5 +1,5 @@ { - "$$version$$": 18, + "$$version$$": 19, "$$credit$$": "", "&e%0% is now trading with you.": "&e%0% is now trading with you.", "&2Villager Store: ": "&2Villager Store: ", @@ -256,5 +256,6 @@ "&cThis item does not match the type of the product so no items were deposited.": "&cThis item does not match the type of the product so no items were deposited.", "&aCreated product %0% with ID %1%.": "&aCreated product %0% with ID %1%.", "&f/spt create &e- Create a new product (run the command to see usage)": "&f/spt create &e- Create a new product (run the command to see usage)", - "&cYou must be holding a matching item in your main hand!": "&cYou must be holding a matching item in your main hand!" + "&cYou must be holding a matching item in your main hand!": "&cYou must be holding a matching item in your main hand!", + "&cYou are currently on cooldown for %0% more seconds.": "&cYou are currently on cooldown for %0% more seconds." } \ No newline at end of file diff --git a/src/main/resources/languages/pl_pl.json b/src/main/resources/languages/pl_pl.json index d24f9b2..a84c0ca 100644 --- a/src/main/resources/languages/pl_pl.json +++ b/src/main/resources/languages/pl_pl.json @@ -1,5 +1,5 @@ { - "$$version$$": 17, + "$$version$$": 18, "$$credit$$": "Polskie tłumaczenie wykonane przez gvvda21", "&e%0% is now trading with you.": "&e%0% przegląda twój sklep.", "&2Villager Store: ": "&2Sklep osadnika: ", @@ -256,5 +256,6 @@ "&cThis item does not match the type of the product so no items were deposited.": "&cThis item does not match the type of the product so no items were deposited.", "&aCreated product %0% with ID %1%.": "&aCreated product %0% with ID %1%.", "&f/spt create &e- Create a new product (run the command to see usage)": "&f/spt create &e- Create a new product (run the command to see usage)", - "&cYou must be holding a matching item in your main hand!": "&cYou must be holding a matching item in your main hand!" + "&cYou must be holding a matching item in your main hand!": "&cYou must be holding a matching item in your main hand!", + "&cYou are currently on cooldown for %0% more seconds.": "&cYou are currently on cooldown for %0% more seconds." } \ No newline at end of file diff --git a/src/main/resources/languages/ru_ru.json b/src/main/resources/languages/ru_ru.json index 4b3fa47..7c8203d 100644 --- a/src/main/resources/languages/ru_ru.json +++ b/src/main/resources/languages/ru_ru.json @@ -1,5 +1,5 @@ { - "$$version$$": 24, + "$$version$$": 25, "$$credit$$": "", "&e%0% is now trading with you.": "&e%0% сейчас с тобой трейдится.", "&2Villager Store: ": "&2Магазин Жителя: ", @@ -256,5 +256,6 @@ "&cThis item does not match the type of the product so no items were deposited.": "&cЭтот предмет не соответствует типу продукта, предметы не были вложены.", "&aCreated product %0% with ID %1%.": "&aСоздан продукт %0% под ID %1%.", "&f/spt create &e- Create a new product (run the command to see usage)": "&f/spt create &e- Создать новый продукт (выполните команду чтобы посмотреть использование)", - "&cYou must be holding a matching item in your main hand!": "&cВы должны держать соответствующий предмет в вашей главной руке!" + "&cYou must be holding a matching item in your main hand!": "&cВы должны держать соответствующий предмет в вашей главной руке!", + "&cYou are currently on cooldown for %0% more seconds.": "&cYou are currently on cooldown for %0% more seconds." } \ No newline at end of file diff --git a/src/main/resources/languages/vi_vn.json b/src/main/resources/languages/vi_vn.json index 9dce5b5..6ebc2b9 100644 --- a/src/main/resources/languages/vi_vn.json +++ b/src/main/resources/languages/vi_vn.json @@ -1,5 +1,5 @@ { - "$$version$$": 18, + "$$version$$": 19, "$$credit$$": "Bản dịch tiếng Việt được dịch bởi CoPeBanSIMP", "&e%0% is now trading with you.": "&e%0% hiện tại đang mở shop của bạn.", "&2Villager Store: ": "&1Người bán: &4&l", @@ -256,5 +256,6 @@ "&cThis item does not match the type of the product so no items were deposited.": "&cThis item does not match the type of the product so no items were deposited.", "&aCreated product %0% with ID %1%.": "&aCreated product %0% with ID %1%.", "&f/spt create &e- Create a new product (run the command to see usage)": "&f/spt create &e- Create a new product (run the command to see usage)", - "&cYou must be holding a matching item in your main hand!": "&cYou must be holding a matching item in your main hand!" + "&cYou must be holding a matching item in your main hand!": "&cYou must be holding a matching item in your main hand!", + "&cYou are currently on cooldown for %0% more seconds.": "&cYou are currently on cooldown for %0% more seconds." } \ No newline at end of file diff --git a/src/main/resources/languages/zh_cn.json b/src/main/resources/languages/zh_cn.json index e9fdbc7..705a1e6 100644 --- a/src/main/resources/languages/zh_cn.json +++ b/src/main/resources/languages/zh_cn.json @@ -1,5 +1,5 @@ { - "$$version$$": 2, + "$$version$$": 3, "$$credit$$": "", "&e%0% is now trading with you.": "&e%0% 正在与您交易。", "&2Villager Store: ": "&2村民商店:", @@ -256,5 +256,6 @@ "&cThis item does not match the type of the product so no items were deposited.": "&cThis item does not match the type of the product so no items were deposited.", "&aCreated product %0% with ID %1%.": "&aCreated product %0% with ID %1%.", "&f/spt create &e- Create a new product (run the command to see usage)": "&f/spt create &e- Create a new product (run the command to see usage)", - "&cYou must be holding a matching item in your main hand!": "&cYou must be holding a matching item in your main hand!" + "&cYou must be holding a matching item in your main hand!": "&cYou must be holding a matching item in your main hand!", + "&cYou are currently on cooldown for %0% more seconds.": "&cYou are currently on cooldown for %0% more seconds." } \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index a2c53a1..b914184 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -36,6 +36,8 @@ permissions: "smileyplayertrader.alltradeslist": true "smileyplayertrader.unlimitedsupply": true "smileyplayertrader.reload": true + "smileyplayertrader.bypasscooldown": true + "smileyplayertrader.trade.rightclick": true "smileyplayertrader.others": description: "Change other players" default: op @@ -45,6 +47,9 @@ permissions: "smileyplayertrader.trade.remote": description: "Trade with players via a command" default: true + "smileyplayertrader.trade.rightclick": + description: "Trade with players by right clicking on them" + default: true "smileyplayertrader.merchant": description: "Be able to trade with" default: true @@ -65,4 +70,7 @@ permissions: default: op "smileyplayertrader.reload": description: "Reload the configuration." + default: op + "smileyplayertrader.bypasscooldown": + description: "Bypass trade cooldown (if enabled)." default: op \ No newline at end of file