finish up improvement to workstation optimization

This commit is contained in:
xGinko 2024-02-08 22:09:03 +01:00
parent ed79060e7f
commit e14c212dff
9 changed files with 149 additions and 127 deletions

View File

@ -4,7 +4,9 @@ import me.xginko.villageroptimizer.enums.Keyring;
import me.xginko.villageroptimizer.enums.OptimizationType; import me.xginko.villageroptimizer.enums.OptimizationType;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Location;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.entity.memory.MemoryKey;
import org.bukkit.inventory.MerchantRecipe; import org.bukkit.inventory.MerchantRecipe;
import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType; import org.bukkit.persistence.PersistentDataType;
@ -13,16 +15,18 @@ import org.jetbrains.annotations.Nullable;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@SuppressWarnings("ALL")
public final class WrappedVillager { public final class WrappedVillager {
private final @NotNull Villager villager; private final @NotNull Villager villager;
private final @NotNull PersistentDataContainer dataContainer; private final @NotNull PersistentDataContainer dataContainer;
private @Nullable CachedJobSite cachedJobSite;
private final boolean parseOther; private final boolean parseOther;
WrappedVillager(@NotNull Villager villager) { WrappedVillager(@NotNull Villager villager) {
this.villager = villager; this.villager = villager;
this.dataContainer = villager.getPersistentDataContainer(); 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. * @param type OptimizationType the villager should be set to.
*/ */
public void setOptimizationType(OptimizationType type) { public void setOptimizationType(final OptimizationType type) {
me.xginko.villageroptimizer.VillagerOptimizer.getFoliaLib().getImpl().runAtEntityTimer(villager, setOptimization -> { VillagerOptimizer.getFoliaLib().getImpl().runAtEntityTimer(villager, setOptimization -> {
// Keep repeating task until villager is no longer trading with a player // Keep repeating task until villager is no longer trading with a player
if (villager.isTrading()) return; if (villager.isTrading()) return;
if (type.equals(OptimizationType.NONE) && isOptimized()) { switch (type) {
if (!parseOther || isOptimized(Keyring.Spaces.VillagerOptimizer)) case NAMETAG, COMMAND, BLOCK, WORKSTATION -> {
dataContainer.remove(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey()); dataContainer.set(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING, type.name());
villager.setAware(true); villager.setAware(false);
villager.setAI(true); // Done for stability so villager is guaranteed to wake up }
} else { case NONE -> {
dataContainer.set(Keyring.VillagerOptimizer.OPTIMIZATION_TYPE.getKey(), PersistentDataType.STRING, type.name()); if (isOptimized(Keyring.Spaces.VillagerOptimizer)) {
villager.setAware(false); 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 // End repeating task once logic is finished
@ -230,7 +246,9 @@ public final class WrappedVillager {
} }
public long getRestockCooldownMillis(final long cooldown_millis) { 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. * @return The time of the in-game world when the entity was last leveled up.
*/ */
public long getLastLevelUpTime() { 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) { 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) { public void memorizeName(final Component customName) {
@ -289,10 +311,40 @@ public final class WrappedVillager {
} }
public @Nullable Component getMemorizedName() { 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() { public void forgetName() {
dataContainer.remove(Keyring.VillagerOptimizer.LAST_OPTIMIZE_NAME.getKey()); 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;
}
} }

View File

@ -28,15 +28,16 @@ public class DisableSubCmd extends SubCommand {
@Override @Override
public void perform(CommandSender sender, String[] args) { public void perform(CommandSender sender, String[] args) {
if (sender.hasPermission(Commands.DISABLE.get())) { 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 {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission); 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));
} }
} }

View File

@ -27,14 +27,15 @@ public class ReloadSubCmd extends SubCommand {
@Override @Override
public void perform(CommandSender sender, String[] args) { public void perform(CommandSender sender, String[] args) {
if (sender.hasPermission(Commands.RELOAD.get())) { 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 {
sender.sendMessage(VillagerOptimizer.getLang(sender).no_permission); 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));
});
} }
} }

View File

@ -5,11 +5,11 @@ import org.bukkit.permissions.PermissionDefault;
public enum Commands { public enum Commands {
VERSION(new Permission("villageroptimizer.cmd.version", 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", 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", 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", OPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.optimize",
"Permission to optimize villagers in a radius", PermissionDefault.TRUE)), "Permission to optimize villagers in a radius", PermissionDefault.TRUE)),
UNOPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.unoptimize", UNOPTIMIZE_RADIUS(new Permission("villageroptimizer.cmd.unoptimize",

View File

@ -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.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;

View File

@ -4,7 +4,7 @@ import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.config.Config; import me.xginko.villageroptimizer.config.Config;
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule; 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.Material;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -58,24 +58,23 @@ public class EnableLeashingVillagers implements VillagerOptimizerModule, Listene
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onLeash(PlayerInteractEntityEvent event) { private void onLeash(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return; if (!event.getRightClicked().getType().equals(EntityType.VILLAGER)) return;
final Player player = event.getPlayer();
Player player = event.getPlayer();
if (!player.getInventory().getItem(event.getHand()).getType().equals(Material.LEAD)) return; if (!player.getInventory().getItem(event.getHand()).getType().equals(Material.LEAD)) return;
Villager villager = (Villager) event.getRightClicked(); Villager villager = (Villager) event.getRightClicked();
if (villager.isLeashed()) { if (villager.isLeashed()) {
// If player clicked leashed villager, unleash. // If leash holder clicked leashed villager, unleash.
try { try {
if (villager.getLeashHolder().getUniqueId().equals(player.getUniqueId())) { if (villager.getLeashHolder().getUniqueId().equals(player.getUniqueId())) {
villager.setLeashHolder(null); villager.setLeashHolder(null);
villagersThatShouldntOpenTradeView.add(villager.getUniqueId()); villagersThatShouldntOpenTradeView.add(villager.getUniqueId());
} }
} catch (IllegalStateException ignored) { } catch (IllegalStateException ignored) {
// This shouldn't throw because we check LivingEntity#isLeashed(), // This shouldn't throw because we check LivingEntity#isLeashed(), but if for some reason it does, we catch it.
// 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; return;
} }

View File

@ -33,7 +33,7 @@ public class UnoptimizeOnJobLoose implements VillagerOptimizerModule, Listener {
@Override @Override
public boolean shouldEnable() { public boolean shouldEnable() {
return VillagerOptimizer.getConfiguration().getBoolean("gameplay.unoptimize-on-job-loose.enable", true, 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) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)

View File

@ -3,7 +3,7 @@ package me.xginko.villageroptimizer.modules.optimization;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import com.tcoded.folialib.impl.ServerImplementation; 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.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer; import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager; import me.xginko.villageroptimizer.WrappedVillager;
@ -22,44 +22,48 @@ import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.entity.memory.MemoryKey;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.VillagerCareerChangeEvent; import org.bukkit.util.NumberConversions;
import org.jetbrains.annotations.Nullable;
import java.time.Duration; import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener { public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
private final ServerImplementation scheduler; private final ServerImplementation scheduler;
private final VillagerCache villagerCache; private final VillagerCache villagerCache;
private final Cache<UUID, Location> cachedVillagerJobSites; private final Cache<Location, WrappedTask> pending_optimizations;
private final long cooldown_millis; private final long cooldown_millis, delay_millis, resettable_delay_millis;
private final double search_radius; private final double search_radius, search_radius_squared;
private final boolean only_while_sneaking, log_enabled, notify_player; private final boolean only_while_sneaking, log_enabled, notify_player;
public OptimizeByWorkstation() { public OptimizeByWorkstation() {
shouldEnable(); shouldEnable();
this.scheduler = VillagerOptimizer.getFoliaLib().getImpl(); this.scheduler = VillagerOptimizer.getFoliaLib().getImpl();
this.villagerCache = VillagerOptimizer.getCache(); this.villagerCache = VillagerOptimizer.getCache();
this.cachedVillagerJobSites = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(2)).build();
// Broken and unfinished, in progress
Config config = VillagerOptimizer.getConfiguration(); Config config = VillagerOptimizer.getConfiguration();
config.master().addComment("optimization-methods.workstation-optimization.enable", """ 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 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."""); 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, """ 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 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( this.cooldown_millis = TimeUnit.SECONDS.toMillis(
config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600, """ config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600, """
Cooldown in seconds until a villager can be optimized again using a workstation.\s 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); 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) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) { private void onBlockPlace(BlockPlaceEvent event) {
Block placed = event.getBlock(); final Block placed = event.getBlock();
Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(placed.getType()); final Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(placed.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return; if (workstationProfession.equals(Villager.Profession.NONE)) return;
final Player player = event.getPlayer();
Player player = event.getPlayer();
if (!player.hasPermission(Optimize.WORKSTATION.get())) return; if (!player.hasPermission(Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return; if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = placed.getLocation().toCenterLocation(); final Location workstationLoc = placed.getLocation().toCenterLocation();
WrappedVillager villagerThatClaimedWorkstation = null; WrappedVillager toOptimize = null;
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) { for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue; if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity; Villager villager = (Villager) entity;
if (!villager.getProfession().equals(workstationProfession)) continue; 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); 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)) { if (wVillager.canOptimize(cooldown_millis)) {
villagerThatClaimedWorkstation = wVillager; toOptimize = wVillager;
break; break;
} }
} }
if (villagerThatClaimedWorkstation == null) return; if (toOptimize == null) return;
WrappedVillager finalToOptimize = toOptimize;
if (villagerThatClaimedWorkstation.canOptimize(cooldown_millis) || player.hasPermission(Bypass.WORKSTATION_COOLDOWN.get())) { pending_optimizations.put(placed.getLocation(), scheduler.runAtLocationLater(workstationLoc,
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(villagerThatClaimedWorkstation, OptimizationType.WORKSTATION, player, event.isAsynchronous()); () -> 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; if (!optimizeEvent.callEvent()) return;
villagerThatClaimedWorkstation.setOptimizationType(optimizeEvent.getOptimizationType()); toOptimize.setOptimizationType(optimizeEvent.getOptimizationType());
villagerThatClaimedWorkstation.saveOptimizeTime(); toOptimize.saveOptimizeTime();
if (notify_player) { if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder() final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%") .matchLiteral("%vil_profession%")
.replacement(villagerThatClaimedWorkstation.villager().getProfession().toString().toLowerCase()) .replacement(toOptimize.villager().getProfession().toString().toLowerCase())
.build(); .build();
final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder() final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%") .matchLiteral("%workstation%")
@ -183,14 +151,16 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
.replaceText(placedWorkstation) .replaceText(placedWorkstation)
)); ));
} }
if (log_enabled) 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 { } else {
CommonUtil.shakeHead(villagerThatClaimedWorkstation.villager()); CommonUtil.shakeHead(toOptimize.villager());
if (notify_player) { if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder() final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%") .matchLiteral("%time%")
.replacement(CommonUtil.formatTime(villagerThatClaimedWorkstation.getOptimizeCooldownMillis(cooldown_millis))) .replacement(CommonUtil.formatTime(toOptimize.getOptimizeCooldownMillis(cooldown_millis)))
.build(); .build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown.forEach(line -> player.sendMessage(line
.replaceText(timeLeft) .replaceText(timeLeft)
@ -201,10 +171,14 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) { private void onBlockBreak(BlockBreakEvent event) {
Block broken = event.getBlock(); final Block broken = event.getBlock();
Villager.Profession workstationProfession = CommonUtil.getWorkstationProfession(broken.getType()); // 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; if (workstationProfession.equals(Villager.Profession.NONE)) return;
Player player = event.getPlayer(); final Player player = event.getPlayer();
if (!player.hasPermission(Optimize.WORKSTATION.get())) return; if (!player.hasPermission(Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return; if (only_while_sneaking && !player.isSneaking()) return;
@ -218,9 +192,9 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
if (!villager.getProfession().equals(workstationProfession)) continue; if (!villager.getProfession().equals(workstationProfession)) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager); 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; closestOptimizedVillager = wVillager;
closestDistance = distance; closestDistance = distance;
} }

View File

@ -60,9 +60,4 @@ public class CommonUtil {
default -> Villager.Profession.NONE; 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;
}
} }