create 1.16.5 module

This commit is contained in:
xGinko 2023-10-01 13:07:32 +02:00
parent af63b38455
commit 1777c643ec
71 changed files with 3083 additions and 17 deletions

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>me.xginko.VillagerOptimizer</groupId>
<artifactId>VillagerOptimizer</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>1.16.5</artifactId>
<name>${project.parent.artifactId}-${project.parent.version}--${project.artifactId}</name>
<packaging>jar</packaging>
<properties>
<java.version>16</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>me.xginko.villageroptimizer.bstats</shadedPattern>
</relocation>
</relocations>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
<repository>
<id>configmaster-repo</id>
<url>https://ci.pluginwiki.us/plugin/repository/everything/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.destroystokyo.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.16.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<version>4.14.0</version>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>4.14.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -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());
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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", "<green>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);
}
}

View File

@ -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<Villager.Profession> 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<Villager> unoptimized_villagers = new ArrayList<>();
List<Villager> 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;
}
}

View File

@ -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);
}
}
}

View File

@ -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)));
}
}
}
}
}

View File

@ -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<Material> 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());
}
}

View File

@ -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<String> 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 + "'");
}
}
}
}

View File

@ -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;
};
}
}

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>me.xginko.VillagerOptimizer</groupId>
<artifactId>VillagerOptimizer</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>1.20.2</artifactId>
<name>${project.parent.artifactId}-${project.parent.version}--${project.artifactId}</name>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>me.xginko.villageroptimizer.bstats</shadedPattern>
</relocation>
</relocations>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
<repository>
<id>configmaster-repo</id>
<url>https://ci.pluginwiki.us/plugin/repository/everything/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.20.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<UUID, WrappedVillager> villagerCache;
VillagerCache(long expireAfterWriteSeconds) {
this.villagerCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(expireAfterWriteSeconds)).build();
}
public @NotNull ConcurrentMap<UUID, WrappedVillager> 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;
}
}

View File

@ -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<String, LanguageCache> 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<String> getDefaultLanguageFiles() {
Set<String> 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;
}
}

View File

@ -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);
}

View File

@ -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<VillagerOptimizerCommand> 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);
}

View File

@ -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<String> 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<String> 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;
}
}

View File

@ -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<String> 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<String> 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;
}
}

View File

@ -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<SubCommand> subCommands = new ArrayList<>(3);
private final List<String> 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<String> 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 <blockradius>").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 <blockradius>").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));
}
}

View File

@ -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);
}
}
}

View File

@ -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<String> getList(@NotNull String path, @NotNull List<String> def, @NotNull String comment) {
config.addDefault(path, def, comment);
return config.getStringList(path);
}
public @NotNull List<String> getList(@NotNull String path, @NotNull List<String> def) {
config.addDefault(path, def);
return config.getStringList(path);
}
public @NotNull ConfigSection getConfigSection(@NotNull String path, @NotNull Map<String, Object> 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<String, Object> 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);
}
}

View File

