diff --git a/VillagerOptimizer-1.16.5/pom.xml b/VillagerOptimizer-1.16.5/pom.xml
new file mode 100644
index 0000000..d778ad9
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+
+ me.xginko.VillagerOptimizer
+ VillagerOptimizer
+ 1.0.0
+
+
+ 1.16.5
+ ${project.parent.artifactId}-${project.parent.version}--${project.artifactId}
+ jar
+
+
+ 16
+ UTF-8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ org.bstats
+ me.xginko.villageroptimizer.bstats
+
+
+ false
+
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+
+
+ papermc-repo
+ https://repo.papermc.io/repository/maven-public/
+
+
+ sonatype
+ https://oss.sonatype.org/content/groups/public/
+
+
+ configmaster-repo
+ https://ci.pluginwiki.us/plugin/repository/everything/
+
+
+
+
+
+ com.destroystokyo.paper
+ paper-api
+ 1.16.5-R0.1-SNAPSHOT
+ provided
+
+
+ net.kyori
+ adventure-text-minimessage
+ 4.14.0
+
+
+ net.kyori
+ adventure-text-serializer-plain
+ 4.14.0
+ compile
+
+
+
diff --git a/src/main/java/me/xginko/villageroptimizer/VillagerCache.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/VillagerCache.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/VillagerCache.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/VillagerCache.java
diff --git a/src/main/java/me/xginko/villageroptimizer/VillagerOptimizer.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/VillagerOptimizer.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/VillagerOptimizer.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/VillagerOptimizer.java
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java
new file mode 100644
index 0000000..1d65f8f
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java
@@ -0,0 +1,188 @@
+package me.xginko.villageroptimizer;
+
+import me.xginko.villageroptimizer.enums.Keys;
+import me.xginko.villageroptimizer.enums.OptimizationType;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import org.bukkit.entity.Villager;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class WrappedVillager {
+
+ private final @NotNull Villager villager;
+ private final @NotNull PersistentDataContainer dataContainer;
+
+ WrappedVillager(@NotNull Villager villager) {
+ this.villager = villager;
+ this.dataContainer = this.villager.getPersistentDataContainer();
+ }
+
+ /**
+ * @return The villager inside the wrapper.
+ */
+ public @NotNull Villager villager() {
+ return villager;
+ }
+
+ /**
+ * @return The data container inside the wrapper.
+ */
+ public @NotNull PersistentDataContainer dataContainer() {
+ return dataContainer;
+ }
+
+ /**
+ * @return True if the villager is optimized by this plugin, otherwise false.
+ */
+ public boolean isOptimized() {
+ return dataContainer.has(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING);
+ }
+
+ /**
+ * @param cooldown_millis The configured cooldown in millis until the next optimization is allowed to occur.
+ * @return True if villager can be optimized again, otherwise false.
+ */
+ public boolean canOptimize(final long cooldown_millis) {
+ return getLastOptimize() + cooldown_millis <= System.currentTimeMillis();
+ }
+
+ /**
+ * @param type OptimizationType the villager should be set to.
+ */
+ public void setOptimization(OptimizationType type) {
+ if (type.equals(OptimizationType.NONE) && isOptimized()) {
+ dataContainer.remove(Keys.OPTIMIZATION_TYPE.key());
+ villager.setAware(true);
+ villager.setAI(true);
+ } else {
+ dataContainer.set(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING, type.name());
+ villager.setAware(false);
+ }
+ }
+
+ /**
+ * @return The current OptimizationType of the villager.
+ */
+ public @NotNull OptimizationType getOptimizationType() {
+ return isOptimized() ? OptimizationType.valueOf(dataContainer.get(Keys.OPTIMIZATION_TYPE.key(), PersistentDataType.STRING)) : OptimizationType.NONE;
+ }
+
+ /**
+ * Saves the system time in millis when the villager was last optimized.
+ */
+ public void saveOptimizeTime() {
+ dataContainer.set(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG, System.currentTimeMillis());
+ }
+
+ /**
+ * @return The system time in millis when the villager was last optimized, 0L if the villager was never optimized.
+ */
+ public long getLastOptimize() {
+ return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) : 0L;
+ }
+
+ /**
+ * Here for convenience so the remaining millis since the last stored optimize time
+ * can be easily calculated.
+ * This enables new configured cooldowns to instantly apply instead of them being persistent.
+ *
+ * @param cooldown_millis The configured cooldown in milliseconds you want to check against.
+ * @return The time left in millis until the villager can be optimized again.
+ */
+ public long getOptimizeCooldownMillis(final long cooldown_millis) {
+ return dataContainer.has(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) ? (System.currentTimeMillis() - (dataContainer.get(Keys.LAST_OPTIMIZE.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
+ }
+
+ /**
+ * Here for convenience so the remaining millis since the last stored restock time
+ * can be easily calculated.
+ *
+ * @param cooldown_millis The configured cooldown in milliseconds you want to check against.
+ * @return True if the villager has been loaded long enough.
+ */
+ public boolean canRestock(final long cooldown_millis) {
+ return getLastRestock() + cooldown_millis <= villager.getWorld().getFullTime();
+ }
+
+ /**
+ * Restock all trading recipes.
+ */
+ public void restock() {
+ villager.getRecipes().forEach(recipe -> recipe.setUses(0));
+ }
+
+ /**
+ * Saves the time of the in-game world when the entity was last restocked.
+ */
+ public void saveRestockTime() {
+ dataContainer.set(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG, villager.getWorld().getFullTime());
+ }
+
+ /**
+ * @return The time of the in-game world when the entity was last restocked.
+ */
+ public long getLastRestock() {
+ return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) : 0L;
+ }
+
+ public long getRestockCooldownMillis(final long cooldown_millis) {
+ return dataContainer.has(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_RESTOCK.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
+ }
+
+ /**
+ * @return The level between 1-5 calculated from the villagers experience.
+ */
+ public int calculateLevel() {
+ // https://minecraft.fandom.com/wiki/Trading#Mechanics
+ int vilEXP = villager.getVillagerExperience();
+ if (vilEXP >= 250) return 5;
+ if (vilEXP >= 150) return 4;
+ if (vilEXP >= 70) return 3;
+ if (vilEXP >= 10) return 2;
+ return 1;
+ }
+
+ /**
+ * @param cooldown_millis The configured cooldown in milliseconds you want to check against.
+ * @return Whether the villager can be leveled up or not with the checked milliseconds
+ */
+ public boolean canLevelUp(final long cooldown_millis) {
+ return getLastLevelUpTime() + cooldown_millis <= villager.getWorld().getFullTime();
+ }
+
+ /**
+ * Saves the time of the in-game world when the entity was last leveled up.
+ */
+ public void saveLastLevelUp() {
+ dataContainer.set(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG, villager.getWorld().getFullTime());
+ }
+
+ /**
+ * Here for convenience so the remaining millis since the last stored level-up time
+ * can be easily calculated.
+ *
+ * @return The time of the in-game world when the entity was last leveled up.
+ */
+ public long getLastLevelUpTime() {
+ return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) : 0L;
+ }
+
+ public long getLevelCooldownMillis(final long cooldown_millis) {
+ return dataContainer.has(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keys.LAST_LEVELUP.key(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis;
+ }
+
+ public void memorizeName(final Component customName) {
+ dataContainer.set(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING, MiniMessage.miniMessage().serialize(customName));
+ }
+
+ public @Nullable Component getMemorizedName() {
+ return dataContainer.has(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING) ? MiniMessage.miniMessage().deserialize(dataContainer.get(Keys.LAST_OPTIMIZE_NAME.key(), PersistentDataType.STRING)) : null;
+ }
+
+ public void forgetName() {
+ dataContainer.remove(Keys.LAST_OPTIMIZE_NAME.key());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/xginko/villageroptimizer/commands/SubCommand.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/SubCommand.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/commands/SubCommand.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/SubCommand.java
diff --git a/src/main/java/me/xginko/villageroptimizer/commands/VillagerOptimizerCommand.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/VillagerOptimizerCommand.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/commands/VillagerOptimizerCommand.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/VillagerOptimizerCommand.java
diff --git a/src/main/java/me/xginko/villageroptimizer/commands/optimizevillagers/OptVillagersRadius.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/optimizevillagers/OptVillagersRadius.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/commands/optimizevillagers/OptVillagersRadius.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/optimizevillagers/OptVillagersRadius.java
diff --git a/src/main/java/me/xginko/villageroptimizer/commands/unoptimizevillagers/UnOptVillagersRadius.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/unoptimizevillagers/UnOptVillagersRadius.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/commands/unoptimizevillagers/UnOptVillagersRadius.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/unoptimizevillagers/UnOptVillagersRadius.java
diff --git a/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/VillagerOptimizerCmd.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/VillagerOptimizerCmd.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/VillagerOptimizerCmd.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/VillagerOptimizerCmd.java
diff --git a/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java
new file mode 100644
index 0000000..700b736
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java
@@ -0,0 +1,41 @@
+package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.commands.SubCommand;
+import me.xginko.villageroptimizer.enums.Permissions;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.command.CommandSender;
+
+public class ReloadSubCmd extends SubCommand {
+
+ @Override
+ public String getLabel() {
+ return "reload";
+ }
+
+ @Override
+ public TextComponent getDescription() {
+ return Component.text("Reload the plugin configuration.").color(NamedTextColor.GRAY);
+ }
+
+ @Override
+ public TextComponent getSyntax() {
+ return Component.text("/villageroptimizer reload").color(VillagerOptimizer.plugin_style.color());
+ }
+
+ @Override
+ public void perform(CommandSender sender, String[] args) {
+ if (sender.hasPermission(Permissions.Commands.RELOAD.get())) {
+ sender.sendMessage(Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE));
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
+ plugin.reloadPlugin();
+ sender.sendMessage(Component.text("Reload complete.").color(NamedTextColor.GREEN));
+ });
+ } else {
+ sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
+ }
+ }
+}
\ No newline at end of file
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/VersionSubCmd.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/VersionSubCmd.java
new file mode 100644
index 0000000..6e1a910
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/VersionSubCmd.java
@@ -0,0 +1,53 @@
+package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.commands.SubCommand;
+import me.xginko.villageroptimizer.enums.Permissions;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.plugin.PluginDescriptionFile;
+
+public class VersionSubCmd extends SubCommand {
+
+ @Override
+ public String getLabel() {
+ return "version";
+ }
+
+ @Override
+ public TextComponent getDescription() {
+ return Component.text("Show the plugin version.").color(NamedTextColor.GRAY);
+ }
+
+ @Override
+ public TextComponent getSyntax() {
+ return Component.text("/villageroptimizer version").color(VillagerOptimizer.plugin_style.color());
+ }
+
+ @Override
+ public void perform(CommandSender sender, String[] args) {
+ if (sender.hasPermission(Permissions.Commands.VERSION.get())) {
+ final PluginDescriptionFile pluginMeta = VillagerOptimizer.getInstance().getDescription();
+ sender.sendMessage(
+ Component.newline()
+ .append(
+ Component.text(pluginMeta.getName()+" "+pluginMeta.getVersion())
+ .style(VillagerOptimizer.plugin_style)
+ .clickEvent(ClickEvent.openUrl(pluginMeta.getWebsite()))
+ )
+ .append(Component.text(" by ").color(NamedTextColor.GRAY))
+ .append(
+ Component.text(pluginMeta.getAuthors().get(0))
+ .color(NamedTextColor.WHITE)
+ .clickEvent(ClickEvent.openUrl("https://github.com/xGinko"))
+ )
+ .append(Component.newline())
+ );
+ } else {
+ sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/xginko/villageroptimizer/config/Config.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/config/Config.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/config/Config.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/config/Config.java
diff --git a/src/main/java/me/xginko/villageroptimizer/config/LanguageCache.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/config/LanguageCache.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/config/LanguageCache.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/config/LanguageCache.java
diff --git a/src/main/java/me/xginko/villageroptimizer/enums/Keys.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/enums/Keys.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/enums/Keys.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/enums/Keys.java
diff --git a/src/main/java/me/xginko/villageroptimizer/enums/OptimizationType.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/enums/OptimizationType.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/enums/OptimizationType.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/enums/OptimizationType.java
diff --git a/src/main/java/me/xginko/villageroptimizer/enums/Permissions.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/enums/Permissions.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/enums/Permissions.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/enums/Permissions.java
diff --git a/src/main/java/me/xginko/villageroptimizer/events/VillagerOptimizeEvent.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/events/VillagerOptimizeEvent.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/events/VillagerOptimizeEvent.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/events/VillagerOptimizeEvent.java
diff --git a/src/main/java/me/xginko/villageroptimizer/events/VillagerUnoptimizeEvent.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/events/VillagerUnoptimizeEvent.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/events/VillagerUnoptimizeEvent.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/events/VillagerUnoptimizeEvent.java
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/RenameOptimizedVillagers.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/RenameOptimizedVillagers.java
new file mode 100644
index 0000000..f64edd4
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/RenameOptimizedVillagers.java
@@ -0,0 +1,83 @@
+package me.xginko.villageroptimizer.modules;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.WrappedVillager;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
+import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+
+public class RenameOptimizedVillagers implements VillagerOptimizerModule, Listener {
+
+ private final VillagerOptimizer plugin;
+ private final Component optimized_name;
+ private final boolean overwrite_previous_name;
+
+ protected RenameOptimizedVillagers() {
+ this.plugin = VillagerOptimizer.getInstance();
+ Config config = VillagerOptimizer.getConfiguration();
+ config.addComment("general.rename-villagers.enable", """
+ Will change a villager's name to the name configured below when they are optimized.\s
+ These names will be removed when unoptimized again if they were not changed in the meantime.
+ """);
+ this.optimized_name = MiniMessage.miniMessage().deserialize(config.getString("general.rename-villagers.optimized-name", "Optimized",
+ "The name that will be used to mark optimized villagers. Uses MiniMessage format."));
+ this.overwrite_previous_name = config.getBoolean("general.rename-villagers.overwrite-existing-name", false,
+ "If set to true, will rename even if the villager has already been named.");
+ }
+
+ @Override
+ public void enable() {
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("general.rename-villagers.enable", true);
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onOptimize(VillagerOptimizeEvent event) {
+ WrappedVillager wVillager = event.getWrappedVillager();
+ Villager villager = wVillager.villager();
+
+ plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
+ if (overwrite_previous_name) {
+ villager.customName(optimized_name);
+ wVillager.memorizeName(optimized_name);
+ } else {
+ final Component currentName = villager.customName();
+ if (currentName == null) {
+ villager.customName(optimized_name);
+ wVillager.memorizeName(optimized_name);
+ }
+ }
+ }, 10L);
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onUnOptimize(VillagerUnoptimizeEvent event) {
+ WrappedVillager wVillager = event.getWrappedVillager();
+ Villager villager = wVillager.villager();
+
+ plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
+ final Component currentName = villager.customName();
+ final Component memorizedName = wVillager.getMemorizedName();
+ if (memorizedName != null)
+ wVillager.forgetName();
+ if (currentName != null && currentName.equals(memorizedName))
+ villager.customName(null);
+ }, 10L);
+ }
+}
\ No newline at end of file
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/VillagerChunkLimit.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/VillagerChunkLimit.java
new file mode 100644
index 0000000..079b5b8
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/VillagerChunkLimit.java
@@ -0,0 +1,153 @@
+package me.xginko.villageroptimizer.modules;
+
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.utils.LogUtil;
+import org.bukkit.Chunk;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.logging.Level;
+
+public class VillagerChunkLimit implements VillagerOptimizerModule, Listener {
+
+ private final VillagerOptimizer plugin;
+ private final VillagerCache villagerCache;
+ private final List removalPriority = new ArrayList<>(16);
+ private final long check_period;
+ private final int max_unoptimized_per_chunk, max_optimized_per_chunk;
+ private final boolean logIsEnabled;
+
+ protected VillagerChunkLimit() {
+ shouldEnable();
+ this.plugin = VillagerOptimizer.getInstance();
+ this.villagerCache = VillagerOptimizer.getCache();
+ Config config = VillagerOptimizer.getConfiguration();
+ config.addComment("villager-chunk-limit.enable", """
+ Checks chunks for too many villagers and removes excess villagers based on priority.\s
+ Naturally, optimized villagers will be picked last since they don't affect performance\s
+ as much as unoptimized villagers.""");
+ this.max_unoptimized_per_chunk = config.getInt("villager-chunk-limit.max-unoptimized-per-chunk", 30,
+ "The maximum amount of unoptimized villagers per chunk.");
+ this.max_optimized_per_chunk = config.getInt("villager-chunk-limit.max-optimized-per-chunk", 20,
+ "The maximum amount of optimized villagers per chunk.");
+ this.check_period = config.getInt("villager-chunk-limit.check-period-in-ticks", 600,
+ "Check all loaded chunks every X ticks. 1 second = 20 ticks");
+ this.logIsEnabled = config.getBoolean("villager-chunk-limit.log-removals", false);
+ config.getList("villager-chunk-limit.removal-priority", List.of(
+ "NONE", "NITWIT", "SHEPHERD", "FISHERMAN", "BUTCHER", "CARTOGRAPHER", "LEATHERWORKER",
+ "FLETCHER", "MASON", "FARMER", "ARMORER", "TOOLSMITH", "WEAPONSMITH", "CLERIC", "LIBRARIAN"
+ ),
+ "Professions that are in the top of the list are going to be scheduled for removal first."
+
+ ).forEach(configuredProfession -> {
+ try {
+ Villager.Profession profession = Villager.Profession.valueOf(configuredProfession);
+ this.removalPriority.add(profession);
+ } catch (IllegalArgumentException e) {
+ LogUtil.moduleLog(Level.WARNING, "villager-chunk-limit",
+ "Villager profession '"+configuredProfession+"' not recognized. Make sure you're using the correct profession enums.");
+ }
+ });
+ }
+
+ @Override
+ public void enable() {
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, () -> {
+ plugin.getServer().getWorlds().forEach(world -> {
+ for (Chunk chunk : world.getLoadedChunks()) {
+ this.checkVillagersInChunk(chunk);
+ }
+ });
+ }, check_period, check_period);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("villager-chunk-limit.enable", false);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onCreatureSpawn(CreatureSpawnEvent event) {
+ Entity spawned = event.getEntity();
+ if (spawned.getType().equals(EntityType.VILLAGER)) {
+ checkVillagersInChunk(spawned.getChunk());
+ }
+ }
+
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+ private void onInteract(PlayerInteractEntityEvent event) {
+ Entity clicked = event.getRightClicked();
+ if (clicked.getType().equals(EntityType.VILLAGER)) {
+ checkVillagersInChunk(clicked.getChunk());
+ }
+ }
+
+ private void checkVillagersInChunk(Chunk chunk) {
+ // Create lists with all optimized and unoptimzed villagers in that chunk
+ List unoptimized_villagers = new ArrayList<>();
+ List optimized_villagers = new ArrayList<>();
+
+ // Collect villagers accordingly
+ for (Entity entity : chunk.getEntities()) {
+ if (entity.getType().equals(EntityType.VILLAGER)) {
+ Villager villager = (Villager) entity;
+ if (villagerCache.getOrAdd(villager).isOptimized()) {
+ optimized_villagers.add(villager);
+ } else {
+ unoptimized_villagers.add(villager);
+ }
+ }
+ }
+
+ // Check if there are more unoptimized villagers in that chunk than allowed
+ final int unoptimized_vils_too_many = unoptimized_villagers.size() - max_unoptimized_per_chunk;
+ if (unoptimized_vils_too_many > 0) {
+ // Sort villagers by profession priority
+ unoptimized_villagers.sort(Comparator.comparingInt(this::getProfessionPriority));
+ // Remove prioritized villagers that are too many
+ for (int i = 0; i < unoptimized_vils_too_many; i++) {
+ Villager villager = unoptimized_villagers.get(i);
+ villager.remove();
+ if (logIsEnabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
+ "Removed unoptimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation());
+ }
+ }
+
+ // Check if there are more optimized villagers in that chunk than allowed
+ final int optimized_vils_too_many = optimized_villagers.size() - max_optimized_per_chunk;
+ if (optimized_vils_too_many > 0) {
+ // Sort villagers by profession priority
+ optimized_villagers.sort(Comparator.comparingInt(this::getProfessionPriority));
+ // Remove prioritized villagers that are too many
+ for (int i = 0; i < optimized_vils_too_many; i++) {
+ Villager villager = optimized_villagers.get(i);
+ villager.remove();
+ if (logIsEnabled) LogUtil.moduleLog(Level.INFO, "villager-chunk-limit",
+ "Removed optimized villager of profession type '"+villager.getProfession().name()+"' at "+villager.getLocation());
+ }
+ }
+ }
+
+ private int getProfessionPriority(Villager villager) {
+ final Villager.Profession profession = villager.getProfession();
+ return removalPriority.contains(profession) ? removalPriority.indexOf(profession) : Integer.MAX_VALUE;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/VillagerOptimizerModule.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/VillagerOptimizerModule.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/VillagerOptimizerModule.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/VillagerOptimizerModule.java
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/extras/MakeVillagersSpawnAdult.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/extras/MakeVillagersSpawnAdult.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/extras/MakeVillagersSpawnAdult.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/extras/MakeVillagersSpawnAdult.java
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventUnoptimizedTrading.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventUnoptimizedTrading.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/extras/PreventUnoptimizedTrading.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventUnoptimizedTrading.java
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerDamage.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerDamage.java
new file mode 100644
index 0000000..19b67bb
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerDamage.java
@@ -0,0 +1,88 @@
+package me.xginko.villageroptimizer.modules.extras;
+
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Mob;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDamageByBlockEvent;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+
+public class PreventVillagerDamage implements VillagerOptimizerModule, Listener {
+
+ private final VillagerCache villagerCache;
+ private final boolean block, player, mob, other;
+
+ public PreventVillagerDamage() {
+ shouldEnable();
+ this.villagerCache = VillagerOptimizer.getCache();
+ Config config = VillagerOptimizer.getConfiguration();
+ config.addComment("gameplay.prevent-damage.enable",
+ "Configure what kind of damage you want to cancel for optimized villagers here.");
+ this.block = config.getBoolean("gameplay.prevent-damage.damagers.block", false,
+ "Prevents damage from blocks like lava, tnt, respawn anchors, etc.");
+ this.player = config.getBoolean("gameplay.prevent-damage.damagers.player", false,
+ "Prevents damage from getting hit by players.");
+ this.mob = config.getBoolean("gameplay.prevent-damage.damagers.mob", true,
+ "Prevents damage from hostile mobs.");
+ this.other = config.getBoolean("gameplay.prevent-damage.damagers.other", true,
+ "Prevents damage from all other entities.");
+ }
+
+ @Override
+ public void enable() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-damage.enable", true);
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onDamageByEntity(EntityDamageByEntityEvent event) {
+ if (
+ event.getEntityType().equals(EntityType.VILLAGER)
+ && villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
+ ) {
+ Entity damager = event.getDamager();
+ if (damager.getType().equals(EntityType.PLAYER)) {
+ if (player) event.setCancelled(true);
+ return;
+ }
+
+ if (damager instanceof Mob) {
+ if (mob) event.setCancelled(true);
+ return;
+ }
+
+ if (other) {
+ event.setCancelled(true);
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onDamageByBlock(EntityDamageByBlockEvent event) {
+ if (
+ block
+ && event.getEntityType().equals(EntityType.VILLAGER)
+ && villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
+ ) {
+ event.setCancelled(true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerTargetting.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerTargetting.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerTargetting.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerTargetting.java
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/mechanics/LevelVillagers.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/mechanics/LevelVillagers.java
new file mode 100644
index 0000000..37a5d62
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/mechanics/LevelVillagers.java
@@ -0,0 +1,90 @@
+package me.xginko.villageroptimizer.modules.mechanics;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.WrappedVillager;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import me.xginko.villageroptimizer.utils.CommonUtil;
+import net.kyori.adventure.text.TextReplacementConfig;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+
+public class LevelVillagers implements VillagerOptimizerModule, Listener {
+
+ private final VillagerOptimizer plugin;
+ private final VillagerCache villagerCache;
+ private final boolean shouldNotify;
+ private final long cooldown;
+
+ public LevelVillagers() {
+ shouldEnable();
+ this.plugin = VillagerOptimizer.getInstance();
+ this.villagerCache = VillagerOptimizer.getCache();
+ Config config = VillagerOptimizer.getConfiguration();
+ config.addComment("gameplay.villager-leveling.enable", """
+ This is needed to allow optimized villagers to level up.\s
+ Temporarily enables the villagers AI to allow it to level up and then disables it again.""");
+ this.cooldown = config.getInt("gameplay.villager-leveling.level-check-cooldown-seconds", 5, """
+ Cooldown in seconds until the level of a villager will be checked and updated again.\s
+ Recommended to leave as is.""") * 1000L;
+ this.shouldNotify = config.getBoolean("gameplay.villager-leveling.notify-player", true,
+ "Tell players to wait when a villager is leveling up.");
+ }
+
+ @Override
+ public void enable() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("gameplay.villager-leveling.enable", true);
+ }
+
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+ private void onTradeScreenClose(InventoryCloseEvent event) {
+ if (
+ event.getInventory().getType().equals(InventoryType.MERCHANT)
+ && event.getInventory().getHolder() instanceof Villager villager
+ ) {
+ WrappedVillager wVillager = villagerCache.getOrAdd(villager);
+ if (!wVillager.isOptimized()) return;
+
+ if (wVillager.canLevelUp(cooldown)) {
+ if (wVillager.calculateLevel() > villager.getVillagerLevel()) {
+ villager.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 120, 120, false, false));
+ villager.setAware(true);
+
+ plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
+ villager.setAware(false);
+ wVillager.saveLastLevelUp();
+ }, 100L);
+ }
+ } else {
+ if (shouldNotify) {
+ Player player = (Player) event.getPlayer();
+ final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
+ .matchLiteral("%time%")
+ .replacement(CommonUtil.formatTime(wVillager.getLevelCooldownMillis(cooldown)))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).villager_leveling_up.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/mechanics/RestockTrades.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/mechanics/RestockTrades.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/mechanics/RestockTrades.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/mechanics/RestockTrades.java
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByBlock.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByBlock.java
new file mode 100644
index 0000000..ee0d34c
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByBlock.java
@@ -0,0 +1,201 @@
+package me.xginko.villageroptimizer.modules.optimizations;
+
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.WrappedVillager;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.enums.OptimizationType;
+import me.xginko.villageroptimizer.enums.Permissions;
+import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
+import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import me.xginko.villageroptimizer.utils.CommonUtil;
+import me.xginko.villageroptimizer.utils.LogUtil;
+import net.kyori.adventure.text.TextReplacementConfig;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+
+import java.util.HashSet;
+import java.util.List;
+
+public class OptimizeByBlock implements VillagerOptimizerModule, Listener {
+
+ private final VillagerCache villagerCache;
+ private final HashSet blocks_that_disable = new HashSet<>(4);
+ private final long cooldown;
+ private final double search_radius;
+ private final boolean onlyWhileSneaking, shouldNotifyPlayer, shouldLog;
+
+ public OptimizeByBlock() {
+ shouldEnable();
+ this.villagerCache = VillagerOptimizer.getCache();
+ Config config = VillagerOptimizer.getConfiguration();
+ config.addComment("optimization-methods.block-optimization.enable", """
+ When enabled, the closest villager standing near a configured block being placed will be optimized.\s
+ If a configured block is broken nearby, the closest villager will become unoptimized again.""");
+ config.getList("optimization-methods.block-optimization.materials", List.of(
+ "LAPIS_BLOCK", "GLOWSTONE", "IRON_BLOCK"
+ ), "Values here need to be valid bukkit Material enums for your server version."
+ ).forEach(configuredMaterial -> {
+ try {
+ Material disableBlock = Material.valueOf(configuredMaterial);
+ this.blocks_that_disable.add(disableBlock);
+ } catch (IllegalArgumentException e) {
+ LogUtil.materialNotRecognized("block-optimization", configuredMaterial);
+ }
+ });
+ this.cooldown = config.getInt("optimization-methods.block-optimization.optimize-cooldown-seconds", 600, """
+ Cooldown in seconds until a villager can be optimized again by using specific blocks. \s
+ Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
+ this.search_radius = config.getDouble("optimization-methods.block-optimization.search-radius-in-blocks", 2.0, """
+ The radius in blocks a villager can be away from the player when he places an optimize block.\s
+ The closest unoptimized villager to the player will be optimized.""") / 2;
+ this.onlyWhileSneaking = config.getBoolean("optimization-methods.block-optimization.only-when-sneaking", true,
+ "Only optimize/unoptimize by workstation when player is sneaking during place or break.");
+ this.shouldNotifyPlayer = config.getBoolean("optimization-methods.block-optimization.notify-player", true,
+ "Sends players a message when they successfully optimized or unoptimized a villager.");
+ this.shouldLog = config.getBoolean("optimization-methods.block-optimization.log", false);
+ }
+
+ @Override
+ public void enable() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.block-optimization.enable", false);
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onBlockPlace(BlockPlaceEvent event) {
+ Block placed = event.getBlock();
+ if (!blocks_that_disable.contains(placed.getType())) return;
+ Player player = event.getPlayer();
+ if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
+ if (onlyWhileSneaking && !player.isSneaking()) return;
+
+ final Location blockLoc = placed.getLocation();
+ WrappedVillager closestOptimizableVillager = null;
+ double closestDistance = Double.MAX_VALUE;
+
+ for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
+ if (!entity.getType().equals(EntityType.VILLAGER)) continue;
+ Villager villager = (Villager) entity;
+ final Villager.Profession profession = villager.getProfession();
+ if (profession.equals(Villager.Profession.NONE) || profession.equals(Villager.Profession.NITWIT)) continue;
+
+ WrappedVillager wVillager = villagerCache.getOrAdd(villager);
+ final double distance = entity.getLocation().distance(blockLoc);
+
+ if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
+ closestOptimizableVillager = wVillager;
+ closestDistance = distance;
+ }
+ }
+
+ if (closestOptimizableVillager == null) return;
+
+ if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.BLOCK_COOLDOWN.get())) {
+ VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.BLOCK, event.isAsynchronous());
+ VillagerOptimizer.callEvent(optimizeEvent);
+ if (optimizeEvent.isCancelled()) return;
+
+ closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
+ closestOptimizableVillager.saveOptimizeTime();
+
+ if (shouldNotifyPlayer) {
+ final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
+ .matchLiteral("%vil_profession%")
+ .replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
+ .build();
+ final TextReplacementConfig placedMaterial = TextReplacementConfig.builder()
+ .matchLiteral("%blocktype%")
+ .replacement(placed.getType().toString().toLowerCase())
+ .build();
+ VillagerOptimizer.getLang(player.locale()).block_optimize_success.forEach(line -> player.sendMessage(line
+ .replaceText(vilProfession)
+ .replaceText(placedMaterial)
+ ));
+ }
+ if (shouldLog)
+ VillagerOptimizer.getLog().info("Villager was optimized by block at "+closestOptimizableVillager.villager().getLocation());
+ } else {
+ if (shouldNotifyPlayer) {
+ final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
+ .matchLiteral("%time%")
+ .replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).block_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onBlockBreak(BlockBreakEvent event) {
+ Block broken = event.getBlock();
+ if (!blocks_that_disable.contains(broken.getType())) return;
+ Player player = event.getPlayer();
+ if (!player.hasPermission(Permissions.Optimize.BLOCK.get())) return;
+ if (onlyWhileSneaking && !player.isSneaking()) return;
+
+ final Location blockLoc = broken.getLocation();
+ WrappedVillager closestOptimizedVillager = null;
+ double closestDistance = Double.MAX_VALUE;
+
+ for (Entity entity : blockLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
+ if (!entity.getType().equals(EntityType.VILLAGER)) continue;
+ Villager villager = (Villager) entity;
+
+ WrappedVillager wVillager = villagerCache.getOrAdd(villager);
+ final double distance = entity.getLocation().distance(blockLoc);
+
+ if (distance < closestDistance && wVillager.isOptimized()) {
+ closestOptimizedVillager = wVillager;
+ closestDistance = distance;
+ }
+ }
+
+ if (closestOptimizedVillager == null) return;
+
+ VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, event.isAsynchronous());
+ VillagerOptimizer.callEvent(unOptimizeEvent);
+ if (unOptimizeEvent.isCancelled()) return;
+
+ closestOptimizedVillager.setOptimization(OptimizationType.NONE);
+
+ if (shouldNotifyPlayer) {
+ final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
+ .matchLiteral("%vil_profession%")
+ .replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
+ .build();
+ final TextReplacementConfig brokenMaterial = TextReplacementConfig.builder()
+ .matchLiteral("%blocktype%")
+ .replacement(broken.getType().toString().toLowerCase())
+ .build();
+ VillagerOptimizer.getLang(player.locale()).block_unoptimize_success.forEach(line -> player.sendMessage(line
+ .replaceText(vilProfession)
+ .replaceText(brokenMaterial)
+ ));
+ }
+ if (shouldLog)
+ VillagerOptimizer.getLog().info("Villager unoptimized because nearby optimization block broken at: "+closestOptimizedVillager.villager().getLocation());
+ }
+}
\ No newline at end of file
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByNametag.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByNametag.java
new file mode 100644
index 0000000..6c46a77
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByNametag.java
@@ -0,0 +1,136 @@
+package me.xginko.villageroptimizer.modules.optimizations;
+
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.WrappedVillager;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.enums.OptimizationType;
+import me.xginko.villageroptimizer.enums.Permissions;
+import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
+import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import me.xginko.villageroptimizer.utils.CommonUtil;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextReplacementConfig;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.bukkit.Material;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.HashSet;
+import java.util.List;
+
+public class OptimizeByNametag implements VillagerOptimizerModule, Listener {
+
+ private final VillagerCache villagerCache;
+ private final HashSet nametags = new HashSet<>(4);
+ private final long cooldown;
+ private final boolean consumeNametag, shouldNotifyPlayer, shouldLog;
+
+ public OptimizeByNametag() {
+ shouldEnable();
+ this.villagerCache = VillagerOptimizer.getCache();
+ Config config = VillagerOptimizer.getConfiguration();
+ config.addComment("optimization-methods.nametag-optimization.enable", """
+ Enable optimization by naming villagers to one of the names configured below.\s
+ Nametag optimized villagers will be unoptimized again when they are renamed to something else.""");
+ this.nametags.addAll(config.getList("optimization-methods.nametag-optimization.names", List.of("Optimize", "DisableAI"),
+ "Names are case insensitive, capital letters won't matter.").stream().map(String::toLowerCase).toList());
+ this.consumeNametag = config.getBoolean("optimization-methods.nametag-optimization.nametags-get-consumed", true,
+ "Enable or disable consumption of the used nametag item.");
+ this.cooldown = config.getInt("optimization-methods.nametag-optimization.optimize-cooldown-seconds", 600, """
+ Cooldown in seconds until a villager can be optimized again using a nametag.\s
+ Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
+ this.shouldNotifyPlayer = config.getBoolean("optimization-methods.nametag-optimization.notify-player", true,
+ "Sends players a message when they successfully optimized a villager.");
+ this.shouldLog = config.getBoolean("optimization-methods.nametag-optimization.log", false);
+ }
+
+ @Override
+ public void enable() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.nametag-optimization.enable", true);
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
+ if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
+ Player player = event.getPlayer();
+ if (!player.hasPermission(Permissions.Optimize.NAMETAG.get())) return;
+
+ ItemStack usedItem = player.getInventory().getItem(event.getHand());
+ if (!usedItem.getType().equals(Material.NAME_TAG)) return;
+ ItemMeta meta = usedItem.getItemMeta();
+ if (!meta.hasDisplayName()) return;
+
+ // Get component name first, so we can manually name the villager when canceling the event to avoid item consumption.
+ Component newVillagerName = meta.displayName();
+ assert newVillagerName != null; // Legitimate since we checked for hasDisplayName()
+
+
+ final String name = PlainTextComponentSerializer.plainText().serialize(newVillagerName);
+ Villager villager = (Villager) event.getRightClicked();
+ WrappedVillager wVillager = villagerCache.getOrAdd(villager);
+
+ if (nametags.contains(name.toLowerCase())) {
+ if (wVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.NAMETAG_COOLDOWN.get())) {
+ if (!consumeNametag) {
+ event.setCancelled(true);
+ villager.customName(newVillagerName);
+ }
+
+ VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.NAMETAG, event.isAsynchronous());
+ VillagerOptimizer.callEvent(optimizeEvent);
+ if (optimizeEvent.isCancelled()) return;
+
+ wVillager.setOptimization(optimizeEvent.getOptimizationType());
+ wVillager.saveOptimizeTime();
+
+ if (shouldNotifyPlayer)
+ VillagerOptimizer.getLang(player.locale()).nametag_optimize_success.forEach(player::sendMessage);
+ if (shouldLog)
+ VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using nametag: '" + name + "'");
+ } else {
+ event.setCancelled(true);
+ if (shouldNotifyPlayer) {
+ final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
+ .matchLiteral("%time%")
+ .replacement(CommonUtil.formatTime(wVillager.getOptimizeCooldownMillis(cooldown)))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
+ }
+ }
+ } else {
+ if (wVillager.isOptimized()) {
+ VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager, event.isAsynchronous());
+ VillagerOptimizer.callEvent(unOptimizeEvent);
+ if (unOptimizeEvent.isCancelled()) return;
+
+ wVillager.setOptimization(OptimizationType.NONE);
+
+ if (shouldNotifyPlayer)
+ VillagerOptimizer.getLang(player.locale()).nametag_unoptimize_success.forEach(player::sendMessage);
+ if (shouldLog)
+ VillagerOptimizer.getLog().info(event.getPlayer().getName() + " disabled optimizations for a villager using nametag: '" + name + "'");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByWorkstation.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByWorkstation.java
new file mode 100644
index 0000000..671ea47
--- /dev/null
+++ b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByWorkstation.java
@@ -0,0 +1,208 @@
+package me.xginko.villageroptimizer.modules.optimizations;
+
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.WrappedVillager;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.enums.OptimizationType;
+import me.xginko.villageroptimizer.enums.Permissions;
+import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
+import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import me.xginko.villageroptimizer.utils.CommonUtil;
+import net.kyori.adventure.text.TextReplacementConfig;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+
+public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
+
+ private final VillagerCache villagerCache;
+ private final long cooldown;
+ private final double search_radius;
+ private final boolean onlyWhileSneaking, shouldLog, shouldNotifyPlayer;
+
+ public OptimizeByWorkstation() {
+ shouldEnable();
+ this.villagerCache = VillagerOptimizer.getCache();
+ Config config = VillagerOptimizer.getConfiguration();
+ config.addComment("optimization-methods.workstation-optimization.enable", """
+ When enabled, the closest villager near a matching workstation being placed will be optimized.\s
+ If a nearby matching workstation is broken, the villager will become unoptimized again.""");
+ this.search_radius = config.getDouble("optimization-methods.workstation-optimization.search-radius-in-blocks", 2.0, """
+ The radius in blocks a villager can be away from the player when he places a workstation.\s
+ The closest unoptimized villager to the player will be optimized.""") / 2;
+ this.cooldown = config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600, """
+ Cooldown in seconds until a villager can be optimized again using a workstation.\s
+ Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
+ this.onlyWhileSneaking = config.getBoolean("optimization-methods.workstation-optimization.only-when-sneaking", true,
+ "Only optimize/unoptimize by workstation when player is sneaking during place or break");
+ this.shouldNotifyPlayer = config.getBoolean("optimization-methods.workstation-optimization.notify-player", true,
+ "Sends players a message when they successfully optimized a villager.");
+ this.shouldLog = config.getBoolean("optimization-methods.workstation-optimization.log", false);
+ }
+
+ @Override
+ public void enable() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.workstation-optimization.enable", false);
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onBlockPlace(BlockPlaceEvent event) {
+ Block placed = event.getBlock();
+ Villager.Profession workstationProfession = getWorkstationProfession(placed.getType());
+ if (workstationProfession.equals(Villager.Profession.NONE)) return;
+ Player player = event.getPlayer();
+ if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
+ if (onlyWhileSneaking && !player.isSneaking()) return;
+
+ final Location workstationLoc = placed.getLocation();
+ WrappedVillager closestOptimizableVillager = null;
+ double closestDistance = Double.MAX_VALUE;
+
+ for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
+ if (!entity.getType().equals(EntityType.VILLAGER)) continue;
+ Villager villager = (Villager) entity;
+ if (!villager.getProfession().equals(workstationProfession)) continue;
+
+ WrappedVillager wVillager = villagerCache.getOrAdd(villager);
+ final double distance = entity.getLocation().distance(workstationLoc);
+
+ if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
+ closestOptimizableVillager = wVillager;
+ closestDistance = distance;
+ }
+ }
+
+ if (closestOptimizableVillager == null) return;
+
+ if (closestOptimizableVillager.canOptimize(cooldown) || player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
+ VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(closestOptimizableVillager, OptimizationType.WORKSTATION, event.isAsynchronous());
+ VillagerOptimizer.callEvent(optimizeEvent);
+ if (optimizeEvent.isCancelled()) return;
+
+ closestOptimizableVillager.setOptimization(optimizeEvent.getOptimizationType());
+ closestOptimizableVillager.saveOptimizeTime();
+
+ if (shouldNotifyPlayer) {
+ final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
+ .matchLiteral("%vil_profession%")
+ .replacement(closestOptimizableVillager.villager().getProfession().toString().toLowerCase())
+ .build();
+ final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
+ .matchLiteral("%workstation%")
+ .replacement(placed.getType().toString().toLowerCase())
+ .build();
+ VillagerOptimizer.getLang(player.locale()).workstation_optimize_success.forEach(line -> player.sendMessage(line
+ .replaceText(vilProfession)
+ .replaceText(placedWorkstation)
+ ));
+ }
+ if (shouldLog)
+ VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using workstation: '" + placed.getType().toString().toLowerCase() + "'");
+ } else {
+ if (shouldNotifyPlayer) {
+ final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
+ .matchLiteral("%time%")
+ .replacement(CommonUtil.formatTime(closestOptimizableVillager.getOptimizeCooldownMillis(cooldown)))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line
+ .replaceText(timeLeft)
+ ));
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onBlockBreak(BlockBreakEvent event) {
+ Block broken = event.getBlock();
+ Villager.Profession workstationProfession = getWorkstationProfession(broken.getType());
+ if (workstationProfession.equals(Villager.Profession.NONE)) return;
+ Player player = event.getPlayer();
+ if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
+ if (onlyWhileSneaking && !player.isSneaking()) return;
+
+ final Location workstationLoc = broken.getLocation();
+ WrappedVillager closestOptimizedVillager = null;
+ double closestDistance = Double.MAX_VALUE;
+
+ for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
+ if (!entity.getType().equals(EntityType.VILLAGER)) continue;
+ Villager villager = (Villager) entity;
+ if (!villager.getProfession().equals(workstationProfession)) continue;
+
+ WrappedVillager wVillager = villagerCache.getOrAdd(villager);
+ final double distance = entity.getLocation().distance(workstationLoc);
+
+ if (distance < closestDistance && wVillager.canOptimize(cooldown)) {
+ closestOptimizedVillager = wVillager;
+ closestDistance = distance;
+ }
+ }
+
+ if (closestOptimizedVillager == null) return;
+
+ VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(closestOptimizedVillager, event.isAsynchronous());
+ VillagerOptimizer.callEvent(unOptimizeEvent);
+ if (unOptimizeEvent.isCancelled()) return;
+
+ closestOptimizedVillager.setOptimization(OptimizationType.NONE);
+
+ if (shouldNotifyPlayer) {
+ final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
+ .matchLiteral("%vil_profession%")
+ .replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
+ .build();
+ final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
+ .matchLiteral("%workstation%")
+ .replacement(broken.getType().toString().toLowerCase())
+ .build();
+ VillagerOptimizer.getLang(player.locale()).workstation_unoptimize_success.forEach(line -> player.sendMessage(line
+ .replaceText(vilProfession)
+ .replaceText(brokenWorkstation)
+ ));
+ }
+ if (shouldLog)
+ VillagerOptimizer.getLog().info(player.getName() + " unoptimized a villager by breaking workstation: '" + broken.getType().toString().toLowerCase() + "'");
+ }
+
+ private Villager.Profession getWorkstationProfession(final Material workstation) {
+ return switch (workstation) {
+ case BARREL -> Villager.Profession.FISHERMAN;
+ case CARTOGRAPHY_TABLE -> Villager.Profession.CARTOGRAPHER;
+ case SMOKER -> Villager.Profession.BUTCHER;
+ case SMITHING_TABLE -> Villager.Profession.TOOLSMITH;
+ case GRINDSTONE -> Villager.Profession.WEAPONSMITH;
+ case BLAST_FURNACE -> Villager.Profession.ARMORER;
+ case CAULDRON -> Villager.Profession.LEATHERWORKER;
+ case BREWING_STAND -> Villager.Profession.CLERIC;
+ case COMPOSTER -> Villager.Profession.FARMER;
+ case FLETCHING_TABLE -> Villager.Profession.FLETCHER;
+ case LOOM -> Villager.Profession.SHEPHERD;
+ case LECTERN -> Villager.Profession.LIBRARIAN;
+ case STONECUTTER -> Villager.Profession.MASON;
+ default -> Villager.Profession.NONE;
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java
diff --git a/src/main/java/me/xginko/villageroptimizer/utils/LogUtil.java b/VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/utils/LogUtil.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/utils/LogUtil.java
rename to VillagerOptimizer-1.16.5/src/main/java/me/xginko/villageroptimizer/utils/LogUtil.java
diff --git a/src/main/resources/lang/en_us.yml b/VillagerOptimizer-1.16.5/src/main/resources/lang/en_us.yml
similarity index 100%
rename from src/main/resources/lang/en_us.yml
rename to VillagerOptimizer-1.16.5/src/main/resources/lang/en_us.yml
diff --git a/src/main/resources/plugin.yml b/VillagerOptimizer-1.16.5/src/main/resources/plugin.yml
similarity index 100%
rename from src/main/resources/plugin.yml
rename to VillagerOptimizer-1.16.5/src/main/resources/plugin.yml
diff --git a/VillagerOptimizer-1.20.2/pom.xml b/VillagerOptimizer-1.20.2/pom.xml
new file mode 100644
index 0000000..fa2ef2a
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/pom.xml
@@ -0,0 +1,87 @@
+
+
+ 4.0.0
+
+
+ me.xginko.VillagerOptimizer
+ VillagerOptimizer
+ 1.0.0
+
+
+ 1.20.2
+ ${project.parent.artifactId}-${project.parent.version}--${project.artifactId}
+ jar
+
+
+ 17
+ UTF-8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ org.bstats
+ me.xginko.villageroptimizer.bstats
+
+
+ false
+
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+
+
+ papermc-repo
+ https://repo.papermc.io/repository/maven-public/
+
+
+ sonatype
+ https://oss.sonatype.org/content/groups/public/
+
+
+ configmaster-repo
+ https://ci.pluginwiki.us/plugin/repository/everything/
+
+
+
+
+
+ io.papermc.paper
+ paper-api
+ 1.20.2-R0.1-SNAPSHOT
+ provided
+
+
+
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/VillagerCache.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/VillagerCache.java
new file mode 100644
index 0000000..5eb4423
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/VillagerCache.java
@@ -0,0 +1,56 @@
+package me.xginko.villageroptimizer;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Villager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.Duration;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentMap;
+
+public final class VillagerCache {
+
+ private final @NotNull Cache villagerCache;
+
+ VillagerCache(long expireAfterWriteSeconds) {
+ this.villagerCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(expireAfterWriteSeconds)).build();
+ }
+
+ public @NotNull ConcurrentMap cacheMap() {
+ return this.villagerCache.asMap();
+ }
+
+ public @Nullable WrappedVillager get(@NotNull UUID uuid) {
+ WrappedVillager wrappedVillager = villagerCache.getIfPresent(uuid);
+ return wrappedVillager == null && Bukkit.getEntity(uuid) instanceof Villager villager ? add(villager) : wrappedVillager;
+ }
+
+ public @NotNull WrappedVillager getOrAdd(@NotNull Villager villager) {
+ WrappedVillager wrappedVillager = villagerCache.getIfPresent(villager.getUniqueId());
+ return wrappedVillager == null ? add(new WrappedVillager(villager)) : add(wrappedVillager);
+ }
+
+ public @NotNull WrappedVillager add(@NotNull WrappedVillager villager) {
+ villagerCache.put(villager.villager().getUniqueId(), villager);
+ return villager;
+ }
+
+ public @NotNull WrappedVillager add(@NotNull Villager villager) {
+ return add(new WrappedVillager(villager));
+ }
+
+ public boolean contains(@NotNull UUID uuid) {
+ return villagerCache.getIfPresent(uuid) != null;
+ }
+
+ public boolean contains(@NotNull WrappedVillager villager) {
+ return villagerCache.getIfPresent(villager.villager().getUniqueId()) != null;
+ }
+
+ public boolean contains(@NotNull Villager villager) {
+ return villagerCache.getIfPresent(villager.getUniqueId()) != null;
+ }
+}
\ No newline at end of file
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/VillagerOptimizer.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/VillagerOptimizer.java
new file mode 100644
index 0000000..07da4d8
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/VillagerOptimizer.java
@@ -0,0 +1,175 @@
+package me.xginko.villageroptimizer;
+
+import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.config.LanguageCache;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.Style;
+import net.kyori.adventure.text.format.TextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import org.bukkit.NamespacedKey;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.ConsoleCommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.jar.JarFile;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class VillagerOptimizer extends JavaPlugin {
+
+ private static VillagerOptimizer instance;
+ private static VillagerCache villagerCache;
+ private static HashMap languageCacheMap;
+ private static Config config;
+ private static Logger logger;
+
+ public final static Style plugin_style = Style.style(TextColor.color(102,255,230), TextDecoration.BOLD);
+
+ @Override
+ public void onEnable() {
+ instance = this;
+ logger = getLogger();
+ ConsoleCommandSender console = getServer().getConsoleSender();
+ console.sendMessage(Component.text("â•────────────────────────────────────────────────────────────╮").style(plugin_style));
+ console.sendMessage(Component.text("│ │").style(plugin_style));
+ console.sendMessage(Component.text("│ │").style(plugin_style));
+ console.sendMessage(Component.text("│ _ __ _ __ __ │").style(plugin_style));
+ console.sendMessage(Component.text("│ | | / /(_)/ // /___ _ ___ _ ___ ____ │").style(plugin_style));
+ console.sendMessage(Component.text("│ | |/ // // // // _ `// _ `// -_)/ __/ │").style(plugin_style));
+ console.sendMessage(Component.text("│ |___//_//_//_/ \\_,_/ \\_, / \\__//_/ │").style(plugin_style));
+ console.sendMessage(Component.text("│ ____ __ _ /___/_ │").style(plugin_style));
+ console.sendMessage(Component.text("│ / __ \\ ___ / /_ (_)__ _ (_)___ ___ ____ │").style(plugin_style));
+ console.sendMessage(Component.text("│ / /_/ // _ \\/ __// // ' \\ / //_ // -_)/ __/ │").style(plugin_style));
+ console.sendMessage(Component.text("│ \\____// .__/\\__//_//_/_/_//_/ /__/\\__//_/ │").style(plugin_style));
+ console.sendMessage(Component.text("│ /_/ by xGinko │").style(plugin_style));
+ console.sendMessage(Component.text("│ │").style(plugin_style));
+ console.sendMessage(Component.text("│ │").style(plugin_style));
+ console.sendMessage(Component.text("│ ").style(plugin_style).append(Component.text("https://github.com/xGinko/VillagerOptimizer").color(NamedTextColor.GRAY)).append(Component.text(" │").style(plugin_style)));
+ console.sendMessage(Component.text("│ │").style(plugin_style));
+ console.sendMessage(Component.text("│ │").style(plugin_style));
+ console.sendMessage(Component.text("│ ").style(plugin_style).append(Component.text(" ➤ Loading Translations...").style(plugin_style)).append(Component.text(" │").style(plugin_style)));
+ reloadLang(true);
+ console.sendMessage(Component.text("│ ").style(plugin_style).append(Component.text(" ➤ Loading Config...").style(plugin_style)).append(Component.text(" │").style(plugin_style)));
+ reloadConfiguration();
+ console.sendMessage(Component.text("│ ").style(plugin_style).append(Component.text(" ✓ Done.").color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD)).append(Component.text(" │").style(plugin_style)));
+ console.sendMessage(Component.text("│ │").style(plugin_style));
+ console.sendMessage(Component.text("│ │").style(plugin_style));
+ console.sendMessage(Component.text("╰────────────────────────────────────────────────────────────╯").style(plugin_style));
+ }
+
+ public static VillagerOptimizer getInstance() {
+ return instance;
+ }
+ public static NamespacedKey getKey(String key) {
+ return new NamespacedKey(instance, key);
+ }
+ public static Config getConfiguration() {
+ return config;
+ }
+ public static VillagerCache getCache() {
+ return villagerCache;
+ }
+ public static Logger getLog() {
+ return logger;
+ }
+ public static LanguageCache getLang(Locale locale) {
+ return getLang(locale.toString().toLowerCase());
+ }
+ public static LanguageCache getLang(CommandSender commandSender) {
+ return commandSender instanceof Player player ? getLang(player.locale()) : getLang(config.default_lang);
+ }
+ public static LanguageCache getLang(String lang) {
+ return config.auto_lang ? languageCacheMap.getOrDefault(lang.replace("-", "_"), languageCacheMap.get(config.default_lang.toString().toLowerCase())) : languageCacheMap.get(config.default_lang.toString().toLowerCase());
+ }
+ public static void callEvent(Event event) {
+ instance.getServer().getPluginManager().callEvent(event);
+ }
+
+ public void reloadPlugin() {
+ reloadLang(false);
+ reloadConfiguration();
+ }
+
+ private void reloadConfiguration() {
+ try {
+ config = new Config();
+ villagerCache = new VillagerCache(config.cache_keep_time_seconds);
+ VillagerOptimizerCommand.reloadCommands();
+ VillagerOptimizerModule.reloadModules();
+ config.saveConfig();
+ } catch (Exception e) {
+ logger.severe("Error loading config! - " + e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ }
+
+ private void reloadLang(boolean startup) {
+ languageCacheMap = new HashMap<>();
+ ConsoleCommandSender console = getServer().getConsoleSender();
+ try {
+ File langDirectory = new File(getDataFolder() + "/lang");
+ Files.createDirectories(langDirectory.toPath());
+ for (String fileName : getDefaultLanguageFiles()) {
+ String localeString = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.lastIndexOf('.'));
+ if (startup) console.sendMessage(
+ Component.text("│ ").style(plugin_style)
+ .append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
+ .append(Component.text(" │").style(plugin_style)));
+ else logger.info(String.format("Found language file for %s", localeString));
+ LanguageCache langCache = new LanguageCache(localeString);
+ languageCacheMap.put(localeString, langCache);
+ }
+ Pattern langPattern = Pattern.compile("([a-z]{1,3}_[a-z]{1,3})(\\.yml)", Pattern.CASE_INSENSITIVE);
+ for (File langFile : langDirectory.listFiles()) {
+ Matcher langMatcher = langPattern.matcher(langFile.getName());
+ if (langMatcher.find()) {
+ String localeString = langMatcher.group(1).toLowerCase();
+ if (!languageCacheMap.containsKey(localeString)) { // make sure it wasn't a default file that we already loaded
+ if (startup) console.sendMessage(
+ Component.text("│ ").style(plugin_style)
+ .append(Component.text(" "+localeString).color(NamedTextColor.WHITE).decorate(TextDecoration.BOLD))
+ .append(Component.text(" │").style(plugin_style)));
+ else logger.info(String.format("Found language file for %s", localeString));
+ LanguageCache langCache = new LanguageCache(localeString);
+ languageCacheMap.put(localeString, langCache);
+ }
+ }
+ }
+ } catch (Exception e) {
+ if (startup) console.sendMessage(
+ Component.text("│ ").style(plugin_style)
+ .append(Component.text("LANG ERROR").color(NamedTextColor.RED).decorate(TextDecoration.BOLD))
+ .append(Component.text(" │").style(plugin_style)));
+ else logger.severe("Error loading language files! Language files will not reload to avoid errors, make sure to correct this before restarting the server!");
+ e.printStackTrace();
+ }
+ }
+
+ private Set getDefaultLanguageFiles() {
+ Set languageFiles = new HashSet<>();
+ try (JarFile jarFile = new JarFile(this.getFile())) {
+ jarFile.entries().asIterator().forEachRemaining(jarFileEntry -> {
+ final String path = jarFileEntry.getName();
+ if (path.startsWith("lang/") && path.endsWith(".yml"))
+ languageFiles.add(path);
+ });
+ } catch (IOException e) {
+ logger.severe("Error while getting default language file names! - " + e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ return languageFiles;
+ }
+}
diff --git a/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/WrappedVillager.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/SubCommand.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/SubCommand.java
new file mode 100644
index 0000000..4ce3d53
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/SubCommand.java
@@ -0,0 +1,11 @@
+package me.xginko.villageroptimizer.commands;
+
+import net.kyori.adventure.text.TextComponent;
+import org.bukkit.command.CommandSender;
+
+public abstract class SubCommand {
+ public abstract String getLabel();
+ public abstract TextComponent getDescription();
+ public abstract TextComponent getSyntax();
+ public abstract void perform(CommandSender sender, String[] args);
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/VillagerOptimizerCommand.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/VillagerOptimizerCommand.java
new file mode 100644
index 0000000..acfbcd8
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/VillagerOptimizerCommand.java
@@ -0,0 +1,35 @@
+package me.xginko.villageroptimizer.commands;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.commands.optimizevillagers.OptVillagersRadius;
+import me.xginko.villageroptimizer.commands.unoptimizevillagers.UnOptVillagersRadius;
+import me.xginko.villageroptimizer.commands.villageroptimizer.VillagerOptimizerCmd;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandMap;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashSet;
+
+public interface VillagerOptimizerCommand extends CommandExecutor {
+
+ String label();
+
+ HashSet commands = new HashSet<>();
+ static void reloadCommands() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ CommandMap commandMap = plugin.getServer().getCommandMap();
+ commands.forEach(command -> plugin.getCommand(command.label()).unregister(commandMap));
+ commands.clear();
+
+ commands.add(new VillagerOptimizerCmd());
+ commands.add(new OptVillagersRadius());
+ commands.add(new UnOptVillagersRadius());
+
+ commands.forEach(command -> plugin.getCommand(command.label()).setExecutor(command));
+ }
+
+ @Override
+ boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args);
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/optimizevillagers/OptVillagersRadius.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/optimizevillagers/OptVillagersRadius.java
new file mode 100644
index 0000000..5e734bd
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/optimizevillagers/OptVillagersRadius.java
@@ -0,0 +1,142 @@
+package me.xginko.villageroptimizer.commands.optimizevillagers;
+
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.WrappedVillager;
+import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.enums.OptimizationType;
+import me.xginko.villageroptimizer.enums.Permissions;
+import me.xginko.villageroptimizer.events.VillagerOptimizeEvent;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextReplacementConfig;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Villager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+
+public class OptVillagersRadius implements VillagerOptimizerCommand, TabCompleter {
+
+ private final List tabCompletes = List.of("5", "10", "25", "50");
+ private final long cooldown;
+ private final int maxRadius;
+
+ public OptVillagersRadius() {
+ Config config = VillagerOptimizer.getConfiguration();
+ this.maxRadius = config.getInt("optimization-methods.commands.optimizevillagers.max-block-radius", 100);
+ this.cooldown = config.getInt("optimization-methods.commands.optimizevillagers.cooldown-seconds", 600, """
+ Cooldown in seconds until a villager can be optimized again using the command.\s
+ Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.""") * 1000L;
+ }
+
+ @Override
+ public String label() {
+ return "optimizevillagers";
+ }
+
+ @Override
+ public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ return args.length == 1 ? tabCompletes : Collections.emptyList();
+ }
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Component.text("This command can only be executed by a player.")
+ .color(NamedTextColor.RED).decorate(TextDecoration.BOLD));
+ return true;
+ }
+
+ if (sender.hasPermission(Permissions.Commands.OPTIMIZE_RADIUS.get())) {
+ if (args.length != 1) {
+ VillagerOptimizer.getLang(player.locale()).command_specify_radius.forEach(player::sendMessage);
+ return true;
+ }
+
+ try {
+ int specifiedRadius = Integer.parseInt(args[0]);
+
+ if (specifiedRadius > maxRadius) {
+ final TextReplacementConfig limit = TextReplacementConfig.builder()
+ .matchLiteral("%distance%")
+ .replacement(Integer.toString(maxRadius))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed.forEach(line -> player.sendMessage(line.replaceText(limit)));
+ return true;
+ }
+
+ VillagerCache villagerCache = VillagerOptimizer.getCache();
+ int successCount = 0;
+ int failCount = 0;
+
+ for (Entity entity : player.getNearbyEntities(specifiedRadius, specifiedRadius, specifiedRadius)) {
+ if (!entity.getType().equals(EntityType.VILLAGER)) continue;
+ Villager villager = (Villager) entity;
+ Villager.Profession profession = villager.getProfession();
+ if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
+
+ WrappedVillager wVillager = villagerCache.getOrAdd(villager);
+
+ if (wVillager.canOptimize(cooldown)) {
+ VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(wVillager, OptimizationType.COMMAND);
+ VillagerOptimizer.callEvent(optimizeEvent);
+ if (!optimizeEvent.isCancelled()) {
+ wVillager.setOptimization(optimizeEvent.getOptimizationType());
+ wVillager.saveOptimizeTime();
+ successCount++;
+ }
+ } else {
+ failCount++;
+ }
+ }
+
+ if (successCount <= 0 && failCount <= 0) {
+ final TextReplacementConfig radius = TextReplacementConfig.builder()
+ .matchLiteral("%radius%")
+ .replacement(Integer.toString(specifiedRadius))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby.forEach(line -> player.sendMessage(line.replaceText(radius)));
+ return true;
+ }
+
+ if (successCount > 0) {
+ final TextReplacementConfig success_amount = TextReplacementConfig.builder()
+ .matchLiteral("%amount%")
+ .replacement(Integer.toString(successCount))
+ .build();
+ final TextReplacementConfig radius = TextReplacementConfig.builder()
+ .matchLiteral("%radius%")
+ .replacement(Integer.toString(specifiedRadius))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).command_optimize_success.forEach(line -> player.sendMessage(line
+ .replaceText(success_amount)
+ .replaceText(radius)
+ ));
+ }
+ if (failCount > 0) {
+ final TextReplacementConfig alreadyOptimized = TextReplacementConfig.builder()
+ .matchLiteral("%amount%")
+ .replacement(Integer.toString(failCount))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).command_optimize_fail.forEach(line -> player.sendMessage(line.replaceText(alreadyOptimized)));
+ }
+ } catch (NumberFormatException e) {
+ VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
+ }
+ } else {
+ sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/unoptimizevillagers/UnOptVillagersRadius.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/unoptimizevillagers/UnOptVillagersRadius.java
new file mode 100644
index 0000000..229790a
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/unoptimizevillagers/UnOptVillagersRadius.java
@@ -0,0 +1,122 @@
+package me.xginko.villageroptimizer.commands.unoptimizevillagers;
+
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.WrappedVillager;
+import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
+import me.xginko.villageroptimizer.enums.OptimizationType;
+import me.xginko.villageroptimizer.enums.Permissions;
+import me.xginko.villageroptimizer.events.VillagerUnoptimizeEvent;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextReplacementConfig;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Villager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+
+public class UnOptVillagersRadius implements VillagerOptimizerCommand, TabCompleter {
+
+ private final List tabCompletes = List.of("5", "10", "25", "50");
+ private final int maxRadius;
+
+ public UnOptVillagersRadius() {
+ this.maxRadius = VillagerOptimizer.getConfiguration().getInt("optimization-methods.commands.unoptimizevillagers.max-block-radius", 100);
+ }
+
+ @Override
+ public String label() {
+ return "unoptimizevillagers";
+ }
+
+ @Override
+ public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ return args.length == 1 ? tabCompletes : Collections.emptyList();
+ }
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Component.text("This command can only be executed by a player.")
+ .color(NamedTextColor.RED).decorate(TextDecoration.BOLD));
+ return true;
+ }
+
+ if (sender.hasPermission(Permissions.Commands.UNOPTIMIZE_RADIUS.get())) {
+ if (args.length != 1) {
+ VillagerOptimizer.getLang(player.locale()).command_specify_radius.forEach(player::sendMessage);
+ return true;
+ }
+
+ try {
+ int specifiedRadius = Integer.parseInt(args[0]);
+
+ if (specifiedRadius > maxRadius) {
+ final TextReplacementConfig limit = TextReplacementConfig.builder()
+ .matchLiteral("%distance%")
+ .replacement(Integer.toString(maxRadius))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).command_radius_limit_exceed.forEach(line -> player.sendMessage(line.replaceText(limit)));
+ return true;
+ }
+
+ VillagerCache villagerCache = VillagerOptimizer.getCache();
+ int successCount = 0;
+
+ for (Entity entity : player.getNearbyEntities(specifiedRadius, specifiedRadius, specifiedRadius)) {
+ if (!entity.getType().equals(EntityType.VILLAGER)) continue;
+ Villager villager = (Villager) entity;
+ Villager.Profession profession = villager.getProfession();
+ if (profession.equals(Villager.Profession.NITWIT) || profession.equals(Villager.Profession.NONE)) continue;
+
+ WrappedVillager wVillager = villagerCache.getOrAdd(villager);
+
+ if (wVillager.isOptimized()) {
+ VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(wVillager);
+ VillagerOptimizer.callEvent(unOptimizeEvent);
+ if (!unOptimizeEvent.isCancelled()) {
+ wVillager.setOptimization(OptimizationType.NONE);
+ successCount++;
+ }
+ }
+ }
+
+ if (successCount <= 0) {
+ final TextReplacementConfig radius = TextReplacementConfig.builder()
+ .matchLiteral("%radius%")
+ .replacement(Integer.toString(specifiedRadius))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).command_no_villagers_nearby.forEach(line -> player.sendMessage(line.replaceText(radius)));
+ } else {
+ final TextReplacementConfig success_amount = TextReplacementConfig.builder()
+ .matchLiteral("%amount%")
+ .replacement(Integer.toString(successCount))
+ .build();
+ final TextReplacementConfig radius = TextReplacementConfig.builder()
+ .matchLiteral("%radius%")
+ .replacement(Integer.toString(specifiedRadius))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).command_unoptimize_success.forEach(line -> player.sendMessage(line
+ .replaceText(success_amount)
+ .replaceText(radius)
+ ));
+ }
+ } catch (NumberFormatException e) {
+ VillagerOptimizer.getLang(player.locale()).command_radius_invalid.forEach(player::sendMessage);
+ }
+ } else {
+ sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/VillagerOptimizerCmd.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/VillagerOptimizerCmd.java
new file mode 100644
index 0000000..9f75aea
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/VillagerOptimizerCmd.java
@@ -0,0 +1,80 @@
+package me.xginko.villageroptimizer.commands.villageroptimizer;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.commands.SubCommand;
+import me.xginko.villageroptimizer.commands.VillagerOptimizerCommand;
+import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.DisableSubCmd;
+import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.ReloadSubCmd;
+import me.xginko.villageroptimizer.commands.villageroptimizer.subcommands.VersionSubCmd;
+import me.xginko.villageroptimizer.enums.Permissions;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class VillagerOptimizerCmd implements TabCompleter, VillagerOptimizerCommand {
+
+ private final List subCommands = new ArrayList<>(3);
+ private final List tabCompleter = new ArrayList<>(3);
+
+ public VillagerOptimizerCmd() {
+ subCommands.add(new ReloadSubCmd());
+ subCommands.add(new VersionSubCmd());
+ subCommands.add(new DisableSubCmd());
+ subCommands.forEach(subCommand -> tabCompleter.add(subCommand.getLabel()));
+ }
+
+ @Override
+ public String label() {
+ return "villageroptimizer";
+ }
+
+ @Override
+ public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
+ return args.length == 1 ? tabCompleter : Collections.emptyList();
+ }
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
+ if (args.length > 0) {
+ boolean cmdExists = false;
+ for (SubCommand subCommand : subCommands) {
+ if (args[0].equalsIgnoreCase(subCommand.getLabel())) {
+ subCommand.perform(sender, args);
+ cmdExists = true;
+ break;
+ }
+ }
+ if (!cmdExists) sendCommandOverview(sender);
+ } else {
+ sendCommandOverview(sender);
+ }
+ return true;
+ }
+
+ private void sendCommandOverview(CommandSender sender) {
+ if (!sender.hasPermission(Permissions.Commands.RELOAD.get()) && !sender.hasPermission(Permissions.Commands.VERSION.get())) return;
+ sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
+ sender.sendMessage(Component.text("VillagerOptimizer Commands").color(VillagerOptimizer.plugin_style.color()));
+ sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
+ subCommands.forEach(subCommand -> sender.sendMessage(
+ subCommand.getSyntax().append(Component.text(" - ").color(NamedTextColor.DARK_GRAY)).append(subCommand.getDescription())));
+ sender.sendMessage(
+ Component.text("/optimizevillagers ").color(VillagerOptimizer.plugin_style.color())
+ .append(Component.text(" - ").color(NamedTextColor.DARK_GRAY))
+ .append(Component.text("Optimize villagers in a radius").color(NamedTextColor.GRAY))
+ );
+ sender.sendMessage(
+ Component.text("/unoptmizevillagers ").color(VillagerOptimizer.plugin_style.color())
+ .append(Component.text(" - ").color(NamedTextColor.DARK_GRAY))
+ .append(Component.text("Unoptimize villagers in a radius").color(NamedTextColor.GRAY))
+ );
+ sender.sendMessage(Component.text("-----------------------------------------------------").color(NamedTextColor.GRAY));
+ }
+}
\ No newline at end of file
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java
new file mode 100644
index 0000000..d7801e1
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java
@@ -0,0 +1,42 @@
+package me.xginko.villageroptimizer.commands.villageroptimizer.subcommands;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.commands.SubCommand;
+import me.xginko.villageroptimizer.enums.Permissions;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.command.CommandSender;
+
+public class DisableSubCmd extends SubCommand {
+
+ @Override
+ public String getLabel() {
+ return "disable";
+ }
+
+ @Override
+ public TextComponent getDescription() {
+ return Component.text("Disable all plugin tasks and listeners.").color(NamedTextColor.GRAY);
+ }
+
+ @Override
+ public TextComponent getSyntax() {
+ return Component.text("/villageroptimizer disable").color(VillagerOptimizer.plugin_style.color());
+ }
+
+ @Override
+ public void perform(CommandSender sender, String[] args) {
+ if (sender.hasPermission(Permissions.Commands.RELOAD.get())) {
+ sender.sendMessage(Component.text("Disabling VillagerOptimizer...").color(NamedTextColor.RED));
+ VillagerOptimizerModule.modules.forEach(VillagerOptimizerModule::disable);
+ VillagerOptimizerModule.modules.clear();
+ VillagerOptimizer.getCache().cacheMap().clear();
+ sender.sendMessage(Component.text("Disabled all plugin listeners and tasks.").color(NamedTextColor.GREEN));
+ sender.sendMessage(Component.text("You can enable the plugin again using the reload command.").color(NamedTextColor.YELLOW));
+ } else {
+ sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java
diff --git a/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/VersionSubCmd.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/VersionSubCmd.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/VersionSubCmd.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/VersionSubCmd.java
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/config/Config.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/config/Config.java
new file mode 100644
index 0000000..b0532ac
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/config/Config.java
@@ -0,0 +1,147 @@
+package me.xginko.villageroptimizer.config;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class Config {
+
+ private final @NotNull ConfigFile config;
+ public final @NotNull Locale default_lang;
+ public final boolean auto_lang;
+ public final long cache_keep_time_seconds;
+
+ public Config() throws Exception {
+ this.config = loadConfig(new File(VillagerOptimizer.getInstance().getDataFolder(), "config.yml"));
+ structureConfig();
+ this.default_lang = Locale.forLanguageTag(
+ getString("general.default-language", "en_us",
+ "The default language that will be used if auto-language is false or no matching language file was found.")
+ .replace("_", "-"));
+ this.auto_lang = getBoolean("general.auto-language", true,
+ "If set to true, will display messages based on client language");
+ this.cache_keep_time_seconds = getInt("general.cache-keep-time-seconds", 30,
+ "The amount of time in seconds a villager will be kept in the plugin's cache.");
+ }
+
+ private ConfigFile loadConfig(File ymlFile) throws Exception {
+ File parent = new File(ymlFile.getParent());
+ if (!parent.exists() && !parent.mkdir())
+ VillagerOptimizer.getLog().severe("Unable to create plugin config directory.");
+ if (!ymlFile.exists())
+ ymlFile.createNewFile(); // Result can be ignored because this method only returns false if the file already exists
+ return ConfigFile.loadConfig(ymlFile);
+ }
+
+ public void saveConfig() {
+ try {
+ config.save();
+ } catch (Exception e) {
+ VillagerOptimizer.getLog().severe("Failed to save config file! - " + e.getLocalizedMessage());
+ }
+ }
+
+ private void structureConfig() {
+ config.addDefault("config-version", 1.00);
+ createTitledSection("General", "general");
+ createTitledSection("Optimization", "optimization-methods");
+ config.addDefault("optimization-methods.commands.unoptimizevillagers", null);
+ config.addComment("optimization-methods.commands", """
+ If you want to disable commands, negate the following permissions:\s
+ villageroptimizer.cmd.optimize\s
+ villageroptimizer.cmd.unoptimize
+ """);
+ config.addDefault("optimization-methods.nametag-optimization.enable", true);
+ createTitledSection("Villager Chunk Limit", "villager-chunk-limit");
+ createTitledSection("Gameplay", "gameplay");
+ config.addDefault("gameplay.villagers-spawn-as-adults.enable", false);
+ config.addDefault("gameplay.prevent-trading-with-unoptimized.enable", false);
+ config.addDefault("gameplay.villager-leveling.enable", true);
+ config.addDefault("gameplay.trade-restocking.enable", true);
+ config.addDefault("gameplay.prevent-targeting.enable", true);
+ config.addDefault("gameplay.prevent-damage.enable", true);
+ }
+
+ public void createTitledSection(@NotNull String title, @NotNull String path) {
+ config.addSection(title);
+ config.addDefault(path, null);
+ }
+
+ public @NotNull ConfigFile master() {
+ return config;
+ }
+
+ public boolean getBoolean(@NotNull String path, boolean def, @NotNull String comment) {
+ config.addDefault(path, def, comment);
+ return config.getBoolean(path, def);
+ }
+
+ public boolean getBoolean(@NotNull String path, boolean def) {
+ config.addDefault(path, def);
+ return config.getBoolean(path, def);
+ }
+
+ public @NotNull String getString(@NotNull String path, @NotNull String def, @NotNull String comment) {
+ config.addDefault(path, def, comment);
+ return config.getString(path, def);
+ }
+
+ public @NotNull String getString(@NotNull String path, @NotNull String def) {
+ config.addDefault(path, def);
+ return config.getString(path, def);
+ }
+
+ public double getDouble(@NotNull String path, @NotNull Double def, @NotNull String comment) {
+ config.addDefault(path, def, comment);
+ return config.getDouble(path, def);
+ }
+
+ public double getDouble(@NotNull String path, @NotNull Double def) {
+ config.addDefault(path, def);
+ return config.getDouble(path, def);
+ }
+
+ public int getInt(@NotNull String path, int def, @NotNull String comment) {
+ config.addDefault(path, def, comment);
+ return config.getInteger(path, def);
+ }
+
+ public int getInt(@NotNull String path, int def) {
+ config.addDefault(path, def);
+ return config.getInteger(path, def);
+ }
+
+ public @NotNull List getList(@NotNull String path, @NotNull List def, @NotNull String comment) {
+ config.addDefault(path, def, comment);
+ return config.getStringList(path);
+ }
+
+ public @NotNull List getList(@NotNull String path, @NotNull List def) {
+ config.addDefault(path, def);
+ return config.getStringList(path);
+ }
+
+ public @NotNull ConfigSection getConfigSection(@NotNull String path, @NotNull Map defaultKeyValue) {
+ config.addDefault(path, null);
+ config.makeSectionLenient(path);
+ defaultKeyValue.forEach((string, object) -> config.addExample(path+"."+string, object));
+ return config.getConfigSection(path);
+ }
+
+ public @NotNull ConfigSection getConfigSection(@NotNull String path, @NotNull Map defaultKeyValue, @NotNull String comment) {
+ config.addDefault(path, null, comment);
+ config.makeSectionLenient(path);
+ defaultKeyValue.forEach((string, object) -> config.addExample(path+"."+string, object));
+ return config.getConfigSection(path);
+ }
+
+ public void addComment(@NotNull String path, @NotNull String comment) {
+ config.addComment(path, comment);
+ }
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/config/LanguageCache.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/config/LanguageCache.java
new file mode 100644
index 0000000..962b4c0
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/config/LanguageCache.java
@@ -0,0 +1,115 @@
+package me.xginko.villageroptimizer.config;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.List;
+
+public class LanguageCache {
+
+ private final @NotNull ConfigFile lang;
+ private final @NotNull MiniMessage miniMessage;
+
+ public final @NotNull Component no_permission;
+ public final @NotNull List nametag_optimize_success, nametag_on_optimize_cooldown, nametag_unoptimize_success,
+ block_optimize_success, block_on_optimize_cooldown, block_unoptimize_success,
+ workstation_optimize_success, workstation_on_optimize_cooldown, workstation_unoptimize_success,
+ command_optimize_success, command_radius_limit_exceed, command_optimize_fail, command_unoptimize_success,
+ command_specify_radius, command_radius_invalid, command_no_villagers_nearby,
+ trades_restocked, optimize_for_trading, villager_leveling_up;
+
+ public LanguageCache(String lang) throws Exception {
+ this.lang = loadLang(new File(VillagerOptimizer.getInstance().getDataFolder() + File.separator + "lang", lang + ".yml"));
+ this.miniMessage = MiniMessage.miniMessage();
+
+ // General
+ this.no_permission = getTranslation("messages.no-permission",
+ "You don't have permission to use this command.");
+ this.trades_restocked = getListTranslation("messages.trades-restocked",
+ List.of("All trades have been restocked! Next restock in %time%"));
+ this.optimize_for_trading = getListTranslation("messages.optimize-to-trade",
+ List.of("You need to optimize this villager before you can trade with it."));
+ this.villager_leveling_up = getListTranslation("messages.villager-leveling-up",
+ List.of("Villager is currently leveling up! You can use the villager again in %time%."));
+ // Nametag
+ this.nametag_optimize_success = getListTranslation("messages.nametag.optimize-success",
+ List.of("Successfully optimized villager by using a nametag."));
+ this.nametag_on_optimize_cooldown = getListTranslation("messages.nametag.optimize-on-cooldown",
+ List.of("You need to wait %time% until you can optimize this villager again."));
+ this.nametag_unoptimize_success = getListTranslation("messages.nametag.unoptimize-success",
+ List.of("Successfully unoptimized villager by using a nametag."));
+ // Block
+ this.block_optimize_success = getListTranslation("messages.block.optimize-success",
+ List.of("%villagertype% villager successfully optimized using block %blocktype%."));
+ this.block_on_optimize_cooldown = getListTranslation("messages.block.optimize-on-cooldown",
+ List.of("You need to wait %time% until you can optimize this villager again."));
+ this.block_unoptimize_success = getListTranslation("messages.block.unoptimize-success",
+ List.of("Successfully unoptimized %villagertype% villager by removing %blocktype%."));
+ // Workstation
+ this.workstation_optimize_success = getListTranslation("messages.workstation.optimize-success",
+ List.of("%villagertype% villager successfully optimized using workstation %workstation%."));
+ this.workstation_on_optimize_cooldown = getListTranslation("messages.workstation.optimize-on-cooldown",
+ List.of("You need to wait %time% until you can optimize this villager again."));
+ this.workstation_unoptimize_success = getListTranslation("messages.workstation.unoptimize-success",
+ List.of("Successfully unoptimized %villagertype% villager by removing workstation block %workstation%."));
+ // Command
+ this.command_optimize_success = getListTranslation("messages.command.optimize-success",
+ List.of("Successfully optimized %amount% villager(s) in a radius of %radius% blocks."));
+ this.command_radius_limit_exceed = getListTranslation("messages.command.radius-limit-exceed",
+ List.of("The radius you entered exceeds the limit of %distance% blocks."));
+ this.command_optimize_fail = getListTranslation("messages.command.optimize-fail",
+ List.of("%amount% villagers couldn't be optimized because they have recently been optimized."));
+ this.command_unoptimize_success = getListTranslation("messages.command.unoptimize-success",
+ List.of("Successfully unoptimized %amount% villager(s) in a radius of %radius% blocks."));
+ this.command_specify_radius = getListTranslation("messages.command.specify-radius",
+ List.of("Please specify a radius."));
+ this.command_radius_invalid = getListTranslation("messages.command.radius-invalid",
+ List.of("The radius you entered is not a valid number. Try again."));
+ this.command_no_villagers_nearby = getListTranslation("messages.command.no-villagers-nearby",
+ List.of("Couldn't find any employed villagers within a radius of %radius%."));
+
+ saveLang();
+ }
+
+ private ConfigFile loadLang(File ymlFile) throws Exception {
+ File parent = new File(ymlFile.getParent());
+ if (!parent.exists())
+ if (!parent.mkdir())
+ VillagerOptimizer.getLog().severe("Unable to create lang directory.");
+ if (!ymlFile.exists())
+ ymlFile.createNewFile(); // Result can be ignored because this method only returns false if the file already exists
+ return ConfigFile.loadConfig(ymlFile);
+ }
+
+ private void saveLang() {
+ try {
+ lang.save();
+ } catch (Exception e) {
+ VillagerOptimizer.getLog().severe("Failed to save language file: "+ lang.getFile().getName() +" - " + e.getLocalizedMessage());
+ }
+ }
+
+ public @NotNull Component getTranslation(@NotNull String path, @NotNull String defaultTranslation) {
+ lang.addDefault(path, defaultTranslation);
+ return miniMessage.deserialize(lang.getString(path, defaultTranslation));
+ }
+
+ public @NotNull Component getTranslation(@NotNull String path, @NotNull String defaultTranslation, @NotNull String comment) {
+ lang.addDefault(path, defaultTranslation, comment);
+ return miniMessage.deserialize(lang.getString(path, defaultTranslation));
+ }
+
+ public @NotNull List getListTranslation(@NotNull String path, @NotNull List defaultTranslation) {
+ lang.addDefault(path, defaultTranslation);
+ return lang.getStringList(path).stream().map(miniMessage::deserialize).toList();
+ }
+
+ public @NotNull List getListTranslation(@NotNull String path, @NotNull List defaultTranslation, @NotNull String comment) {
+ lang.addDefault(path, defaultTranslation, comment);
+ return lang.getStringList(path).stream().map(miniMessage::deserialize).toList();
+ }
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/enums/Keys.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/enums/Keys.java
new file mode 100644
index 0000000..4c75d96
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/enums/Keys.java
@@ -0,0 +1,24 @@
+package me.xginko.villageroptimizer.enums;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import org.bukkit.NamespacedKey;
+
+public enum Keys {
+
+ OPTIMIZATION_TYPE(VillagerOptimizer.getKey("optimization-type")),
+ LAST_OPTIMIZE(VillagerOptimizer.getKey("last-optimize")),
+ LAST_LEVELUP(VillagerOptimizer.getKey("last-levelup")),
+ LAST_RESTOCK(VillagerOptimizer.getKey("last-restock")),
+ LAST_OPTIMIZE_NAME(VillagerOptimizer.getKey("last-optimize-name"));
+
+ private final NamespacedKey key;
+
+ Keys(NamespacedKey key) {
+ this.key = key;
+ }
+
+ public NamespacedKey key() {
+ return key;
+ }
+
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/enums/OptimizationType.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/enums/OptimizationType.java
new file mode 100644
index 0000000..09c304b
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/enums/OptimizationType.java
@@ -0,0 +1,11 @@
+package me.xginko.villageroptimizer.enums;
+
+public enum OptimizationType {
+
+ COMMAND,
+ NAMETAG,
+ WORKSTATION,
+ BLOCK,
+ NONE
+
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/enums/Permissions.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/enums/Permissions.java
new file mode 100644
index 0000000..aec2615
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/enums/Permissions.java
@@ -0,0 +1,44 @@
+package me.xginko.villageroptimizer.enums;
+
+public class Permissions {
+ public enum Commands {
+ VERSION("villageroptimizer.cmd.version"),
+ RELOAD("villageroptimizer.cmd.reload"),
+ OPTIMIZE_RADIUS("villageroptimizer.cmd.optimize"),
+ UNOPTIMIZE_RADIUS("villageroptimizer.cmd.unoptimize");
+ private final String key;
+ Commands(String key) {
+ this.key = key;
+ }
+ public String get() {
+ return key;
+ }
+ }
+ public enum Optimize {
+ NAMETAG("villageroptimizer.optimize.nametag"),
+ BLOCK("villageroptimizer.optimize.block"),
+ WORKSTATION("villageroptimizer.optimize.workstation");
+ private final String key;
+ Optimize(String key) {
+ this.key = key;
+ }
+ public String get() {
+ return key;
+ }
+ }
+ public enum Bypass {
+ TRADE_PREVENTION("villageroptimizer.bypass.tradeprevention"),
+ RESTOCK_COOLDOWN("villageroptimizer.bypass.restockcooldown"),
+ NAMETAG_COOLDOWN("villageroptimizer.bypass.nametagcooldown"),
+ BLOCK_COOLDOWN("villageroptimizer.bypass.blockcooldown"),
+ WORKSTATION_COOLDOWN("villageroptimizer.bypass.workstationcooldown"),
+ COMMAND_COOLDOWN("villageroptimizer.bypass.commandcooldown");
+ private final String key;
+ Bypass(String key) {
+ this.key = key;
+ }
+ public String get() {
+ return key;
+ }
+ }
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/events/VillagerOptimizeEvent.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/events/VillagerOptimizeEvent.java
new file mode 100644
index 0000000..2dc79e9
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/events/VillagerOptimizeEvent.java
@@ -0,0 +1,70 @@
+package me.xginko.villageroptimizer.events;
+
+import me.xginko.villageroptimizer.WrappedVillager;
+import me.xginko.villageroptimizer.enums.OptimizationType;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+public class VillagerOptimizeEvent extends Event implements Cancellable {
+
+ private static final @NotNull HandlerList handlers = new HandlerList();
+ private final @NotNull WrappedVillager wrappedVillager;
+ private @NotNull OptimizationType type;
+ private boolean isCancelled = false;
+
+ public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType type, boolean isAsync) throws IllegalArgumentException {
+ super(isAsync);
+ this.wrappedVillager = wrappedVillager;
+ if (type.equals(OptimizationType.NONE)) {
+ throw new IllegalArgumentException("OptimizationType can't be NONE.");
+ } else {
+ this.type = type;
+ }
+ }
+
+ public VillagerOptimizeEvent(@NotNull WrappedVillager wrappedVillager, @NotNull OptimizationType type) throws IllegalArgumentException {
+ this.wrappedVillager = wrappedVillager;
+ if (type.equals(OptimizationType.NONE)) {
+ throw new IllegalArgumentException("OptimizationType can't be NONE.");
+ } else {
+ this.type = type;
+ }
+ }
+
+ public @NotNull WrappedVillager getWrappedVillager() {
+ return wrappedVillager;
+ }
+
+ public @NotNull OptimizationType getOptimizationType() {
+ return type;
+ }
+
+ public void setOptimizationType(@NotNull OptimizationType type) throws IllegalArgumentException {
+ if (type.equals(OptimizationType.NONE)) {
+ throw new IllegalArgumentException("OptimizationType can't be NONE.");
+ } else {
+ this.type = type;
+ }
+ }
+
+ @Override
+ public void setCancelled(boolean cancel) {
+ isCancelled = cancel;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return isCancelled;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return handlers;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/events/VillagerUnoptimizeEvent.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/events/VillagerUnoptimizeEvent.java
new file mode 100644
index 0000000..41f1f9f
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/events/VillagerUnoptimizeEvent.java
@@ -0,0 +1,46 @@
+package me.xginko.villageroptimizer.events;
+
+import me.xginko.villageroptimizer.WrappedVillager;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+
+public class VillagerUnoptimizeEvent extends Event implements Cancellable {
+
+ private static final @NotNull HandlerList handlers = new HandlerList();
+ private final @NotNull WrappedVillager wrappedVillager;
+ private boolean isCancelled = false;
+
+ public VillagerUnoptimizeEvent(@NotNull WrappedVillager wrappedVillager, boolean isAsync) {
+ super(isAsync);
+ this.wrappedVillager = wrappedVillager;
+ }
+
+ public VillagerUnoptimizeEvent(@NotNull WrappedVillager wrappedVillager) {
+ this.wrappedVillager = wrappedVillager;
+ }
+
+ public @NotNull WrappedVillager getWrappedVillager() {
+ return wrappedVillager;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return isCancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.isCancelled = cancel;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return handlers;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+}
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/RenameOptimizedVillagers.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/RenameOptimizedVillagers.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/RenameOptimizedVillagers.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/RenameOptimizedVillagers.java
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/VillagerChunkLimit.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/VillagerChunkLimit.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/VillagerChunkLimit.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/VillagerChunkLimit.java
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/VillagerOptimizerModule.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/VillagerOptimizerModule.java
new file mode 100644
index 0000000..7f17ebe
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/VillagerOptimizerModule.java
@@ -0,0 +1,46 @@
+package me.xginko.villageroptimizer.modules;
+
+import me.xginko.villageroptimizer.modules.extras.PreventUnoptimizedTrading;
+import me.xginko.villageroptimizer.modules.extras.PreventVillagerDamage;
+import me.xginko.villageroptimizer.modules.extras.PreventVillagerTargetting;
+import me.xginko.villageroptimizer.modules.extras.MakeVillagersSpawnAdult;
+import me.xginko.villageroptimizer.modules.mechanics.LevelVillagers;
+import me.xginko.villageroptimizer.modules.mechanics.RestockTrades;
+import me.xginko.villageroptimizer.modules.optimizations.OptimizeByBlock;
+import me.xginko.villageroptimizer.modules.optimizations.OptimizeByNametag;
+import me.xginko.villageroptimizer.modules.optimizations.OptimizeByWorkstation;
+
+import java.util.HashSet;
+
+public interface VillagerOptimizerModule {
+
+ void enable();
+ void disable();
+ boolean shouldEnable();
+
+ HashSet modules = new HashSet<>();
+
+ static void reloadModules() {
+ modules.forEach(VillagerOptimizerModule::disable);
+ modules.clear();
+
+ modules.add(new OptimizeByNametag());
+ modules.add(new OptimizeByBlock());
+ modules.add(new OptimizeByWorkstation());
+
+ modules.add(new RestockTrades());
+ modules.add(new LevelVillagers());
+
+ modules.add(new MakeVillagersSpawnAdult());
+ modules.add(new PreventUnoptimizedTrading());
+ modules.add(new PreventVillagerDamage());
+ modules.add(new PreventVillagerTargetting());
+
+ modules.add(new VillagerChunkLimit());
+ modules.add(new RenameOptimizedVillagers());
+
+ modules.forEach(module -> {
+ if (module.shouldEnable()) module.enable();
+ });
+ }
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/MakeVillagersSpawnAdult.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/MakeVillagersSpawnAdult.java
new file mode 100644
index 0000000..a536642
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/MakeVillagersSpawnAdult.java
@@ -0,0 +1,44 @@
+package me.xginko.villageroptimizer.modules.extras;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.CreatureSpawnEvent;
+
+public class MakeVillagersSpawnAdult implements VillagerOptimizerModule, Listener {
+
+ public MakeVillagersSpawnAdult() {}
+
+ @Override
+ public void enable() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("gameplay.villagers-spawn-as-adults.enable", false, """
+ Spawned villagers will immediately be adults.\s
+ This is to save some more performance as players don't have to keep unoptimized\s
+ villagers loaded because they have to wait for them to turn into adults before they can\s
+ optimize them.""");
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onVillagerSpawn(CreatureSpawnEvent event) {
+ if (event.getEntityType().equals(EntityType.VILLAGER)) {
+ Villager villager = (Villager) event.getEntity();
+ if (!villager.isAdult()) villager.setAdult();
+ }
+ }
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventUnoptimizedTrading.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventUnoptimizedTrading.java
new file mode 100644
index 0000000..54fb7ac
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventUnoptimizedTrading.java
@@ -0,0 +1,80 @@
+package me.xginko.villageroptimizer.modules.extras;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.enums.Permissions;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.event.inventory.TradeSelectEvent;
+
+public class PreventUnoptimizedTrading implements VillagerOptimizerModule, Listener {
+
+ private final VillagerCache villagerCache;
+ private final boolean notifyPlayer;
+
+ public PreventUnoptimizedTrading() {
+ shouldEnable();
+ this.villagerCache = VillagerOptimizer.getCache();
+ Config config = VillagerOptimizer.getConfiguration();
+ config.addComment("gameplay.prevent-trading-with-unoptimized.enable", """
+ Will prevent players from selecting and using trades of unoptimized villagers.\s
+ Use this if you have a lot of villagers and therefore want to force your players to optimize them.\s
+ Inventories can still be opened so players can move villagers around.""");
+ this.notifyPlayer = config.getBoolean("gameplay.prevent-trading-with-unoptimized.notify-player", true,
+ "Sends players a message when they try to trade with an unoptimized villager.");
+ }
+
+ @Override
+ public void enable() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-trading-with-unoptimized.enable", false);
+ }
+
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+ private void onTradeOpen(TradeSelectEvent event) {
+ Player player = (Player) event.getWhoClicked();
+ if (player.hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
+ if (
+ event.getInventory().getType().equals(InventoryType.MERCHANT)
+ && event.getInventory().getHolder() instanceof Villager villager
+ && !villagerCache.getOrAdd(villager).isOptimized()
+ ) {
+ event.setCancelled(true);
+ if (notifyPlayer)
+ VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
+ }
+ }
+
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+ private void onInventoryClick(InventoryClickEvent event) {
+ Player player = (Player) event.getWhoClicked();
+ if (player.hasPermission(Permissions.Bypass.TRADE_PREVENTION.get())) return;
+ if (
+ event.getInventory().getType().equals(InventoryType.MERCHANT)
+ && event.getInventory().getHolder() instanceof Villager villager
+ && !villagerCache.getOrAdd(villager).isOptimized()
+ ) {
+ event.setCancelled(true);
+ if (notifyPlayer)
+ VillagerOptimizer.getLang(player.locale()).optimize_for_trading.forEach(player::sendMessage);
+ }
+ }
+}
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerDamage.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerDamage.java
similarity index 96%
rename from src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerDamage.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerDamage.java
index 75dd232..9db543b 100644
--- a/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerDamage.java
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerDamage.java
@@ -56,7 +56,7 @@ public class PreventVillagerDamage implements VillagerOptimizerModule, Listener
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
- private void onDamageReceive(EntityDamageByEntityEvent event) {
+ private void onDamageByEntity(EntityDamageByEntityEvent event) {
if (
event.getEntityType().equals(EntityType.VILLAGER)
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
@@ -79,7 +79,7 @@ public class PreventVillagerDamage implements VillagerOptimizerModule, Listener
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
- private void onDamageReceive(EntityDamageByBlockEvent event) {
+ private void onDamageByBlock(EntityDamageByBlockEvent event) {
if (
block
&& event.getEntityType().equals(EntityType.VILLAGER)
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerTargetting.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerTargetting.java
new file mode 100644
index 0000000..dc4e0f2
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/extras/PreventVillagerTargetting.java
@@ -0,0 +1,78 @@
+package me.xginko.villageroptimizer.modules.extras;
+
+import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Mob;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
+
+public class PreventVillagerTargetting implements VillagerOptimizerModule, Listener {
+
+ private final VillagerCache villagerCache;
+
+ public PreventVillagerTargetting() {
+ this.villagerCache = VillagerOptimizer.getCache();
+ }
+
+ @Override
+ public void enable() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("gameplay.prevent-targeting.enable", true,
+ "Prevents hostile entities from targeting optimized villagers.");
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onTarget(EntityTargetLivingEntityEvent event) {
+ // Yes, instanceof checks would look way more beautiful here but checking type is much faster
+ Entity target = event.getTarget();
+ if (
+ target != null
+ && target.getType().equals(EntityType.VILLAGER)
+ && villagerCache.getOrAdd((Villager) target).isOptimized()
+ ) {
+ event.setCancelled(true);
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onEntityTargetVillager(EntityPathfindEvent event) {
+ Entity target = event.getTargetEntity();
+ if (
+ target != null
+ && target.getType().equals(EntityType.VILLAGER)
+ && villagerCache.getOrAdd((Villager) target).isOptimized()
+ ) {
+ event.setCancelled(true);
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ private void onEntityAttackVillager(EntityDamageByEntityEvent event) {
+ if (
+ event.getEntityType().equals(EntityType.VILLAGER)
+ && event.getDamager() instanceof Mob attacker
+ && villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
+ ) {
+ attacker.setTarget(null);
+ }
+ }
+ }
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/mechanics/LevelVillagers.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/mechanics/LevelVillagers.java
similarity index 98%
rename from src/main/java/me/xginko/villageroptimizer/modules/mechanics/LevelVillagers.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/mechanics/LevelVillagers.java
index 28fda83..85c7217 100644
--- a/src/main/java/me/xginko/villageroptimizer/modules/mechanics/LevelVillagers.java
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/mechanics/LevelVillagers.java
@@ -68,7 +68,7 @@ public class LevelVillagers implements VillagerOptimizerModule, Listener {
if (wVillager.canLevelUp(cooldown)) {
if (wVillager.calculateLevel() > villager.getVillagerLevel()) {
villager.getScheduler().run(plugin, enableAI -> {
- villager.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, (int) (20 + (cooldown / 50L)), 120, false, false));
+ villager.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 120, 120, false, false));
villager.setAware(true);
}, null);
villager.getScheduler().runDelayed(plugin, disableAI -> {
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/mechanics/RestockTrades.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/mechanics/RestockTrades.java
new file mode 100644
index 0000000..594db8f
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/mechanics/RestockTrades.java
@@ -0,0 +1,80 @@
+package me.xginko.villageroptimizer.modules.mechanics;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+import me.xginko.villageroptimizer.VillagerCache;
+import me.xginko.villageroptimizer.config.Config;
+import me.xginko.villageroptimizer.enums.Permissions;
+import me.xginko.villageroptimizer.WrappedVillager;
+import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
+import me.xginko.villageroptimizer.utils.CommonUtil;
+import net.kyori.adventure.text.TextReplacementConfig;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Villager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+
+public class RestockTrades implements VillagerOptimizerModule, Listener {
+
+ private final VillagerCache villagerCache;
+ private final long restock_delay_millis;
+ private final boolean shouldLog, notifyPlayer;
+
+ public RestockTrades() {
+ shouldEnable();
+ this.villagerCache = VillagerOptimizer.getCache();
+ Config config = VillagerOptimizer.getConfiguration();
+ config.addComment("gameplay.trade-restocking.enable", """
+ This is for automatic restocking of trades for optimized villagers. Optimized Villagers\s
+ Don't have enough AI to do trade restocks themselves, so this needs to always be enabled.""");
+ this.restock_delay_millis = config.getInt("gameplay.trade-restocking.delay-in-ticks", 1000,
+ "1 second = 20 ticks. There are 24.000 ticks in a single minecraft day.") * 50L;
+ this.notifyPlayer = config.getBoolean("gameplay.trade-restocking.notify-player", true,
+ "Sends the player a message when the trades were restocked on a clicked villager.");
+ this.shouldLog = config.getBoolean("gameplay.trade-restocking.log", false);
+ }
+
+ @Override
+ public void enable() {
+ VillagerOptimizer plugin = VillagerOptimizer.getInstance();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void disable() {
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public boolean shouldEnable() {
+ return VillagerOptimizer.getConfiguration().getBoolean("gameplay.trade-restocking.enable", true);
+ }
+
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+ private void onInteract(PlayerInteractEntityEvent event) {
+ if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
+
+ WrappedVillager wVillager = villagerCache.getOrAdd((Villager) event.getRightClicked());
+ if (!wVillager.isOptimized()) return;
+ Player player = event.getPlayer();
+
+ final boolean player_bypassing = player.hasPermission(Permissions.Bypass.RESTOCK_COOLDOWN.get());
+
+ if (wVillager.canRestock(restock_delay_millis) || player_bypassing) {
+ wVillager.restock();
+ wVillager.saveRestockTime();
+ if (notifyPlayer && !player_bypassing) {
+ final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
+ .matchLiteral("%time%")
+ .replacement(CommonUtil.formatTime(wVillager.getRestockCooldownMillis(restock_delay_millis)))
+ .build();
+ VillagerOptimizer.getLang(player.locale()).trades_restocked.forEach(line -> player.sendMessage(line.replaceText(timeLeft)));
+ }
+ if (shouldLog)
+ VillagerOptimizer.getLog().info("Restocked optimized villager at "+ wVillager.villager().getLocation());
+ }
+ }
+}
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByBlock.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByBlock.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByBlock.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByBlock.java
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByNametag.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByNametag.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByNametag.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByNametag.java
diff --git a/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByWorkstation.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByWorkstation.java
similarity index 100%
rename from src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByWorkstation.java
rename to VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/modules/optimizations/OptimizeByWorkstation.java
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java
new file mode 100644
index 0000000..1a0e2f1
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java
@@ -0,0 +1,24 @@
+package me.xginko.villageroptimizer.utils;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.time.Duration;
+
+import static java.lang.String.format;
+
+public class CommonUtil {
+ public static @NotNull String formatTime(final long millis) {
+ Duration duration = Duration.ofMillis(millis);
+ final int seconds = duration.toSecondsPart();
+ final int minutes = duration.toMinutesPart();
+ final int hours = duration.toHoursPart();
+
+ if (hours > 0) {
+ return format("%02dh %02dm %02ds", hours, minutes, seconds);
+ } else if (minutes > 0) {
+ return format("%02dm %02ds", minutes, seconds);
+ } else {
+ return format("%02ds", seconds);
+ }
+ }
+}
diff --git a/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/utils/LogUtil.java b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/utils/LogUtil.java
new file mode 100644
index 0000000..b0bd059
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/java/me/xginko/villageroptimizer/utils/LogUtil.java
@@ -0,0 +1,28 @@
+package me.xginko.villageroptimizer.utils;
+
+import me.xginko.villageroptimizer.VillagerOptimizer;
+
+import java.util.logging.Level;
+
+public class LogUtil {
+
+ public static void moduleLog(Level logLevel, String path, String logMessage) {
+ VillagerOptimizer.getLog().log(logLevel, "(" + path + ") " + logMessage);
+ }
+
+ public static void materialNotRecognized(String path, String material) {
+ moduleLog(Level.WARNING, path, "Material '" + material + "' not recognized. Please use correct Spigot Material enums for your Minecraft version!");
+ }
+
+ public static void entityTypeNotRecognized(String path, String entityType) {
+ moduleLog(Level.WARNING, path, "EntityType '" + entityType + "' not recognized. Please use correct Spigot EntityType enums for your Minecraft version!");
+ }
+
+ public static void enchantmentNotRecognized(String path, String enchantment) {
+ moduleLog(Level.WARNING, path, "Enchantment '" + enchantment + "' not recognized. Please use correct Spigot Enchantment enums for your Minecraft version!");
+ }
+
+ public static void integerNotRecognized(String path, String element) {
+ moduleLog(Level.WARNING, path, "The configured amount for "+element+" is not an integer.");
+ }
+}
\ No newline at end of file
diff --git a/VillagerOptimizer-1.20.2/src/main/resources/lang/en_us.yml b/VillagerOptimizer-1.20.2/src/main/resources/lang/en_us.yml
new file mode 100644
index 0000000..44e663f
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/resources/lang/en_us.yml
@@ -0,0 +1,44 @@
+messages:
+ no-permission: "You don't have permission to use this command."
+ optimize-to-trade:
+ - "You need to optimize this villager before you can trade with it."
+ trades-restocked:
+ - "All trades have been restocked! Next restock in %time%."
+ villager-leveling-up:
+ - "Villager is currently leveling up! You can use the villager again in %time%"
+ nametag:
+ optimize-success:
+ - "Successfully optimized villager by using a nametag."
+ optimize-on-cooldown:
+ - "You need to wait %time% until you can optimize this villager again."
+ unoptimize-success:
+ - "Successfully unoptimized villager by using a nametag."
+ block:
+ optimize-success:
+ - "%vil_profession% villager successfully optimized using block %blocktype%."
+ optimize-on-cooldown:
+ - "You need to wait %time% until you can optimize this villager again."
+ unoptimize-success:
+ - "Successfully unoptimized %vil_profession% villager by removing %blocktype%."
+ workstation:
+ optimize-success:
+ - "%vil_profession% villager successfully optimized using workstation block %blocktype%."
+ optimize-on-cooldown:
+ - "You need to wait %time% until you can optimize this villager again."
+ unoptimize-success:
+ - "Successfully unoptimized villager by removing workstation block %blocktype%."
+ command:
+ optimize-success:
+ - "Successfully optimized %amount% villager(s) in a radius of %radius% blocks."
+ optimize-fail:
+ - "%amount% villagers couldn't be optimized because they have recently been optimized."
+ radius-limit-exceed:
+ - "The radius you entered exceeds the limit of %distance% blocks."
+ unoptimize-success:
+ - "Successfully unoptimized %amount% villager(s) in a radius of %radius% blocks."
+ specify-radius:
+ - "Please specify a radius."
+ radius-invalid:
+ - "The radius you entered is not a valid number. Try again."
+ no-villagers-nearby:
+ - "Couldn't find any employed villagers within a radius of %radius%."
\ No newline at end of file
diff --git a/VillagerOptimizer-1.20.2/src/main/resources/plugin.yml b/VillagerOptimizer-1.20.2/src/main/resources/plugin.yml
new file mode 100644
index 0000000..d6221c3
--- /dev/null
+++ b/VillagerOptimizer-1.20.2/src/main/resources/plugin.yml
@@ -0,0 +1,93 @@
+name: VillagerOptimizer
+version: '${project.version}'
+main: me.xginko.villageroptimizer.VillagerOptimizer
+authors: [ xGinko ]
+description: ${project.description}
+website: ${project.url}
+api-version: '1.19'
+folia-supported: true
+commands:
+ villageroptimizer:
+ usage: /villageroptimizer [ reload, version ]
+ description: VillagerOptimizer admin commands
+ aliases:
+ - voptimizer
+ - vo
+ optimizevillagers:
+ usage: /optimizevillagers
+ description: Optmize villagers in a radius around you
+ aliases:
+ - optvils
+ unoptimizevillagers:
+ usage: /unoptimizevillagers
+ description: Unoptmize villagers in a radius around you
+ aliases:
+ - unoptvils
+permissions:
+ villageroptimizer.ignore:
+ description: Players with this permission won't be able to use the plugin features
+ children:
+ villageroptimizer.optimize.nametag: false
+ villageroptimizer.optimize.block: false
+ villageroptimizer.optimize.workstation: false
+ villageroptimizer.playerdefaults:
+ description: Default permissions for players
+ default: true
+ children:
+ villageroptimizer.cmd.optimize: true
+ villageroptimizer.cmd.unoptimize: true
+ villageroptimizer.optimize.*: true
+ villageroptimizer.*:
+ description: All plugin permissions
+ children:
+ villageroptimizer.cmd.*: true
+ villageroptimizer.bypass.*: true
+ villageroptimizer.optimize.*: true
+ villageroptimizer.optimize.*:
+ description: Optimization type permissions
+ children:
+ villageroptimizer.optimize.nametag: true
+ villageroptimizer.optimize.block: true
+ villageroptimizer.optimize.workstation: true
+ villageroptimizer.optimize.nametag:
+ description: Optimize/Unoptimize villagers using nametags
+ villageroptimizer.optimize.block:
+ description: Optimize/Unoptimize villagers using specific blocks
+ villageroptimizer.optimize.workstation:
+ description: Optimize/Unoptimize villagers using workstations
+ villageroptimizer.cmd.*:
+ description: All command permissions
+ children:
+ villageroptimizer.cmd.reload: true
+ villageroptimizer.cmd.version: true
+ villageroptimizer.cmd.optimize: true
+ villageroptimizer.cmd.unoptimize: true
+ villageroptimizer.cmd.reload:
+ description: Reload the plugin configuration
+ villageroptimizer.cmd.version:
+ description: Show the plugin version
+ villageroptimizer.cmd.optimize:
+ description: Optimize villagers in a radius
+ villageroptimizer.cmd.unoptimize:
+ description: Unoptimize villagers in a radius
+ villageroptimizer.bypass.*:
+ description: All bypass permissions
+ children:
+ villageroptimizer.bypass.tradeprevention: true
+ villageroptimizer.bypass.restockcooldown: true
+ villageroptimizer.bypass.nametagcooldown: true
+ villageroptimizer.bypass.blockcooldown: true
+ villageroptimizer.bypass.workstationcooldown: true
+ villageroptimizer.bypass.commandcooldown: true
+ villageroptimizer.bypass.tradeprevention:
+ description: Bypass unoptimized trading prevention if enabled
+ villageroptimizer.bypass.restockcooldown:
+ description: Bypass permission for optimized trade restock cooldown
+ villageroptimizer.bypass.nametagcooldown:
+ description: Bypass permission for nametag optimization cooldown
+ villageroptimizer.bypass.blockcooldown:
+ description: Bypass permission for block optimization cooldown
+ villageroptimizer.bypass.workstationcooldown:
+ description: Bypass permission for workstation optimization cooldown
+ villageroptimizer.bypass.commandcooldown:
+ description: Bypass permission for command optimization cooldown
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 9826e44..72399eb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,17 +4,20 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- me.xginko
+ me.xginko.VillagerOptimizer
VillagerOptimizer
1.0.0
- jar
+
+ VillagerOptimizer-1.20.2
+ VillagerOptimizer-1.16.5
+
+ pom
VillagerOptimizer
Combat heavy villager lag by letting players optimize their trading halls.
https://github.com/xGinko/VillagerOptimizer
- 17
UTF-8
@@ -32,7 +35,7 @@
org.apache.maven.plugins
maven-shade-plugin
- 3.5.0
+ 3.5.1
package
@@ -41,6 +44,7 @@
false
+ ${project.parent.artifactId}-${project.parent.version}--${project.artifactId}
*:*
@@ -65,12 +69,8 @@
- papermc-repo
- https://repo.papermc.io/repository/maven-public/
-
-
- sonatype
- https://oss.sonatype.org/content/groups/public/
+ papermc
+ https://papermc.io/repo/repository/maven-public/
configmaster-repo
@@ -79,18 +79,21 @@
+
- io.papermc.paper
- paper-api
- 1.20.1-R0.1-SNAPSHOT
- provided
+ org.bstats
+ bstats-bukkit
+ 3.0.2
+ compile
+
com.github.thatsmusic99
ConfigurationMaster-API
v2.0.0-rc.1
compile
+
com.github.ben-manes.caffeine
caffeine