change workstation optimization

This commit is contained in:
xGinko 2024-03-20 21:57:21 +01:00
parent 69881d7128
commit 6272383dd4
3 changed files with 95 additions and 142 deletions

View File

@ -3,6 +3,7 @@ package me.xginko.villageroptimizer;
import me.xginko.villageroptimizer.enums.Keyring; import me.xginko.villageroptimizer.enums.Keyring;
import me.xginko.villageroptimizer.enums.OptimizationType; import me.xginko.villageroptimizer.enums.OptimizationType;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.Sound; import org.bukkit.Sound;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.bukkit.entity.memory.MemoryKey; import org.bukkit.entity.memory.MemoryKey;
@ -19,7 +20,6 @@ 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) {
@ -316,30 +316,11 @@ public final class WrappedVillager {
villager.shakeHead(); villager.shakeHead();
} catch (NoSuchMethodError e) { } catch (NoSuchMethodError e) {
villager.getWorld().playSound(villager.getEyeLocation(), Sound.ENTITY_VILLAGER_NO, 1.0F, 1.0F); villager.getWorld().playSound(villager.getEyeLocation(), Sound.ENTITY_VILLAGER_NO, 1.0F, 1.0F);
} villager.getWorld().spawnParticle(Particle.CLOUD, villager.getEyeLocation(), 4);
}
private static class CachedJobSite {
private final @NotNull Villager villager;
private @Nullable Location jobSite;
private long lastRefresh;
private CachedJobSite(@NotNull Villager villager) {
this.villager = villager;
this.jobSite = getJobSite();
}
private @Nullable Location getJobSite() {
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() { public @Nullable Location getJobSite() {
if (cachedJobSite == null) return villager.getMemory(MemoryKey.JOB_SITE);
cachedJobSite = new CachedJobSite(villager);
return cachedJobSite.getJobSite();
} }
} }

View File

