add experimental regional optimization
This commit is contained in:
parent
4019afe89b
commit
15fcd14e16
@ -20,6 +20,7 @@ public class LanguageCache {
|
||||
public final @NotNull List<Component> nametag_optimize_success, nametag_on_optimize_cooldown, nametag_unoptimize_success,
|
||||
block_optimize_success, block_on_optimize_cooldown, block_unoptimize_success,
|
||||
workstation_optimize_success, workstation_on_optimize_cooldown, workstation_unoptimize_success,
|
||||
activity_optimize_success,
|
||||
command_optimize_success, command_radius_limit_exceed, command_optimize_fail, command_unoptimize_success,
|
||||
command_specify_radius, command_radius_invalid, command_no_villagers_nearby,
|
||||
trades_restocked, optimize_for_trading, villager_leveling_up;
|
||||
@ -67,6 +68,10 @@ public class LanguageCache {
|
||||
"<gray>You need to wait %time% until you can optimize this villager again.");
|
||||
this.workstation_unoptimize_success = getListTranslation("messages.workstation.unoptimize-success",
|
||||
"<green>Successfully unoptimized %villagertype% villager by removing workstation block %blocktype%.");
|
||||
// Activity
|
||||
this.activity_optimize_success = getListTranslation("messages.activity.optimized-near-you",
|
||||
"<gray>%amount% villagers close to you were automatically optimized due to high activity.");
|
||||
|
||||
// Command
|
||||
this.command_optimize_success = getListTranslation("messages.command.optimize-success",
|
||||
"<green>Successfully optimized %amount% villager(s) in a radius of %radius% blocks.");
|
||||
|
@ -0,0 +1,240 @@
|
||||
package me.xginko.villageroptimizer.modules.optimization;
|
||||
|
||||
import com.cryptomorin.xseries.XEntityType;
|
||||
import com.destroystokyo.paper.event.entity.EntityPathfindEvent;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import me.xginko.villageroptimizer.modules.VillagerOptimizerModule;
|
||||
import me.xginko.villageroptimizer.struct.enums.OptimizationType;
|
||||
import me.xginko.villageroptimizer.struct.models.BlockRegion2D;
|
||||
import me.xginko.villageroptimizer.wrapper.WrappedVillager;
|
||||
import net.kyori.adventure.text.TextReplacementConfig;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityInteractEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class OptimizeByActivity extends VillagerOptimizerModule implements Listener {
|
||||
|
||||
protected static class RegionData {
|
||||
|
||||
public final BlockRegion2D region;
|
||||
public final AtomicInteger pathfindCount, entityInteractCount;
|
||||
public final AtomicBoolean regionBusy;
|
||||
|
||||
public RegionData(BlockRegion2D region) {
|
||||
this.region = region;
|
||||
this.pathfindCount = new AtomicInteger();
|
||||
this.entityInteractCount = new AtomicInteger();
|
||||
this.regionBusy = new AtomicBoolean(false);
|
||||
}
|
||||
}
|
||||
|
||||
private final Cache<BlockRegion2D, RegionData> regionDataCache;
|
||||
private final double checkRadius;
|
||||
private final int pathfindLimit, entityInteractLimit;
|
||||
private final boolean notifyPlayers, doLogging;
|
||||
|
||||
public OptimizeByActivity() {
|
||||
super("optimization-methods.regional-activity");
|
||||
config.master().addComment(configPath + ".enable",
|
||||
"Enable optimization by naming villagers to one of the names configured below.\n" +
|
||||
"Nametag optimized villagers will be unoptimized again when they are renamed to something else.");
|
||||
|
||||
this.checkRadius = config.getDouble(configPath + ".check-radius-blocks", 500.0,
|
||||
"The radius in blocks in which activity will be grouped together and measured.");
|
||||
this.regionDataCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofMillis(
|
||||
config.getInt(configPath + ".data-keep-time-millis", 10000,
|
||||
"The time in milliseconds before a region and its data will be expired\n" +
|
||||
"if no activity has been detected.\n" +
|
||||
"For proper functionality, needs to be at least as long as your pause time."))).build();
|
||||
|
||||
this.pathfindLimit = config.getInt(configPath + ".limits.pathfind-event", 150);
|
||||
this.entityInteractLimit = config.getInt(configPath + ".limits.interact-event", 50);
|
||||
|
||||
this.notifyPlayers = config.getBoolean(configPath + ".notify-players", true,
|
||||
"Sends players a message to any player near an auto-optimized villager.");
|
||||
this.doLogging = config.getBoolean(configPath + ".log", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
HandlerList.unregisterAll(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnable() {
|
||||
return config.getBoolean(configPath + ".enable", false);
|
||||
}
|
||||
|
||||
private @NotNull RegionData getRegionData(Location location) {
|
||||
return regionDataCache.get(getRegion(location), RegionData::new);
|
||||
}
|
||||
|
||||
private @NotNull BlockRegion2D getRegion(Location location) {
|
||||
// Find and return region containing this location
|
||||
for (Map.Entry<BlockRegion2D, RegionData> regionDataEntry : regionDataCache.asMap().entrySet()) {
|
||||
if (regionDataEntry.getKey().contains(location)) {
|
||||
return regionDataEntry.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
// Create and cache region if none exists
|
||||
BlockRegion2D region = BlockRegion2D.of(location.getWorld(), location.getX(), location.getZ(), checkRadius);
|
||||
regionDataCache.put(region, new RegionData(region));
|
||||
return region;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onEntityPathfind(EntityPathfindEvent event) {
|
||||
if (event.getEntityType() != XEntityType.VILLAGER.get()) return;
|
||||
|
||||
Location location = event.getEntity().getLocation();
|
||||
BlockRegion2D region2D = getRegion(location);
|
||||
RegionData regionData = getRegionData(location);
|
||||
|
||||
if (regionData.regionBusy.get() || regionData.pathfindCount.incrementAndGet() <= pathfindLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
regionData.regionBusy.set(true);
|
||||
|
||||
AtomicInteger optimizeCount = new AtomicInteger();
|
||||
Set<Player> playersWithinArea = new CopyOnWriteArraySet<>();
|
||||
|
||||
region2D.getEntities()
|
||||
.thenAccept(entities -> {
|
||||
for (Entity entity : entities) {
|
||||
scheduling.entitySpecificScheduler(entity).run(() -> {
|
||||
if (entity.getType() == XEntityType.VILLAGER.get()) {
|
||||
WrappedVillager wrappedVillager = wrapperCache.get((Villager) entity, WrappedVillager::new);
|
||||
|
||||
if (wrappedVillager.isOptimized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrappedVillager.setOptimizationType(OptimizationType.REGIONAL_ACTIVITY);
|
||||
optimizeCount.incrementAndGet();
|
||||
}
|
||||
|
||||
if (notifyPlayers && entity.getType() == XEntityType.PLAYER.get()) {
|
||||
playersWithinArea.add((Player) entity);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
})
|
||||
.thenRun(() -> {
|
||||
if (notifyPlayers) {
|
||||
TextReplacementConfig amount = TextReplacementConfig.builder()
|
||||
.matchLiteral("%amount%")
|
||||
.replacement(optimizeCount.toString())
|
||||
.build();
|
||||
|
||||
for (Player player : playersWithinArea) {
|
||||
VillagerOptimizer.scheduling().entitySpecificScheduler(player).run(() ->
|
||||
VillagerOptimizer.getLang(player.locale()).activity_optimize_success
|
||||
.forEach(line -> player.sendMessage(line.replaceText(amount))),
|
||||
null);
|
||||
}
|
||||
|
||||
playersWithinArea.clear();
|
||||
}
|
||||
|
||||
if (doLogging) {
|
||||
info( "Optimized " + optimizeCount.get() + " villagers in a radius of " + checkRadius +
|
||||
" blocks from center at x=" + regionData.region.getCenterX() + ", z=" + regionData.region.getCenterZ() +
|
||||
" in world " + location.getWorld().getName() +
|
||||
"because of too high pathfinding activity within the configured timeframe: " +
|
||||
regionData.pathfindCount + " (limit: " + pathfindLimit + ")");
|
||||
}
|
||||
|
||||
regionDataCache.invalidate(region2D);
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
private void onEntityInteract(EntityInteractEvent event) {
|
||||
if (event.getEntityType() != XEntityType.VILLAGER.get()) return;
|
||||
|
||||
Location location = event.getEntity().getLocation();
|
||||
BlockRegion2D region2D = getRegion(location);
|
||||
RegionData regionData = getRegionData(location);
|
||||
|
||||
if (regionData.regionBusy.get() || regionData.entityInteractCount.incrementAndGet() <= entityInteractLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
regionData.regionBusy.set(true);
|
||||
|
||||
AtomicInteger optimizeCount = new AtomicInteger();
|
||||
Set<Player> playersWithinArea = new CopyOnWriteArraySet<>();
|
||||
|
||||
region2D.getEntities()
|
||||
.thenAccept(entities -> {
|
||||
for (Entity entity : entities) {
|
||||
scheduling.entitySpecificScheduler(entity).run(() -> {
|
||||
if (entity.getType() == XEntityType.VILLAGER.get()) {
|
||||
WrappedVillager wrappedVillager = wrapperCache.get((Villager) entity, WrappedVillager::new);
|
||||
|
||||
if (wrappedVillager.isOptimized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrappedVillager.setOptimizationType(OptimizationType.REGIONAL_ACTIVITY);
|
||||
optimizeCount.incrementAndGet();
|
||||
}
|
||||
|
||||
if (notifyPlayers && entity.getType() == XEntityType.PLAYER.get()) {
|
||||
playersWithinArea.add((Player) entity);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
})
|
||||
.thenRun(() -> {
|
||||
if (notifyPlayers) {
|
||||
TextReplacementConfig amount = TextReplacementConfig.builder()
|
||||
.matchLiteral("%amount%")
|
||||
.replacement(optimizeCount.toString())
|
||||
.build();
|
||||
|
||||
for (Player player : playersWithinArea) {
|
||||
VillagerOptimizer.scheduling().entitySpecificScheduler(player).run(() ->
|
||||
VillagerOptimizer.getLang(player.locale()).activity_optimize_success
|
||||
.forEach(line -> player.sendMessage(line.replaceText(amount))),
|
||||
null);
|
||||
}
|
||||
|
||||
playersWithinArea.clear();
|
||||
}
|
||||
|
||||
if (doLogging) {
|
||||
info( "Optimized " + optimizeCount.get() + " villagers in a radius of " + checkRadius +
|
||||
" blocks from center at x=" + regionData.region.getCenterX() + ", z=" + regionData.region.getCenterZ() +
|
||||
" in world " + location.getWorld().getName() +
|
||||
"because of too many villagers interacting with objects within the configured timeframe: " +
|
||||
regionData.pathfindCount + " (limit: " + pathfindLimit + ")");
|
||||
}
|
||||
|
||||
regionDataCache.invalidate(region2D);
|
||||
});
|
||||
}
|
||||
}
|
@ -99,13 +99,13 @@ public class OptimizeByNametag extends VillagerOptimizerModule implements Listen
|
||||
|
||||
if (!optimizeEvent.callEvent()) return;
|
||||
|
||||
wrapped.setOptimizationType(optimizeEvent.getOptimizationType());
|
||||
wrapped.saveOptimizeTime();
|
||||
|
||||
if (!consume_nametag && player.getGameMode() == GameMode.SURVIVAL) {
|
||||
player.getInventory().addItem(usedItem.asOne());
|
||||
}
|
||||
|
||||
wrapped.setOptimizationType(optimizeEvent.getOptimizationType());
|
||||
wrapped.saveOptimizeTime();
|
||||
|
||||
if (notify_player) {
|
||||
VillagerOptimizer.getLang(player.locale()).nametag_optimize_success
|
||||
.forEach(line -> KyoriUtil.sendMessage(player, line));
|
||||
|
@ -1,6 +1,8 @@
|
||||
package me.xginko.villageroptimizer.struct.enums;
|
||||
|
||||
public enum OptimizationType {
|
||||
CHUNK_LIMIT,
|
||||
REGIONAL_ACTIVITY,
|
||||
COMMAND,
|
||||
NAMETAG,
|
||||
WORKSTATION,
|
||||
|
@ -0,0 +1,131 @@
|
||||
package me.xginko.villageroptimizer.struct.models;
|
||||
|
||||
import me.xginko.villageroptimizer.VillagerOptimizer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class BlockRegion2D {
|
||||
|
||||
private final UUID worldUID;
|
||||
private final double halfSideLength, centerX, centerZ;
|
||||
|
||||
/**
|
||||
* A square region on a minecraft world map.
|
||||
*
|
||||
* @param worldUID The UUID of the world this region is in.
|
||||
* @param centerX The X-axis of the center location on the map.
|
||||
* @param centerZ The Z-axis of the center location on the map.
|
||||
* @param halfSideLength Half the length of the square's side. Acts like a radius would on circular regions.
|
||||
*/
|
||||
public BlockRegion2D(UUID worldUID, double centerX, double centerZ, double halfSideLength) {
|
||||
this.worldUID = worldUID;
|
||||
this.centerX = centerX;
|
||||
this.centerZ = centerZ;
|
||||
this.halfSideLength = halfSideLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a square region on a minecraft world map.
|
||||
*
|
||||
* @param worldUID The UUID of the world this region is in.
|
||||
* @param centerX The X-axis of the center location on the map.
|
||||
* @param centerZ The Z-axis of the center location on the map.
|
||||
* @param halfSideLength Half the length of the square's side. Acts like a radius would on circular regions.
|
||||
*/
|
||||
public static BlockRegion2D of(UUID worldUID, double centerX, double centerZ, double halfSideLength) {
|
||||
return new BlockRegion2D(worldUID, centerX, centerZ, halfSideLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a square region on a minecraft world map.
|
||||
*
|
||||
* @param world The world this region is in.
|
||||
* @param centerX The X-axis of the center location on the map.
|
||||
* @param centerZ The Z-axis of the center location on the map.
|
||||
* @param halfSideLength Half the length of the square's side. Acts like a radius would on circular regions.
|
||||
*/
|
||||
public static BlockRegion2D of(World world, double centerX, double centerZ, double halfSideLength) {
|
||||
return BlockRegion2D.of(world.getUID(), centerX, centerZ, halfSideLength);
|
||||
}
|
||||
|
||||
public UUID getWorldUID() {
|
||||
return this.worldUID;
|
||||
}
|
||||
|
||||
public double getHalfSideLength() {
|
||||
return this.halfSideLength;
|
||||
}
|
||||
|
||||
public double getCenterX() {
|
||||
return this.centerX;
|
||||
}
|
||||
|
||||
public double getCenterZ() {
|
||||
return this.centerZ;
|
||||
}
|
||||
|
||||
public boolean contains(Location location) {
|
||||
if (!location.getWorld().getUID().equals(this.worldUID)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return location.getX() >= this.centerX - this.halfSideLength
|
||||
&& location.getX() <= this.centerX + this.halfSideLength
|
||||
&& location.getZ() >= this.centerZ - this.halfSideLength
|
||||
&& location.getZ() <= this.centerZ + this.halfSideLength;
|
||||
}
|
||||
|
||||
public CompletableFuture<Collection<Entity>> getEntities() {
|
||||
World world = Bukkit.getWorld(worldUID);
|
||||
|
||||
if (world == null) {
|
||||
// Only way I can imagine this happening would be if the server is using a world manager plugin and unloads
|
||||
// the world during an operation.
|
||||
// Since these plugins are rather common though, we will silently complete with an empty set instead of exceptionally.
|
||||
return CompletableFuture.completedFuture(Collections.emptySet());
|
||||
}
|
||||
|
||||
CompletableFuture<Collection<Entity>> future = new CompletableFuture<>();
|
||||
Location centerLoc = new Location(world, centerX, world.getMinHeight(), centerZ);
|
||||
|
||||
VillagerOptimizer.scheduling().regionSpecificScheduler(centerLoc).run(() -> future.complete(
|
||||
centerLoc.getNearbyEntities(
|
||||
halfSideLength,
|
||||
Math.abs(world.getMaxHeight()) + Math.abs(world.getMinHeight()), // World y can be between -64 and 320, we want everything from top to bottom
|
||||
halfSideLength
|
||||
)));
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (null == obj || obj.getClass() != BlockRegion2D.class)
|
||||
return false;
|
||||
BlockRegion2D blockRegion2D = (BlockRegion2D)obj;
|
||||
return blockRegion2D.worldUID.equals(this.worldUID) && blockRegion2D.centerX == this.centerX && blockRegion2D.centerZ == this.centerZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.worldUID, this.centerX, this.centerZ, this.halfSideLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BlockRegion2D{" +
|
||||
" radius(half side length)=" + halfSideLength +
|
||||
", centerX=" + centerX +
|
||||
", centerZ=" + centerZ +
|
||||
", worldUID=" + worldUID +
|
||||
"}";
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import org.bukkit.Location;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.entity.memory.MemoryKey;
|
||||
import org.bukkit.event.entity.VillagerReplenishTradeEvent;
|
||||
import org.bukkit.inventory.MerchantRecipe;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -41,8 +42,11 @@ public class WrappedVillager extends PDCWrapper {
|
||||
*/
|
||||
public void restock() {
|
||||
VillagerOptimizer.scheduling().entitySpecificScheduler(villager).run(() -> {
|
||||
for (MerchantRecipe recipe : villager.getRecipes()) {
|
||||
recipe.setUses(0);
|
||||
for (MerchantRecipe merchantRecipe : villager.getRecipes()) {
|
||||
VillagerReplenishTradeEvent restockRecipeEvent = new VillagerReplenishTradeEvent(villager, merchantRecipe);
|
||||
if (restockRecipeEvent.callEvent()) {
|
||||
restockRecipeEvent.getRecipe().setUses(0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user