diff --git a/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java b/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java index bb389dc..476c4a5 100644 --- a/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java +++ b/src/main/java/me/xginko/villageroptimizer/WrappedVillager.java @@ -4,7 +4,9 @@ import me.xginko.villageroptimizer.enums.Keyring; import me.xginko.villageroptimizer.enums.OptimizationType; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Location; import org.bukkit.entity.Villager; +import org.bukkit.entity.memory.MemoryKey; import org.bukkit.inventory.MerchantRecipe; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; @@ -13,16 +15,18 @@ import org.jetbrains.annotations.Nullable; import java.util.concurrent.TimeUnit; +@SuppressWarnings("ALL") public final class WrappedVillager { private final @NotNull Villager villager; private final @NotNull PersistentDataContainer dataContainer; + private @Nullable CachedJobSite cachedJobSite; private final boolean parseOther; WrappedVillager(@NotNull Villager villager) { this.villager = villager; this.dataContainer = villager.getPersistentDataContainer(); - this.parseOther = me.xginko.villageroptimizer.VillagerOptimizer.getConfiguration().support_other_plugins; + this.parseOther = VillagerOptimizer.getConfiguration().support_other_plugins; } /** @@ -83,19 +87,31 @@ public final class WrappedVillager { /** * @param type OptimizationType the villager should be set to. */ - public void setOptimizationType(OptimizationType type) { - me.xginko.villageroptimizer.VillagerOptimizer.getFoliaLib().getImpl().runAtEntityTimer(villager, setOptimization -> { + public void setOptimizationType(final OptimizationType type) { + VillagerOptimizer.getFoliaLib().getImpl().runAtEntityTimer(villager, setOptimization -> { // Keep repeating task until villager is no longer trading with a player if (villager.isTrading()) return; - if (type.equals(OptimizationType.NONE) && isOptimized()) { - if (!parseOther || isOptimized(Keyring.Spaces.VillagerOptimizer)) - dataContainer.remove(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey()); - villager.setAware(true); - villager.setAI(true); // Done for stability so villager is guaranteed to wake up - } else { - dataContainer.set(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING, type.name()); - villager.setAware(false); + switch (type) { + case NAMETAG, COMMAND, BLOCK, WORKSTATION -> { + dataContainer.set(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING, type.name()); + villager.setAware(false); + } + case NONE -> { + if (isOptimized(Keyring.Spaces.VillagerOptimizer)) { + dataContainer.remove(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey()); + } + if (parseOther) { + if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey(), PersistentDataType.STRING)) + dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_ANY.getKey()); + if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey(), PersistentDataType.STRING)) + dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_WORKSTATION.getKey()); + if (dataContainer.has(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey(), PersistentDataType.STRING)) + dataContainer.remove(Keyring.AntiVillagerLag.OPTIMIZED_BLOCK.getKey()); + } + villager.setAware(true); + villager.setAI(true); + } } // End repeating task once logic is finished @@ -230,7 +246,9 @@ public final class WrappedVillager { } public long getRestockCooldownMillis(final long cooldown_millis) { - return dataContainer.has(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis; + if (dataContainer.has(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG)) + return villager.getWorld().getFullTime() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_RESTOCK.getKey(), PersistentDataType.LONG) + cooldown_millis); + return cooldown_millis; } /** @@ -277,11 +295,15 @@ public final class WrappedVillager { * @return The time of the in-game world when the entity was last leveled up. */ public long getLastLevelUpTime() { - return dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG) ? dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG) : 0L; + if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG)) + return dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG); + return 0L; } public long getLevelCooldownMillis(final long cooldown_millis) { - return dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG) ? (villager.getWorld().getFullTime() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG) + cooldown_millis)) : cooldown_millis; + if (dataContainer.has(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG)) + return villager.getWorld().getFullTime() - (dataContainer.get(Keyring.VillagerOptimizer.LAST_LEVELUP.getKey(), PersistentDataType.LONG) + cooldown_millis); + return cooldown_millis; } public void memorizeName(final Component customName) { @@ -289,10 +311,40 @@ public final class WrappedVillager { } public @Nullable Component getMemorizedName() { - return dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING) ? MiniMessage.miniMessage().deserialize(dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING)) : null; + if (dataContainer.has(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING)) + return MiniMessage.miniMessage().deserialize(dataContainer.get(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey(), PersistentDataType.STRING)); + return null; } public void forgetName() { dataContainer.remove(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey()); } + + private static class CachedJobSite { + private @Nullable Location jobSite; + private long lastRefresh; + private CachedJobSite(Villager villager) { + this.jobSite = villager.getMemory(MemoryKey.JOB_SITE); + this.lastRefresh = System.currentTimeMillis(); + } + private @Nullable Location getJobSite(Villager villager) { + final long now = System.currentTimeMillis(); + if (now - lastRefresh > 1000L) { + this.jobSite = villager.getMemory(MemoryKey.JOB_SITE); + this.lastRefresh = now; + } + return jobSite; + } + } + + public @Nullable Location getJobSite() { + if (cachedJobSite == null) + cachedJobSite = new CachedJobSite(villager); + return cachedJobSite.getJobSite(villager); + } + + public boolean canLooseProfession() { + // A villager with a level of 1 and no trading experience is liable to lose its profession. + return villager.getVillagerLevel() <= 1 && villager.getVillagerExperience() <= 0; + } } \ No newline at end of file diff --git a/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java b/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java index a633757..6dfa75a 100644 --- a/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java +++ b/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/DisableSubCmd.java @@ -28,15 +28,16 @@ public class DisableSubCmd extends SubCommand { @Override public void perform(CommandSender sender, String[] args) { - if (sender.hasPermission(Commands.DISABLE.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 { + if (!sender.hasPermission(Commands.DISABLE.get())) { sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission); + return; } + + 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)); } } \ No newline at end of file diff --git a/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java b/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java index afa36cd..b442866 100644 --- a/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java +++ b/src/main/java/me/xginko/villageroptimizer/commands/villageroptimizer/subcommands/ReloadSubCmd.java @@ -27,14 +27,15 @@ public class ReloadSubCmd extends SubCommand { @Override public void perform(CommandSender sender, String[] args) { - if (sender.hasPermission(Commands.RELOAD.get())) { - sender.sendMessage(Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE)); - VillagerOptimizer.getFoliaLib().getImpl().runNextTick(reload -> { // Reload in sync with the server - VillagerOptimizer.getInstance().reloadPlugin(); - sender.sendMessage(Component.text("Reload complete.").color(NamedTextColor.GREEN)); - }); - } else { + if (!sender.hasPermission(Commands.RELOAD.get())) { sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission); + return; } + + sender.sendMessage(Component.text("Reloading VillagerOptimizer...").color(NamedTextColor.WHITE)); + VillagerOptimizer.getFoliaLib().getImpl().runNextTick(reload -> { // Reload in sync with the server + VillagerOptimizer.getInstance().reloadPlugin(); + sender.sendMessage(Component.text("Reload complete.").color(NamedTextColor.GREEN)); + }); } } \ No newline at end of file diff --git a/src/main/java/me/xginko/villageroptimizer/enums/permissions/Commands.java b/src/main/java/me/xginko/villageroptimizer/enums/permissions/Commands.java index 6c7acf9..e51f294 100644 --- a/src/main/java/me/xginko/villageroptimizer/enums/permissions/Commands.java +++ b/src/main/java/me/xginko/villageroptimizer/enums/permissions/Commands.java @@ -5,11 +5,11 @@ import org.bukkit.permissions.PermissionDefault; public enum Commands { VERSION(new Permission("villageroptimizer.cmd.version", - "Permission get the plugin version", PermissionDefault.FALSE)), + "Permission get the plugin version", PermissionDefault.OP)), RELOAD(new Permission("villageroptimizer.cmd.reload", - "Permission to reload the plugin config", PermissionDefault.FALSE)), + "Permission to reload the plugin config", PermissionDefault.OP)), DISABLE(new Permission("villageroptimizer.cmd.disable", - "Permission to disable the plugin", PermissionDefault.FALSE)), + "Permission to disable the plugin", PermissionDefault.OP)), OPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.optimize", "Permission to optimize villagers in a radius", PermissionDefault.TRUE)), UNOPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.unoptimize", diff --git a/src/main/java/me/xginko/villageroptimizer/utils/ExpiringSet.java b/src/main/java/me/xginko/villageroptimizer/models/ExpiringSet.java similarity index 94% rename from src/main/java/me/xginko/villageroptimizer/utils/ExpiringSet.java rename to src/main/java/me/xginko/villageroptimizer/models/ExpiringSet.java index 0848a7f..a5e44ee 100644 --- a/src/main/java/me/xginko/villageroptimizer/utils/ExpiringSet.java +++ b/src/main/java/me/xginko/villageroptimizer/models/ExpiringSet.java @@ -1,4 +1,4 @@ -package me.xginko.villageroptimizer.utils; +package me.xginko.villageroptimizer.models; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; diff --git a/src/main/java/me/xginko/villageroptimizer/modules/gameplay/EnableLeashingVillagers.java b/src/main/java/me/xginko/villageroptimizer/modules/gameplay/EnableLeashingVillagers.java index 24c515d..7d2cea2 100644 --- a/src/main/java/me/xginko/villageroptimizer/modules/gameplay/EnableLeashingVillagers.java +++ b/src/main/java/me/xginko/villageroptimizer/modules/gameplay/EnableLeashingVillagers.java @@ -4,7 +4,7 @@ import me.xginko.villageroptimizer.VillagerCache; import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.config.Config; import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; -import me.xginko.villageroptimizer.utils.ExpiringSet; +import me.xginko.villageroptimizer.models.ExpiringSet; import org.bukkit.Material; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; @@ -58,24 +58,23 @@ public class EnableLeashingVillagers implements VillagerOptimizerModule, Listene @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) private void onLeash(PlayerInteractEntityEvent event) { if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return; - - Player player = event.getPlayer(); + final Player player = event.getPlayer(); if (!player.getInventory().getItem(event.getHand()).getType().equals(Material.LEAD)) return; Villager villager = (Villager) event.getRightClicked(); if (villager.isLeashed()) { - // If player clicked leashed villager, unleash. + // If leash holder clicked leashed villager, unleash. try { if (villager.getLeashHolder().getUniqueId().equals(player.getUniqueId())) { villager.setLeashHolder(null); villagersThatShouldntOpenTradeView.add(villager.getUniqueId()); } } catch (IllegalStateException ignored) { - // This shouldn't throw because we check LivingEntity#isLeashed(), - // but if for some reason it does, we catch it. + // This shouldn't throw because we check LivingEntity#isLeashed(), but if for some reason it does, we catch it. } - // Otherwise do not continue if already leashed + + // Otherwise do nothing. There should only ever be one leash holder. return; } diff --git a/src/main/java/me/xginko/villageroptimizer/modules/gameplay/UnoptimizeOnJobLoose.java b/src/main/java/me/xginko/villageroptimizer/modules/gameplay/UnoptimizeOnJobLoose.java index 433fbb7..364018c 100644 --- a/src/main/java/me/xginko/villageroptimizer/modules/gameplay/UnoptimizeOnJobLoose.java +++ b/src/main/java/me/xginko/villageroptimizer/modules/gameplay/UnoptimizeOnJobLoose.java @@ -33,7 +33,7 @@ public class UnoptimizeOnJobLoose implements VillagerOptimizerModule, Listener { @Override public boolean shouldEnable() { return VillagerOptimizer.getConfiguration().getBoolean("gameplay.unoptimize-on-job-loose.enable", true, - "Villagers that get their jobs reset will become unoptimized again. Highly recommended to leave on."); + "Villagers that get their jobs reset will become unoptimized again."); } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) diff --git a/src/main/java/me/xginko/villageroptimizer/modules/optimization/OptimizeByWorkstation.java b/src/main/java/me/xginko/villageroptimizer/modules/optimization/OptimizeByWorkstation.java index cec550d..a646e44 100644 --- a/src/main/java/me/xginko/villageroptimizer/modules/optimization/OptimizeByWorkstation.java +++ b/src/main/java/me/xginko/villageroptimizer/modules/optimization/OptimizeByWorkstation.java @@ -3,7 +3,7 @@ package me.xginko.villageroptimizer.modules.optimization; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.tcoded.folialib.impl.ServerImplementation; -import io.papermc.paper.event.entity.EntityMoveEvent; +import com.tcoded.folialib.wrapper.task.WrappedTask; import me.xginko.villageroptimizer.VillagerCache; import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.WrappedVillager; @@ -22,44 +22,48 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.entity.Villager; -import org.bukkit.entity.memory.MemoryKey; 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 org.bukkit.event.entity.VillagerCareerChangeEvent; -import org.jetbrains.annotations.Nullable; +import org.bukkit.util.NumberConversions; import java.time.Duration; -import java.util.UUID; import java.util.concurrent.TimeUnit; public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener { private final ServerImplementation scheduler; private final VillagerCache villagerCache; - private final Cache cachedVillagerJobSites; - private final long cooldown_millis; - private final double search_radius; + private final Cache pending_optimizations; + private final long cooldown_millis, delay_millis, resettable_delay_millis; + private final double search_radius, search_radius_squared; private final boolean only_while_sneaking, log_enabled, notify_player; public OptimizeByWorkstation() { shouldEnable(); this.scheduler = VillagerOptimizer.getFoliaLib().getImpl(); this.villagerCache = VillagerOptimizer.getCache(); - this.cachedVillagerJobSites = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(2)).build(); - - // Broken and unfinished, in progress - Config config = VillagerOptimizer.getConfiguration(); config.master().addComment("optimization-methods.workstation-optimization.enable", """ When enabled, villagers that have a job and have been traded with at least once will become optimized,\s if near their workstation. If the workstation is broken, the villager will become unoptimized again."""); + this.delay_millis = Math.max(config.getInt("optimization-methods.workstation-optimization.delay.default-delay-in-ticks", 10, """ + The delay in ticks the plugin should wait before trying to optimize the closest villager on workstation place.\s + Gives the villager time to claim the placed workstation. Minimum delay is 1 Tick (Not recommended)"""), 1) * 50L; + this.resettable_delay_millis = Math.max(config.getInt("optimization-methods.workstation-optimization.delay.resettable-delay-in-ticks", 60, """ + The delay in ticks the plugin should wait before trying to optimize a villager that can loose its profession\s + by having their workstation destroyed.\s + Intended to fix issues while trade rolling."""), 1) * 50L; + this.pending_optimizations = Caffeine.newBuilder() + .expireAfterWrite(Duration.ofMillis(Math.max(resettable_delay_millis, delay_millis) + 500L)) + .build(); 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; + The closest unoptimized villager to the player will be optimized."""); + this.search_radius_squared = NumberConversions.square(search_radius); this.cooldown_millis = TimeUnit.SECONDS.toMillis( config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600, """ Cooldown in seconds until a villager can be optimized again using a workstation.\s @@ -87,92 +91,56 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener return VillagerOptimizer.getConfiguration().getBoolean("optimization-methods.workstation-optimization.enable", false); } - private @Nullable Location getJobSite(Villager villager) { - Location jobSite = cachedVillagerJobSites.getIfPresent(villager.getUniqueId()); - if (jobSite == null) { - jobSite = villager.getMemory(MemoryKey.JOB_SITE); - cachedVillagerJobSites.put(villager.getUniqueId(), jobSite); - } - return jobSite; - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - private void onVillagerMove(EntityMoveEvent event) { - if (!event.getEntityType().equals(EntityType.VILLAGER)) return; - - final Villager villager = (Villager) event.getEntity(); - - if (villager.getProfession().equals(Villager.Profession.NONE)) return; - if (CommonUtil.canLooseProfession(villager)) return; - - final Location jobSite = getJobSite(villager); - if (jobSite == null) return; - // Using distanceSquared is faster. 1*1=1 -> 1 block away from the workstation - if (!(villager.getLocation().distanceSquared(jobSite) <= 1)) return; - - WrappedVillager wrappedVillager = villagerCache.getOrAdd(villager); - - if (wrappedVillager.canOptimize(cooldown_millis)) { - wrappedVillager.setOptimizationType(OptimizationType.WORKSTATION); - } - } - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - private void onCareerChange(VillagerCareerChangeEvent event) { - if (!event.getReason().equals(VillagerCareerChangeEvent.ChangeReason.EMPLOYED)) return; - if (CommonUtil.canLooseProfession(event.getEntity())) return; - - WrappedVillager wrappedVillager = villagerCache.getOrAdd(event.getEntity()); - - if (!wrappedVillager.isOptimized() && wrappedVillager.canOptimize(cooldown_millis)) { - wrappedVillager.setOptimizationType(OptimizationType.WORKSTATION); - } - } - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) private void onBlockPlace(BlockPlaceEvent event) { - Block placed = event.getBlock(); - Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(placed.getType()); + final Block placed = event.getBlock(); + final Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(placed.getType()); if (workstationProfession.equals(Villager.Profession.NONE)) return; - - Player player = event.getPlayer(); + final Player player = event.getPlayer(); if (!player.hasPermission(Optimize.WORKSTATION.get())) return; if (only_while_sneaking && !player.isSneaking()) return; final Location workstationLoc = placed.getLocation().toCenterLocation(); - WrappedVillager villagerThatClaimedWorkstation = null; + WrappedVillager toOptimize = null; 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; - // Ignore villagers that haven't been locked into a profession yet, so we don't disturb trade rollers - if (CommonUtil.canLooseProfession(villager)) continue; - Location jobSite = getJobSite(villager); - if (jobSite == null) continue; - if (!workstationLoc.equals(jobSite.toBlockLocation())) continue; WrappedVillager wVillager = villagerCache.getOrAdd(villager); + final Location jobSite = wVillager.getJobSite(); + if (jobSite == null) continue; + if (jobSite.distanceSquared(workstationLoc) > search_radius_squared) continue; + if (wVillager.canOptimize(cooldown_millis)) { - villagerThatClaimedWorkstation = wVillager; + toOptimize = wVillager; break; } } - if (villagerThatClaimedWorkstation == null) return; + if (toOptimize == null) return; + WrappedVillager finalToOptimize = toOptimize; - if (villagerThatClaimedWorkstation.canOptimize(cooldown_millis) || player.hasPermission(Bypass.WORKSTATION_COOLDOWN.get())) { - VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(villagerThatClaimedWorkstation, OptimizationType.WORKSTATION, player, event.isAsynchronous()); + pending_optimizations.put(placed.getLocation(), scheduler.runAtLocationLater(workstationLoc, + () -> optimize(finalToOptimize, player, placed, event.isAsynchronous()), + toOptimize.canLooseProfession() ? resettable_delay_millis : delay_millis, + TimeUnit.MILLISECONDS)); + } + + private void optimize(WrappedVillager toOptimize, Player player, Block placed, boolean async) { + if (toOptimize.canOptimize(cooldown_millis) || player.hasPermission(Bypass.WORKSTATION_COOLDOWN.get())) { + VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(toOptimize, OptimizationType.WORKSTATION, player, async); if (!optimizeEvent.callEvent()) return; - villagerThatClaimedWorkstation.setOptimizationType(optimizeEvent.getOptimizationType()); - villagerThatClaimedWorkstation.saveOptimizeTime(); + toOptimize.setOptimizationType(optimizeEvent.getOptimizationType()); + toOptimize.saveOptimizeTime(); if (notify_player) { final TextReplacementConfig vilProfession = TextReplacementConfig.builder() .matchLiteral("%vil_profession%") - .replacement(villagerThatClaimedWorkstation.villager().getProfession().toString().toLowerCase()) + .replacement(toOptimize.villager().getProfession().toString().toLowerCase()) .build(); final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder() .matchLiteral("%workstation%") @@ -183,14 +151,16 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener .replaceText(placedWorkstation) )); } + if (log_enabled) - VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using workstation: '" + placed.getType().toString().toLowerCase() + "'"); + VillagerOptimizer.getLog().info(player.getName() + " optimized a villager using workstation: '" + + placed.getType().toString().toLowerCase() + "'"); } else { - CommonUtil.shakeHead(villagerThatClaimedWorkstation.villager()); + CommonUtil.shakeHead(toOptimize.villager()); if (notify_player) { final TextReplacementConfig timeLeft = TextReplacementConfig.builder() .matchLiteral("%time%") - .replacement(CommonUtil.formatTime(villagerThatClaimedWorkstation.getOptimizeCooldownMillis(cooldown_millis))) + .replacement(CommonUtil.formatTime(toOptimize.getOptimizeCooldownMillis(cooldown_millis))) .build(); VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line .replaceText(timeLeft) @@ -201,10 +171,14 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) private void onBlockBreak(BlockBreakEvent event) { - Block broken = event.getBlock(); - Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(broken.getType()); + final Block broken = event.getBlock(); + // Cancel any pending optimization for this block + WrappedTask pendingOpt = pending_optimizations.getIfPresent(broken.getLocation()); + if (pendingOpt != null) pendingOpt.cancel(); + + final Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(broken.getType()); if (workstationProfession.equals(Villager.Profession.NONE)) return; - Player player = event.getPlayer(); + final Player player = event.getPlayer(); if (!player.hasPermission(Optimize.WORKSTATION.get())) return; if (only_while_sneaking && !player.isSneaking()) return; @@ -218,9 +192,9 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener if (!villager.getProfession().equals(workstationProfession)) continue; WrappedVillager wVillager = villagerCache.getOrAdd(villager); - final double distance = entity.getLocation().distance(workstationLoc); + final double distance = entity.getLocation().distanceSquared(workstationLoc); - if (distance < closestDistance && wVillager.canOptimize(cooldown_millis)) { + if (distance < closestDistance && wVillager.isOptimized()) { closestOptimizedVillager = wVillager; closestDistance = distance; } diff --git a/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java b/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java index 07f9c0b..4192796 100644 --- a/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java +++ b/src/main/java/me/xginko/villageroptimizer/utils/CommonUtil.java @@ -60,9 +60,4 @@ public class CommonUtil { default -> Villager.Profession.NONE; }; } - - public static boolean canLooseProfession(@NotNull Villager villager) { - // A villager with a level of 1 and no trading experience is liable to lose its profession. - return villager.getVillagerLevel() <= 1 && villager.getVillagerExperience() <= 0; - } }