From 8720e840e296f66da4516ecea70d9dbcc8213e8c Mon Sep 17 00:00:00 2001 From: PretzelJohn <58197328+PretzelJohn@users.noreply.github.com> Date: Sat, 15 Jan 2022 12:10:28 -0500 Subject: [PATCH] Version 1.5.0-pre4: * Add per-villager restock cooldowns To-do: * Add enchantments, data, etc. to ingredients and result * Add config editor GUI in-game --- .../VillagerTradeLimiter.java | 3 + .../villagertradelimiter/data/Cooldown.java | 8 +++ .../database/DatabaseManager.java | 22 +++--- .../listeners/InventoryListener.java | 24 ++++--- .../listeners/PlayerListener.java | 8 +++ .../listeners/VillagerListener.java | 69 +++++++++++++++++++ src/main/resources/config.yml | 9 ++- 7 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 src/com/pretzel/dev/villagertradelimiter/listeners/VillagerListener.java diff --git a/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java b/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java index 19bf9e7..d25919f 100644 --- a/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java +++ b/src/com/pretzel/dev/villagertradelimiter/VillagerTradeLimiter.java @@ -5,6 +5,7 @@ import com.pretzel.dev.villagertradelimiter.commands.CommandBase; import com.pretzel.dev.villagertradelimiter.data.PlayerData; import com.pretzel.dev.villagertradelimiter.database.DatabaseManager; import com.pretzel.dev.villagertradelimiter.listeners.InventoryListener; +import com.pretzel.dev.villagertradelimiter.listeners.VillagerListener; import com.pretzel.dev.villagertradelimiter.settings.ConfigUpdater; import com.pretzel.dev.villagertradelimiter.lib.Metrics; import com.pretzel.dev.villagertradelimiter.lib.Util; @@ -76,6 +77,7 @@ public class VillagerTradeLimiter extends JavaPlugin { //Load/reload database manager if(this.databaseManager == null) this.databaseManager = new DatabaseManager(this); + else onDisable(); this.databaseManager.load(); } @@ -99,6 +101,7 @@ public class VillagerTradeLimiter extends JavaPlugin { this.playerListener = new PlayerListener(this, settings); this.getServer().getPluginManager().registerEvents(this.playerListener, this); this.getServer().getPluginManager().registerEvents(new InventoryListener(this, settings), this); + this.getServer().getPluginManager().registerEvents(new VillagerListener(this, settings), this); } diff --git a/src/com/pretzel/dev/villagertradelimiter/data/Cooldown.java b/src/com/pretzel/dev/villagertradelimiter/data/Cooldown.java index 4416f95..f47764f 100644 --- a/src/com/pretzel/dev/villagertradelimiter/data/Cooldown.java +++ b/src/com/pretzel/dev/villagertradelimiter/data/Cooldown.java @@ -39,10 +39,18 @@ public class Cooldown { return 0; } + /** + * @param date The date to format + * @return The date as a 'yyyy-MM-dd HH:mm:ss' formatted string + */ public static String formatTime(Date date) { return FORMAT.format(date); } + /** + * @param timeStr The string to format, in 'yyyy-MM-dd HH:mm:ss' format + * @return The date that the string represents + */ public static Date parseTime(final String timeStr) { try { return FORMAT.parse(timeStr); diff --git a/src/com/pretzel/dev/villagertradelimiter/database/DatabaseManager.java b/src/com/pretzel/dev/villagertradelimiter/database/DatabaseManager.java index ad85e51..0195761 100644 --- a/src/com/pretzel/dev/villagertradelimiter/database/DatabaseManager.java +++ b/src/com/pretzel/dev/villagertradelimiter/database/DatabaseManager.java @@ -4,7 +4,9 @@ import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter; import com.pretzel.dev.villagertradelimiter.data.Cooldown; import com.pretzel.dev.villagertradelimiter.data.PlayerData; import com.pretzel.dev.villagertradelimiter.lib.Util; +import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Villager; import java.time.Instant; import java.util.Date; @@ -52,18 +54,20 @@ public class DatabaseManager { if(date == null) continue; long time = date.getTime(); - PlayerData playerData = instance.getPlayerData().get(uuid); - if(playerData == null) { - playerData = new PlayerData(); - instance.getPlayerData().put(uuid, playerData); + PlayerData data = instance.getPlayerData().get(uuid); + if(data == null) { + data = new PlayerData(); + instance.getPlayerData().put(uuid, data); } + String key = (Bukkit.getEntity(uuid) instanceof Villager ? "Restock" : "Cooldown"); + String cooldownStr = instance.getCfg().getString(key, "0"); + cooldownStr = instance.getCfg().getString("Overrides."+item+"."+key, cooldownStr); + long cooldown = Cooldown.parseCooldown(cooldownStr); + final Date now = Date.from(Instant.now()); - final String global = instance.getCfg().getString("Cooldown", "0"); - final String local = instance.getCfg().getString("Overrides."+item+".Cooldown", global); - long cooldown = Cooldown.parseCooldown(local); if(cooldown != 0 && now.getTime()/1000L < time/1000L + cooldown) { - playerData.getTradingCooldowns().put(item, Cooldown.formatTime(date)); + data.getTradingCooldowns().put(item, Cooldown.formatTime(date)); } } } @@ -88,7 +92,7 @@ public class DatabaseManager { String values = ""; for(String item : playerData.getTradingCooldowns().keySet()) { - String time = playerData.getTradingCooldowns().get(item); + final String time = playerData.getTradingCooldowns().get(item); if(!values.isEmpty()) values += ","; values += "('"+uuid+"','"+item+"','"+time+"')"; diff --git a/src/com/pretzel/dev/villagertradelimiter/listeners/InventoryListener.java b/src/com/pretzel/dev/villagertradelimiter/listeners/InventoryListener.java index 49b74fa..80d666c 100644 --- a/src/com/pretzel/dev/villagertradelimiter/listeners/InventoryListener.java +++ b/src/com/pretzel/dev/villagertradelimiter/listeners/InventoryListener.java @@ -79,13 +79,12 @@ public class InventoryListener implements Listener { if(overrides == null) return; final String type = settings.getType(result, ingredient1, ingredient2); - if(instance.getCfg().getString("Cooldown", "0").equals("0")) { - Util.consoleMsg("global cooldown is 0"); - if(overrides.getString(type+".Cooldown", "0").equals("0")) { - Util.consoleMsg("local cooldown is 0"); - return; - } - } + String cooldownStr = instance.getCfg().getString("Cooldown", "0"); + cooldownStr = overrides.getString(type+".Cooldown", cooldownStr); + String restockStr = instance.getCfg().getString("Restock", "0"); + restockStr = overrides.getString(type+".Restock", restockStr); + + if(cooldownStr.equals("0") && restockStr.equals("0")) return; //Get the selected recipe by the items in the slots final MerchantRecipe selectedRecipe = getSelectedRecipe((Villager)event.getInventory().getHolder(), ingredient1, ingredient2, result); @@ -96,11 +95,18 @@ public class InventoryListener implements Listener { //Add a cooldown to the trade if the player has reached the max uses final PlayerData playerData = instance.getPlayerData().get(player.getUniqueId()); + final PlayerData villagerData = instance.getPlayerData().get(((Villager)event.getInventory().getHolder()).getUniqueId()); if(playerData == null || playerData.getTradingVillager() == null) return; Bukkit.getScheduler().runTaskLater(instance, () -> { int uses = selectedRecipe.getUses(); - if(!playerData.getTradingCooldowns().containsKey(type) && uses >= selectedRecipe.getMaxUses()) { - playerData.getTradingCooldowns().put(type, Cooldown.formatTime(Date.from(Instant.now()))); + final String time = Cooldown.formatTime(Date.from(Instant.now())); + if(uses >= selectedRecipe.getMaxUses()) { + if(!playerData.getTradingCooldowns().containsKey(type)) { + playerData.getTradingCooldowns().put(type, time); + } + if(villagerData != null && !villagerData.getTradingCooldowns().containsKey(type)) { + villagerData.getTradingCooldowns().put(type, time); + } } }, 1); } diff --git a/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java b/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java index 447b87f..ef3cd49 100644 --- a/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java +++ b/src/com/pretzel/dev/villagertradelimiter/listeners/PlayerListener.java @@ -67,6 +67,9 @@ public class PlayerListener implements Listener { if(!instance.getPlayerData().containsKey(player.getUniqueId())) { instance.getPlayerData().put(player.getUniqueId(), new PlayerData()); } + if(!instance.getPlayerData().containsKey(villager.getUniqueId())) { + instance.getPlayerData().put(villager.getUniqueId(), new PlayerData()); + } this.see(villager, player, player); } @@ -143,6 +146,7 @@ public class PlayerListener implements Listener { * @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) { + //Calculates the total discount int basePrice = getBasePrice(recipe); int demand = getDemand(recipe); float priceMultiplier = recipe.getPriceMultiplier(); @@ -150,10 +154,12 @@ public class PlayerListener implements Listener { double maxDiscount = settings.fetchDouble(recipe, "MaxDiscount", 0.3); if(maxDiscount >= 0.0 && maxDiscount <= 1.0) { + //Change the discount to the smaller MaxDiscount if(basePrice + discount < basePrice * (1.0 - maxDiscount)) { discount = -(int)(basePrice * maxDiscount); } } else if(maxDiscount > 1.0) { + //Change the discount to the larger MaxDiscount //TODO: Allow for better fine-tuning discount = (int)(discount * maxDiscount); } @@ -169,6 +175,7 @@ public class PlayerListener implements Listener { int maxUses = settings.fetchInt(recipe, "MaxUses", -1); boolean disabled = settings.fetchBoolean(recipe, "Disabled", false); + //Disables the trade if the player has an active cooldown for the trade final PlayerData playerData = instance.getPlayerData().get(player.getUniqueId()); if(playerData != null && playerData.getTradingVillager() != null) { final ConfigurationSection overrides = instance.getCfg().getConfigurationSection("Overrides"); @@ -210,6 +217,7 @@ public class PlayerListener implements Listener { final PotionEffect effect = player.getPotionEffect(effectType); if(effect == null) return 0.0; + //Calculates the discount factor from the player's current effect level or the defined maximum int heroLevel = effect.getAmplifier()+1; final int maxHeroLevel = instance.getCfg().getInt("MaxHeroLevel", -1); if(maxHeroLevel == 0 || heroLevel == 0) return 0.0; diff --git a/src/com/pretzel/dev/villagertradelimiter/listeners/VillagerListener.java b/src/com/pretzel/dev/villagertradelimiter/listeners/VillagerListener.java new file mode 100644 index 0000000..1b6da07 --- /dev/null +++ b/src/com/pretzel/dev/villagertradelimiter/listeners/VillagerListener.java @@ -0,0 +1,69 @@ +package com.pretzel.dev.villagertradelimiter.listeners; + +import com.pretzel.dev.villagertradelimiter.VillagerTradeLimiter; +import com.pretzel.dev.villagertradelimiter.data.Cooldown; +import com.pretzel.dev.villagertradelimiter.data.PlayerData; +import com.pretzel.dev.villagertradelimiter.lib.Util; +import com.pretzel.dev.villagertradelimiter.settings.Settings; +import org.bukkit.entity.Villager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.VillagerReplenishTradeEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.MerchantRecipe; + +import java.time.Instant; +import java.util.Date; +import java.util.UUID; + +public class VillagerListener implements Listener { + private final VillagerTradeLimiter instance; + private final Settings settings; + + /** + * @param instance The instance of VillagerTradeLimiter.java + * @param settings The settings instance + */ + public VillagerListener(final VillagerTradeLimiter instance, final Settings settings) { + this.instance = instance; + this.settings = settings; + } + + /** Handles villager restocks */ + @EventHandler + public void onVillagerRestock(final VillagerReplenishTradeEvent event) { + if(!(event.getEntity() instanceof Villager)) return; + if(Util.isNPC((Villager) event.getEntity())) return; + + //Get the items involved in the restock + final MerchantRecipe recipe = event.getRecipe(); + final ItemStack result = recipe.getResult(); + ItemStack ingredient1 = recipe.getIngredients().get(0); + ItemStack ingredient2 = recipe.getIngredients().get(1); + final String type = settings.getType(result, ingredient1, ingredient2); + + //Get the villager's data container + final UUID uuid = event.getEntity().getUniqueId(); + final PlayerData villagerData = instance.getPlayerData().get(uuid); + if(villagerData == null) return; + + //Get the time of the last trade, restock cooldown setting, and now + final String lastTradeStr = villagerData.getTradingCooldowns().get(type); + if(lastTradeStr == null) return; + + String cooldownStr = instance.getCfg().getString("Restock", "0"); + cooldownStr = instance.getCfg().getString("Overrides."+type+".Restock", cooldownStr); + + final Date now = Date.from(Instant.now()); + final Date lastTrade = Cooldown.parseTime(lastTradeStr); + if(lastTrade == null) return; + final long cooldown = Cooldown.parseCooldown(cooldownStr); + + //Cancel the event if there is an active restock cooldown, otherwise remove the restock cooldown + if(now.getTime()/1000L >= lastTrade.getTime()/1000L + cooldown) { + villagerData.getTradingCooldowns().remove(type); + } else { + event.setCancelled(true); + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index bf89877..a3a67ed 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -61,6 +61,12 @@ MaxUses: -1 # * w = weeks (e.g. 2w) Cooldown: 0 +# The per-villager, per-trade cooldown in real-world time. +# This is the same as Cooldown, but applies to a villager's restocking function +# * Set to 0 to disable this feature and keep vanilla behavior +# * Set to a number and interval to add a per-villager, per-trade cooldown for all trades (see below) +Restock: 0 + #-------------------------------- PER-ITEM SETTINGS --------------------------------# # Override the global settings for individual items. To disable, set like this --> Overrides: none @@ -91,4 +97,5 @@ Overrides: clock: MaxDemand: 12 paper: - Disabled: true \ No newline at end of file + MaxUses: 1 + Restock: 1h \ No newline at end of file