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

View File

@ -1,9 +1,6 @@
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.wrapper.task.WrappedTask;
import me.xginko.villageroptimizer.VillagerCache;
import me.xginko.villageroptimizer.VillagerOptimizer;
import me.xginko.villageroptimizer.WrappedVillager;
@ -19,8 +16,6 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import org.bukkit.Location;
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;
@ -29,19 +24,19 @@ 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.bukkit.util.NumberConversions;
import java.time.Duration;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener {
private final ServerImplementation scheduler;
private final VillagerCache villagerCache;
private final Cache<Location, WrappedTask> pending_optimizations;
private final long cooldown_millis, delay_millis, resettable_delay_millis;
private final double search_radius, search_radius_squared;
private final long cooldown_millis;
private final double search_radius;
private final int check_duration_ticks;
private final boolean only_while_sneaking, log_enabled, notify_player;
public OptimizeByWorkstation() {
@ -52,26 +47,18 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
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" +
"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.\n" +
"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\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.check_duration_ticks = Math.max(config.getInt("optimization-methods.workstation-optimization.check-linger-duration-ticks", 100,
"After a workstation has been placed, the plugin will wait for the configured amount of time in ticks\n" +
"for a villager to claim that workstation. Not recommended to go below 100 ticks."), 1);
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 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,
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" +
"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,
"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,
"Sends players a message when they successfully optimized a villager.");
this.log_enabled = config.getBoolean("optimization-methods.workstation-optimization.log", false);
@ -93,134 +80,118 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
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)
private void onBlockPlace(BlockPlaceEvent event) {
final Block placed = event.getBlock();
final Villager.Profession workstationProfession = GenericUtil.getWorkstationProfession(placed.getType());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
if (workstationProfession == null) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
final Location workstationLoc = placed.getLocation().toCenterLocation();
WrappedVillager toOptimize = null;
final Location workstationLoc = placed.getLocation();
final AtomicInteger taskAliveTicks = new AtomicInteger(check_duration_ticks);
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 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;
WrappedVillager finalToOptimize = toOptimize;
pending_optimizations.put(placed.getLocation(), scheduler.runAtLocationLater(workstationLoc, () -> {
if (!finalToOptimize.canOptimize(cooldown_millis) && !player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
finalToOptimize.sayNo();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(GenericUtil.formatDuration(Duration.ofMillis(finalToOptimize.getOptimizeCooldownMillis(cooldown_millis))))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
}
scheduler.runAtLocationTimer(workstationLoc, lingeringRepeatingCheck -> {
if (taskAliveTicks.getAndAdd(-10) <= 0) {
lingeringRepeatingCheck.cancel();
return;
}
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
finalToOptimize,
OptimizationType.WORKSTATION,
player,
event.isAsynchronous()
);
final Iterator<WrappedVillager> villagerIterator = workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)
.stream()
.filter(villager -> villager.isAdult() && villager.getProfession() != Villager.Profession.NITWIT)
.map(villagerCache::getOrAdd)
.iterator();
if (!optimizeEvent.callEvent()) return;
while (villagerIterator.hasNext()) {
final WrappedVillager wrapped = villagerIterator.next();
if (wrapped.villager().getProfession() != workstationProfession) continue;
if (wrapped.getJobSite() == null) continue;
if (wrapped.getJobSite().distanceSquared(workstationLoc) > 1) continue;
finalToOptimize.setOptimizationType(optimizeEvent.getOptimizationType());
finalToOptimize.saveOptimizeTime();
if (!wrapped.canOptimize(cooldown_millis) && !player.hasPermission(Permissions.Bypass.WORKSTATION_COOLDOWN.get())) {
wrapped.sayNo();
if (notify_player) {
final TextReplacementConfig timeLeft = TextReplacementConfig.builder()
.matchLiteral("%time%")
.replacement(GenericUtil.formatDuration(Duration.ofMillis(wrapped.getOptimizeCooldownMillis(cooldown_millis))))
.build();
VillagerOptimizer.getLang(player.locale()).nametag_on_optimize_cooldown
.forEach(line -> KyoriUtil.sendMessage(player, line.replaceText(timeLeft)));
}
return;
}
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(finalToOptimize.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 -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedWorkstation)));
VillagerOptimizeEvent optimizeEvent = new VillagerOptimizeEvent(
wrapped,
OptimizationType.WORKSTATION,
player,
event.isAsynchronous()
);
if (!optimizeEvent.callEvent()) return;
wrapped.setOptimizationType(optimizeEvent.getOptimizationType());
wrapped.saveOptimizeTime();
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(wrapped.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 -> KyoriUtil.sendMessage(player, line.replaceText(vilProfession).replaceText(placedWorkstation)));
}
if (log_enabled) {
VillagerOptimizer.getLog().info(Component.text(player.getName() +
" optimized villager by workstation (" + placed.getType().toString().toLowerCase() + ") at " +
GenericUtil.formatLocation(wrapped.villager().getLocation())).color(GenericUtil.COLOR));
}
lingeringRepeatingCheck.cancel();
break;
}
if (log_enabled) {
VillagerOptimizer.getLog().info(Component.text(player.getName() +
" optimized villager by workstation (" + placed.getType().toString().toLowerCase() + ") at " +
GenericUtil.formatLocation(finalToOptimize.villager().getLocation())).color(GenericUtil.COLOR));
}
}, toOptimize.canLooseProfession() ? resettable_delay_millis : delay_millis, TimeUnit.MILLISECONDS));
}, 1L, 10L);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onBlockBreak(BlockBreakEvent event) {
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());
if (workstationProfession.equals(Villager.Profession.NONE)) return;
if (workstationProfession == null) return;
final Player player = event.getPlayer();
if (!player.hasPermission(Permissions.Optimize.WORKSTATION.get())) return;
if (only_while_sneaking && !player.isSneaking()) return;
final Location workstationLoc = broken.getLocation();
WrappedVillager closestOptimizedVillager = null;
WrappedVillager closestOptimized = 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;
for (Villager villager : workstationLoc.getNearbyEntitiesByType(Villager.class, search_radius)) {
if (!villager.getProfession().equals(workstationProfession)) continue;
final double distance = villager.getLocation().distanceSquared(workstationLoc);
if (distance >= closestDistance) continue;
WrappedVillager wVillager = villagerCache.getOrAdd(villager);
final double distance = entity.getLocation().distanceSquared(workstationLoc);
WrappedVillager wrapped = villagerCache.getOrAdd(villager);
if (distance < closestDistance && wVillager.isOptimized()) {
closestOptimizedVillager = wVillager;
if (wrapped.isOptimized()) {
closestOptimized = wrapped;
closestDistance = distance;
}
}
if (closestOptimizedVillager == null) return;
if (closestOptimized == null) return;
VillagerUnoptimizeEvent unOptimizeEvent = new VillagerUnoptimizeEvent(
closestOptimizedVillager,
closestOptimized,
player,
OptimizationType.WORKSTATION,
event.isAsynchronous()
@ -228,12 +199,12 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
if (!unOptimizeEvent.callEvent()) return;
closestOptimizedVillager.setOptimizationType(OptimizationType.NONE);
closestOptimized.setOptimizationType(OptimizationType.NONE);
if (notify_player) {
final TextReplacementConfig vilProfession = TextReplacementConfig.builder()
.matchLiteral("%vil_profession%")
.replacement(closestOptimizedVillager.villager().getProfession().toString().toLowerCase())
.replacement(closestOptimized.villager().getProfession().toString().toLowerCase())
.build();
final TextReplacementConfig brokenWorkstation = TextReplacementConfig.builder()
.matchLiteral("%workstation%")
@ -246,7 +217,7 @@ public class OptimizeByWorkstation implements VillagerOptimizerModule, Listener
if (log_enabled) {
VillagerOptimizer.getLog().info(Component.text(player.getName() +
" 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.entity.Villager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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) {
case BARREL:
return Villager.Profession.FISHERMAN;
@ -80,7 +81,7 @@ public class GenericUtil {
case STONECUTTER:
return Villager.Profession.MASON;
default:
return Villager.Profession.NONE;
return null;
}
}
}