@ -1,9 +1,6 @@
package me.xginko.villageroptimizer.modules.optimization; 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 com.tcoded.folialib.impl.ServerImplementation;
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;
@ -19,8 +16,6 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
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.event.EventHandler; import org.bukkit.event.EventHandler;
@ -29,19 +24,19 @@ 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 java.time.Duration; import java.time.Duration;
import java.util.Iterator;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
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<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 int check_duration_ticks;
private final boolean only_while_sneaking, log_enabled, notify_player; private final boolean only_while_sneaking, log_enabled, notify_player;
public OptimizeByWorkstation() { public OptimizeByWorkstation() {
@ -52,26 +47,18 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
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,\n" + "When enabled, villagers that have a job and have been traded with at least once will become optimized,\n" +
"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, this.check_duration_ticks = Math.max(config.getInt("optimization-methods.workstation-optimization.check-linger-duration-ticks", 100,
"The delay in ticks the plugin should wait before trying to optimize the closest villager on workstation place.\n" + "After a workstation has been placed, the plugin will wait for the configured amount of time in ticks\n" +
"Gives the villager time to claim the placed workstation. Minimum delay is 1 Tick (Not recommended)"), 1) * 50L; "for a villager to claim that workstation. Not recommended to go below 100 ticks."), 1);
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\n" +
"by having their workstation destroyed.\n" +
"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.\n" + "The radius in blocks a villager can be away from the player when he places a workstation.\n" +
"The closest unoptimized villager to the player will be optimized."); "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, Math.max(1, config.getInt("optimization-methods.workstation-optimization.optimize-cooldown-seconds", 600,
"Cooldown in seconds until a villager can be optimized again using a workstation.\n" + "Cooldown in seconds until a villager can be optimized again using a workstation.\n" +
"Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.")); "Here for configuration freedom. Recommended to leave as is to not enable any exploitable behavior.")));
this.only_while_sneaking = config.getBoolean("optimization-methods.workstation-optimization.only-when-sneaking", true, this.only_while_sneaking = config.getBoolean("optimization-methods.workstation-optimization.only-when-sneaking", true,
"Only optimize/unoptimize by workstation when player is sneaking during place or break"); "Only optimize/unoptimize by workstation when player is sneaking during place or break. Useful for villager rolling.");
this.notify_player = config.getBoolean("optimization-methods.workstation-optimization.notify-player", true, this.notify_player = config.getBoolean("optimization-methods.workstation-optimization.notify-player", true,
"Sends players a message when they successfully optimized a villager."); "Sends players a message when they successfully optimized a villager.");
this.log_enabled = config.getBoolean("optimization-methods.workstation-optimization.log", false); this.log_enabled = config.getBoolean("optimization-methods.workstation-optimization.log", false);
@ -93,59 +80,43 @@ 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);
} }
// Place block -> Remember what and where
// Wait for villager to claim jobsite
// Do optimization on that villager
// Destroy workstation -> look for nearby villager that claimed it
// Do unoptimization immediately
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onCareerChange(VillagerCareerChangeEvent event) {
if (!event.getReason().equals(VillagerCareerChangeEvent.ChangeReason.EMPLOYED)) return;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockPlace(BlockPlaceEvent event) { private void onBlockPlace(BlockPlaceEvent event) {
final Block placed = event.getBlock(); final Block placed = event.getBlock();
final Villager.Profession workstationProfession = GenericUtil.getWorkstationProfession(placed.getType()); final Villager.Profession workstationProfession = GenericUtil.getWorkstationProfession(placed.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return; if (workstationProfession == null) return;
final Player player = event.getPlayer(); final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return; if (only_while_sneaking && !player.isSneaking()) return;
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
final Location workstationLoc = placed.getLocation().toCenterLocation(); final Location workstationLoc = placed.getLocation();
WrappedVillager toOptimize = null; final AtomicInteger taskAliveTicks = new AtomicInteger(check_duration_ticks);
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) { scheduler.runAtLocationTimer(workstationLoc, lingeringRepeatingCheck -> {
if (!entity.getType().equals(EntityType.VILLAGER)) continue; if (taskAliveTicks.getAndAdd(-10) <= 0) {
Villager villager = (Villager) entity; lingeringRepeatingCheck.cancel();
if (!villager.getProfession().equals(workstationProfession)) continue; return;
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)) {
toOptimize = wVillager;
break;
}
} }
if (toOptimize == null) return; final Iterator<WrappedVillager> villagerIterator = workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)
WrappedVillager finalToOptimize = toOptimize; .stream()
.filter(villager -> villager.isAdult() && villager.getProfession() != Villager.Profession.NITWIT)
.map(villagerCache::getOrAdd)
.iterator();
pending_optimizations.put(placed.getLocation(), scheduler.runAtLocationLater(workstationLoc, () -> { while (villagerIterator.hasNext()) {
if (!finalToOptimize.canOptimize(cooldown_millis) && !player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) { final WrappedVillager wrapped = villagerIterator.next();
finalToOptimize.sayNo(); if (wrapped.villager().getProfession() != workstationProfession) continue;
if (wrapped.getJobSite() == null) continue;
if (wrapped.getJobSite().distanceSquared(workstationLoc) > 1) continue;
if (!wrapped.canOptimize(cooldown_millis) && !player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
wrapped.sayNo();
if (notify_player) { if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder() final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%") .matchLiteral("%time%")
.replacement(GenericUtil.formatDuration(Duration.ofMillis(finalToOptimize.getOptimizeCooldownMillis(cooldown_millis)))) .replacement(GenericUtil.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown_millis))))
.build(); .build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft))); .forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
@ -154,7 +125,7 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
} }
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent( VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
finalToOptimize, wrapped,
OptimizationType.WORKSTATION, OptimizationType.WORKSTATION,
player, player,
event.isAsynchronous() event.isAsynchronous()
@ -162,13 +133,13 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
if (!optimizeEvent.callEvent()) return; if (!optimizeEvent.callEvent()) return;
finalToOptimize.setOptimizationType(optimizeEvent.getOptimizationType()); wrapped.setOptimizationType(optimizeEvent.getOptimizationType());
finalToOptimize.saveOptimizeTime(); wrapped.saveOptimizeTime();
if (notify_player) { if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder() final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%") .matchLiteral("%vil_profession%")
.replacement(finalToOptimize.villager().getProfession().toString().toLowerCase()) .replacement(wrapped.villager().getProfession().toString().toLowerCase())
.build(); .build();
final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder() final TextReplacementConfig placedWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%") .matchLiteral("%workstation%")
@ -181,46 +152,46 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
if (log_enabled) { if (log_enabled) {
VillagerOptimizer.getLog().info(Component.text(player.getName() + VillagerOptimizer.getLog().info(Component.text(player.getName() +
" optimized villager by workstation (" + placed.getType().toString().toLowerCase() + ") at " + " optimized villager by workstation (" + placed.getType().toString().toLowerCase() + ") at " +
GenericUtil.formatLocation(finalToOptimize.villager().getLocation())).color(GenericUtil.COLOR)); GenericUtil.formatLocation(wrapped.villager().getLocation())).color(GenericUtil.COLOR));
} }
}, toOptimize.canLooseProfession() ? resettable_delay_millis : delay_millis, TimeUnit.MILLISECONDS));
lingeringRepeatingCheck.cancel();
break;
}
}, 1L, 10L);
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) { private void onBlockBreak(BlockBreakEvent event) {
final Block broken = event.getBlock(); 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 = GenericUtil.getWorkstationProfession(broken.getType()); final Villager.Profession workstationProfession = GenericUtil.getWorkstationProfession(broken.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return; if (workstationProfession == null) return;
final Player player = event.getPlayer(); final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return; if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return; if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = broken.getLocation(); final Location workstationLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null; WrappedVillager closestOptimized = null;
double closestDistance = Double.MAX_VALUE; double closestDistance = Double.MAX_VALUE;
for (Entity entity : workstationLoc.getNearbyEntities(search_radius, search_radius, search_radius)) { for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
if (!entity.getType().equals(EntityType.VILLAGER)) continue;
Villager villager = (Villager) entity;
if (!villager.getProfession().equals(workstationProfession)) continue; if (!villager.getProfession().equals(workstationProfession)) continue;
final double distance = villager.getLocation().distanceSquared(workstationLoc);
if (distance >= closestDistance) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager); WrappedVillager wrapped = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distanceSquared(workstationLoc);
if (distance < closestDistance && wVillager.isOptimized()) { if (wrapped.isOptimized()) {
closestOptimizedVillager = wVillager; closestOptimized = wrapped;
closestDistance = distance; closestDistance = distance;
} }
} }
if (closestOptimizedVillager == null) return; if (closestOptimized == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent( VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
closestOptimizedVillager, closestOptimized,
player, player,
OptimizationType.WORKSTATION, OptimizationType.WORKSTATION,
event.isAsynchronous() event.isAsynchronous()
@ -228,12 +199,12 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
if (!unOptimizeEvent.callEvent()) return; if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimizationType(OptimizationType.NONE); closestOptimized.setOptimizationType(OptimizationType.NONE);
if (notify_player) { if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder() final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%") .matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase()) .replacement(closestOptimized.villager().getProfession().toString().toLowerCase())
.build(); .build();
final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder() final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%") .matchLiteral("%workstation%")
@ -246,7 +217,7 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
if (log_enabled) { if (log_enabled) {
VillagerOptimizer.getLog().info(Component.text(player.getName() + VillagerOptimizer.getLog().info(Component.text(player.getName() +
" unoptimized villager by workstation (" + broken.getType().toString().toLowerCase() + ") at " + " unoptimized villager by workstation (" + broken.getType().toString().toLowerCase() + ") at " +
GenericUtil.formatLocation(closestOptimizedVillager.villager().getLocation())).color(GenericUtil.COLOR)); GenericUtil.formatLocation(closestOptimized.villager().getLocation())).color(GenericUtil.COLOR));
} }
} }
} }

View File

@ -9,6 +9,7 @@ import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Villager; import org.bukkit.entity.Villager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Duration; import java.time.Duration;
@ -51,7 +52,7 @@ public class GenericUtil {
} }
} }
public static @NotNull Villager.Profession getWorkstationProfession(@NotNull Material workstation) { public static @Nullable Villager.Profession getWorkstationProfession(@NotNull Material workstation) {
switch (workstation) { switch (workstation) {
case BARREL: case BARREL:
return Villager.Profession.FISHERMAN; return Villager.Profession.FISHERMAN;
@ -80,7 +81,7 @@ public class GenericUtil {
case STONECUTTER: case STONECUTTER:
return Villager.Profession.MASON; return Villager.Profession.MASON;
default: default:
return Villager.Profession.NONE; return null;
} }
} }
} }