@ -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<Component> 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",
"<red>You don't have permission to use this command.");
this.trades_restocked = getListTranslation("messages.trades-restocked",
List.of("<green>All trades have been restocked! Next restock in %time%"));
this.optimize_for_trading = getListTranslation("messages.optimize-to-trade",
List.of("<red>You need to optimize this villager before you can trade with it."));
this.villager_leveling_up = getListTranslation("messages.villager-leveling-up",
List.of("<yellow>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("<green>Successfully optimized villager by using a nametag."));
this.nametag_on_optimize_cooldown = getListTranslation("messages.nametag.optimize-on-cooldown",
List.of("<gray>You need to wait %time% until you can optimize this villager again."));
this.nametag_unoptimize_success = getListTranslation("messages.nametag.unoptimize-success",
List.of("<green>Successfully unoptimized villager by using a nametag."));
// Block
this.block_optimize_success = getListTranslation("messages.block.optimize-success",
List.of("<green>%villagertype% villager successfully optimized using block %blocktype%."));
this.block_on_optimize_cooldown = getListTranslation("messages.block.optimize-on-cooldown",
List.of("<gray>You need to wait %time% until you can optimize this villager again."));
this.block_unoptimize_success = getListTranslation("messages.block.unoptimize-success",
List.of("<green>Successfully unoptimized %villagertype% villager by removing %blocktype%."));
// Workstation
this.workstation_optimize_success = getListTranslation("messages.workstation.optimize-success",
List.of("<green>%villagertype% villager successfully optimized using workstation %workstation%."));
this.workstation_on_optimize_cooldown = getListTranslation("messages.workstation.optimize-on-cooldown",
List.of("<gray>You need to wait %time% until you can optimize this villager again."));
this.workstation_unoptimize_success = getListTranslation("messages.workstation.unoptimize-success",
List.of("<green>Successfully unoptimized %villagertype% villager by removing workstation block %workstation%."));
// Command
this.command_optimize_success = getListTranslation("messages.command.optimize-success",
List.of("<green>Successfully optimized %amount% villager(s) in a radius of %radius% blocks."));
this.command_radius_limit_exceed = getListTranslation("messages.command.radius-limit-exceed",
List.of("<red>The radius you entered exceeds the limit of %distance% blocks."));
this.command_optimize_fail = getListTranslation("messages.command.optimize-fail",
List.of("<gray>%amount% villagers couldn't be optimized because they have recently been optimized."));
this.command_unoptimize_success = getListTranslation("messages.command.unoptimize-success",
List.of("<green>Successfully unoptimized %amount% villager(s) in a radius of %radius% blocks."));
this.command_specify_radius = getListTranslation("messages.command.specify-radius",
List.of("<red>Please specify a radius."));
this.command_radius_invalid = getListTranslation("messages.command.radius-invalid",
List.of("<red>The radius you entered is not a valid number. Try again."));
this.command_no_villagers_nearby = getListTranslation("messages.command.no-villagers-nearby",
List.of("<gray>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<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation) {
lang.addDefault(path, defaultTranslation);
return lang.getStringList(path).stream().map(miniMessage::deserialize).toList();
}
public @NotNull List<Component> getListTranslation(@NotNull String path, @NotNull List<String> defaultTranslation, @NotNull String comment) {
lang.addDefault(path, defaultTranslation, comment);
return lang.getStringList(path).stream().map(miniMessage::deserialize).toList();
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
package me.xginko.villageroptimizer.enums;
public enum OptimizationType {
COMMAND,
NAMETAG,
WORKSTATION,
BLOCK,
NONE
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<VillagerOptimizerModule> 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();
});
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -56,7 +56,7 @@ public class PreventVillagerDamage implements VillagerOptimizerModule, Listener
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onDamageReceive(EntityDamageByEntityEvent event) { private void onDamageByEntity(EntityDamageByEntityEvent event) {
if ( if (
event.getEntityType().equals(EntityType.VILLAGER) event.getEntityType().equals(EntityType.VILLAGER)
&& villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized() && villagerCache.getOrAdd((Villager) event.getEntity()).isOptimized()
@ -79,7 +79,7 @@ public class PreventVillagerDamage implements VillagerOptimizerModule, Listener
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onDamageReceive(EntityDamageByBlockEvent event) { private void onDamageByBlock(EntityDamageByBlockEvent event) {
if ( if (
block block
&& event.getEntityType().equals(EntityType.VILLAGER) && event.getEntityType().equals(EntityType.VILLAGER)

View File

@ -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);
}
}
}

View File

@ -68,7 +68,7 @@ public class LevelVillagers implements VillagerOptimizerModule, Listener {
if (wVillager.canLevelUp(cooldown)) { if (wVillager.canLevelUp(cooldown)) {
if (wVillager.calculateLevel() > villager.getVillagerLevel()) { if (wVillager.calculateLevel() > villager.getVillagerLevel()) {
villager.getScheduler().run(plugin, enableAI -> { 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); villager.setAware(true);
}, null); }, null);
villager.getScheduler().runDelayed(plugin, disableAI -> { villager.getScheduler().runDelayed(plugin, disableAI -> {

View File

@ -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());
}
}
}

View File

@ -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);
}
}
}

View File

@ -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.");
}
}

View File

@ -0,0 +1,44 @@
messages:
no-permission: "<red>You don't have permission to use this command."
optimize-to-trade:
- "<red>You need to optimize this villager before you can trade with it."
trades-restocked:
- "<green>All trades have been restocked! Next restock in %time%."
villager-leveling-up:
- "<yellow>Villager is currently leveling up! You can use the villager again in %time%"
nametag:
optimize-success:
- "<green>Successfully optimized villager by using a nametag."
optimize-on-cooldown:
- "<gray>You need to wait %time% until you can optimize this villager again."
unoptimize-success:
- "<green>Successfully unoptimized villager by using a nametag."
block:
optimize-success:
- "<green>%vil_profession% villager successfully optimized using block %blocktype%."
optimize-on-cooldown:
- "<gray>You need to wait %time% until you can optimize this villager again."
unoptimize-success:
- "<green>Successfully unoptimized %vil_profession% villager by removing %blocktype%."
workstation:
optimize-success:
- "<green>%vil_profession% villager successfully optimized using workstation block %blocktype%."
optimize-on-cooldown:
- "<gray>You need to wait %time% until you can optimize this villager again."
unoptimize-success:
- "<green>Successfully unoptimized villager by removing workstation block %blocktype%."
command:
optimize-success:
- "<green>Successfully optimized %amount% villager(s) in a radius of %radius% blocks."
optimize-fail:
- "<gray>%amount% villagers couldn't be optimized because they have recently been optimized."
radius-limit-exceed:
- "<red>The radius you entered exceeds the limit of %distance% blocks."
unoptimize-success:
- "<green>Successfully unoptimized %amount% villager(s) in a radius of %radius% blocks."
specify-radius:
- "<red>Please specify a radius."
radius-invalid:
- "<red>The radius you entered is not a valid number. Try again."
no-villagers-nearby:
- "<gray>Couldn't find any employed villagers within a radius of %radius%."

View File

@ -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 <blockradius>
description: Optmize villagers in a radius around you
aliases:
- optvils
unoptimizevillagers:
usage: /unoptimizevillagers <blockradius>
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

31
pom.xml
View File

@ -4,17 +4,20 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>me.xginko</groupId> <groupId>me.xginko.VillagerOptimizer</groupId>
<artifactId>VillagerOptimizer</artifactId> <artifactId>VillagerOptimizer</artifactId>
<version>1.0.0</version> <version>1.0.0</version>
<packaging>jar</packaging> <modules>
<module>VillagerOptimizer-1.20.2</module>
<module>VillagerOptimizer-1.16.5</module>
</modules>
<packaging>pom</packaging>
<name>VillagerOptimizer</name> <name>VillagerOptimizer</name>
<description>Combat heavy villager lag by letting players optimize their trading halls.</description> <description>Combat heavy villager lag by letting players optimize their trading halls.</description>
<url>https://github.com/xGinko/VillagerOptimizer</url> <url>https://github.com/xGinko/VillagerOptimizer</url>
<properties> <properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@ -32,7 +35,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version> <version>3.5.1</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
@ -41,6 +44,7 @@
</goals> </goals>
<configuration> <configuration>
<createDependencyReducedPom>false</createDependencyReducedPom> <createDependencyReducedPom>false</createDependencyReducedPom>
<finalName>${project.parent.artifactId}-${project.parent.version}--${project.artifactId}</finalName>
<filters> <filters>
<filter> <filter>
<artifact>*:*</artifact> <artifact>*:*</artifact>
@ -65,12 +69,8 @@
<repositories> <repositories>
<repository> <repository>
<id>papermc-repo</id> <id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url> <url>https://papermc.io/repo/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository> </repository>
<repository> <repository>
<id>configmaster-repo</id> <id>configmaster-repo</id>
@ -79,18 +79,21 @@
</repositories> </repositories>
<dependencies> <dependencies>
<!-- Bukkit bStats -->
<dependency> <dependency>
<groupId>io.papermc.paper</groupId> <groupId>org.bstats</groupId>
<artifactId>paper-api</artifactId> <artifactId>bstats-bukkit</artifactId>
<version>1.20.1-R0.1-SNAPSHOT</version> <version>3.0.2</version>
<scope>provided</scope> <scope>compile</scope>
</dependency> </dependency>
<!-- Enhanced config.yml manager -->
<dependency> <dependency>
<groupId>com.github.thatsmusic99</groupId> <groupId>com.github.thatsmusic99</groupId>
<artifactId>ConfigurationMaster-API</artifactId> <artifactId>ConfigurationMaster-API</artifactId>
<version>v2.0.0-rc.1</version> <version>v2.0.0-rc.1</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<!-- Caching -->
<dependency> <dependency>
<groupId>com.github.ben-manes.caffeine</groupId> <groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId> <artifactId>caffeine</artifactId>