From 609990e344c8675105de5f419cc596d4b33631dd Mon Sep 17 00:00:00 2001 From: Intelli Date: Sun, 14 Dec 2025 17:56:43 -0700 Subject: [PATCH] Finalized copper golem logging --- .../listener/CopperGolemChestListener.java | 631 +++++++++++++----- 1 file changed, 457 insertions(+), 174 deletions(-) diff --git a/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java index 96167d4..097d2dc 100644 --- a/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java +++ b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java @@ -1,133 +1,424 @@ package net.coreprotect.paper.listener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import org.bukkit.GameEvent; import org.bukkit.Location; import org.bukkit.Material; -import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.entity.CopperGolem; import org.bukkit.entity.Entity; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.world.GenericGameEvent; import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; -import org.bukkit.scheduler.BukkitTask; -import io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent; import net.coreprotect.CoreProtect; import net.coreprotect.bukkit.BukkitAdapter; import net.coreprotect.config.Config; +import net.coreprotect.config.ConfigHandler; import net.coreprotect.listener.player.InventoryChangeListener; import net.coreprotect.utility.ItemUtils; public final class CopperGolemChestListener implements Listener { - private static final String COPPER_GOLEM_NAME = "COPPER_GOLEM"; private static final String USERNAME = "#copper_golem"; - private static final long INITIAL_DELAY_TICKS = 5L; - private static final long POLL_INTERVAL_TICKS = 15L; - private static final long MAX_POLL_DURATION_TICKS = 600L; - private static final long MIN_THROTTLE_MILLIS = 2800L; + private static final long OPEN_INTERACTION_TIMEOUT_MILLIS = 20000L; + private static final long CLEANUP_INTERVAL_MILLIS = 60000L; + private static final long EMPTY_COPPER_CHEST_SKIP_TTL_MILLIS = 6000L; + private static final long CLOSE_FINALIZE_DELAY_TICKS = 1L; + private static final int CLOSE_FINALIZE_MAX_ATTEMPTS = 3; + private static final int CLOSE_FALLBACK_MAX_ATTEMPTS = 2; private final CoreProtect plugin; - private final Map pendingTransactions = new ConcurrentHashMap<>(); - private final Map throttleUntil = new ConcurrentHashMap<>(); + private final Map openInteractions = new ConcurrentHashMap<>(); + private final Map recentEmptyCopperChestSkips = new ConcurrentHashMap<>(); + private volatile long lastCleanupMillis; public CopperGolemChestListener(CoreProtect plugin) { this.plugin = plugin; } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onValidate(ItemTransportingEntityValidateTargetEvent event) { - if (!event.isAllowed()) { + public void onGenericGameEvent(GenericGameEvent event) { + if (event == null) { + return; + } + + GameEvent gameEvent = event.getEvent(); + if (gameEvent != GameEvent.CONTAINER_OPEN && gameEvent != GameEvent.CONTAINER_CLOSE) { return; } Entity entity = event.getEntity(); - if (entity == null || entity.getType() == null || !COPPER_GOLEM_NAME.equals(entity.getType().name())) { - return; - } - CopperGolem copperGolem = (CopperGolem) entity; - Material heldMaterial = getHeldItemMaterial(copperGolem); - - Block block = event.getBlock(); - if (block == null) { + if (!(entity instanceof CopperGolem)) { return; } - BlockState blockState = block.getState(); + Location eventLocation = event.getLocation(); + if (eventLocation == null || eventLocation.getWorld() == null) { + return; + } + + if (!Config.getConfig(eventLocation.getWorld()).ITEM_TRANSACTIONS) { + return; + } + + BlockState blockState = eventLocation.getBlock().getState(); if (!(blockState instanceof InventoryHolder)) { return; } - Location location = blockState.getLocation(); - if (location == null || location.getWorld() == null) { + Location containerLocation = blockState.getLocation(); + if (containerLocation == null || containerLocation.getWorld() == null) { return; } - if (!Config.getConfig(location.getWorld()).ITEM_TRANSACTIONS) { + Material containerType = blockState.getType(); + boolean isCopperChest = BukkitAdapter.ADAPTER.isCopperChest(containerType); + boolean isStandardChest = containerType == Material.CHEST || containerType == Material.TRAPPED_CHEST; + if (!isCopperChest && !isStandardChest) { return; } - scheduleTransaction(copperGolem, blockState, heldMaterial); + long now = System.currentTimeMillis(); + cleanupOpenInteractions(now); + + CopperGolem golem = (CopperGolem) entity; + TransactionKey containerKey = TransactionKey.of(containerLocation); + + if (gameEvent == GameEvent.CONTAINER_OPEN) { + handleContainerOpen(golem, containerLocation, containerKey, containerType, (InventoryHolder) blockState, now); + } + else { + handleContainerClose(golem, containerLocation, containerKey, containerType, (InventoryHolder) blockState, now); + } } - private void scheduleTransaction(CopperGolem copperGolem, BlockState blockState, Material heldMaterial) { - Location location = blockState.getLocation(); - if (location == null || copperGolem == null) { - return; - } - - TransactionKey transactionKey = TransactionKey.of(location); - Location targetLocation = location.clone(); - ItemStack[] baselineState = captureInventoryState(targetLocation); - if (baselineState == null) { - return; - } - if (!shouldMonitorInteraction(blockState.getType(), baselineState, heldMaterial)) { - return; - } - - PendingTransaction existing = pendingTransactions.get(transactionKey); - if (existing != null) { - existing.refresh(baselineState); - return; - } - - if (isThrottled(transactionKey)) { - return; - } - - PendingTransaction scheduled = new PendingTransaction(transactionKey, targetLocation, baselineState); - pendingTransactions.put(transactionKey, scheduled); - throttleUntil.put(transactionKey, System.currentTimeMillis() + MIN_THROTTLE_MILLIS); - scheduled.start(); - } - - private ItemStack[] captureInventoryState(Location location) { - if (location == null || location.getWorld() == null) { - return null; - } - - BlockState blockState = location.getBlock().getState(); - if (!(blockState instanceof InventoryHolder)) { - return null; - } - - InventoryHolder inventoryHolder = (InventoryHolder) blockState; + private void handleContainerOpen(CopperGolem golem, Location containerLocation, TransactionKey containerKey, Material containerType, InventoryHolder inventoryHolder, long nowMillis) { Inventory inventory = inventoryHolder.getInventory(); if (inventory == null) { + return; + } + + HeldItemSnapshot held = getHeldItemSnapshot(golem); + boolean isCopperChest = BukkitAdapter.ADAPTER.isCopperChest(containerType); + if (isCopperChest) { + if (held.material != null) { + return; + } + } + else { + if (held.material == null) { + return; + } + } + + ItemStack[] contents = inventory.getContents(); + if (contents == null) { + return; + } + + if (isCopperChest) { + if (isInventoryEmpty(contents)) { + recentEmptyCopperChestSkips.put(golem.getUniqueId(), new RecentEmptyCopperChestSkip(containerKey, nowMillis)); + return; + } + } + else { + if (!isInventoryEmpty(contents) && !containsOnlyMaterial(contents, held.material)) { + return; + } + + if (!hasSpaceForMaterial(contents, held.material)) { + return; + } + } + + ItemStack[] baseline = ItemUtils.getContainerState(contents); + if (baseline == null) { + return; + } + + Material heldMaterial = isCopperChest ? null : held.material; + int heldAmount = isCopperChest ? 0 : held.amount; + OpenInteraction interaction = new OpenInteraction(containerKey, containerLocation.clone(), containerType, baseline, heldMaterial, heldAmount, nowMillis); + recentEmptyCopperChestSkips.remove(golem.getUniqueId()); + openInteractions.put(golem.getUniqueId(), interaction); + } + + private void handleContainerClose(CopperGolem golem, Location containerLocation, TransactionKey containerKey, Material containerType, InventoryHolder inventoryHolder, long nowMillis) { + UUID golemId = golem.getUniqueId(); + OpenInteraction interaction = openInteractions.get(golemId); + if (interaction == null) { + if (BukkitAdapter.ADAPTER.isCopperChest(containerType)) { + RecentEmptyCopperChestSkip emptySkip = recentEmptyCopperChestSkips.get(golemId); + if (emptySkip != null && emptySkip.containerKey.equals(containerKey) && (nowMillis - emptySkip.skippedAtMillis) <= EMPTY_COPPER_CHEST_SKIP_TTL_MILLIS) { + recentEmptyCopperChestSkips.remove(golemId, emptySkip); + return; + } + + scheduleUntrackedCopperChestCloseFinalize(golemId, containerLocation.clone(), containerType, 1); + return; + } + + return; + } + + if (nowMillis - interaction.openedAtMillis > OPEN_INTERACTION_TIMEOUT_MILLIS) { + openInteractions.remove(golemId, interaction); + return; + } + + if (!interaction.containerKey.equals(containerKey)) { + return; + } + + openInteractions.remove(golemId, interaction); + scheduleCloseFinalize(golemId, interaction, containerKey, 1); + } + + private void scheduleCloseFinalize(UUID golemId, OpenInteraction interaction, TransactionKey containerKey, int attempt) { + plugin.getServer().getScheduler().runTaskLater(plugin, () -> finalizeContainerClose(golemId, interaction, containerKey, attempt), CLOSE_FINALIZE_DELAY_TICKS); + } + + private void scheduleUntrackedCopperChestCloseFinalize(UUID golemId, Location containerLocation, Material containerType, int attempt) { + plugin.getServer().getScheduler().runTaskLater(plugin, () -> finalizeUntrackedCopperChestClose(golemId, containerLocation, containerType, attempt), CLOSE_FINALIZE_DELAY_TICKS); + } + + private void finalizeContainerClose(UUID golemId, OpenInteraction interaction, TransactionKey containerKey, int attempt) { + Entity entity = plugin.getServer().getEntity(golemId); + if (!(entity instanceof CopperGolem)) { + return; + } + + BlockState blockState = interaction.location.getBlock().getState(); + if (!(blockState instanceof InventoryHolder)) { + return; + } + + Inventory inventory = ((InventoryHolder) blockState).getInventory(); + if (inventory == null) { + return; + } + + ItemStack[] currentContents = inventory.getContents(); + if (currentContents == null) { + return; + } + + boolean changed = hasInventoryChanged(interaction.baselineState, currentContents); + if (!changed) { + if (attempt < CLOSE_FINALIZE_MAX_ATTEMPTS) { + scheduleCloseFinalize(golemId, interaction, containerKey, attempt + 1); + } + return; + } + + CopperGolem golem = (CopperGolem) entity; + if (!isAttributableToGolem(golem, interaction, currentContents)) { + return; + } + + recordForcedContainerState(interaction.location, currentContents); + InventoryChangeListener.inventoryTransaction(USERNAME, interaction.location, interaction.baselineState); + } + + private void finalizeUntrackedCopperChestClose(UUID golemId, Location containerLocation, Material containerType, int attempt) { + Entity entity = plugin.getServer().getEntity(golemId); + if (!(entity instanceof CopperGolem)) { + return; + } + + if (!BukkitAdapter.ADAPTER.isCopperChest(containerType)) { + return; + } + + CopperGolem golem = (CopperGolem) entity; + ItemStack heldNowStack = getHeldItemStack(golem); + if (isEmptyItem(heldNowStack) || heldNowStack == null || heldNowStack.getAmount() <= 0) { + if (attempt < CLOSE_FALLBACK_MAX_ATTEMPTS) { + scheduleUntrackedCopperChestCloseFinalize(golemId, containerLocation, containerType, attempt + 1); + } + return; + } + + int heldAmount = heldNowStack.getAmount(); + if (heldAmount > 16) { + return; + } + + BlockState blockState = containerLocation.getBlock().getState(); + if (!(blockState instanceof InventoryHolder) || !BukkitAdapter.ADAPTER.isCopperChest(blockState.getType())) { + return; + } + + Inventory inventory = ((InventoryHolder) blockState).getInventory(); + if (inventory == null) { + return; + } + + ItemStack[] currentContents = inventory.getContents(); + if (currentContents == null) { + return; + } + + ItemStack[] baselineCandidate = reconstructCopperChestBaseline(currentContents, heldNowStack); + if (baselineCandidate == null) { + return; + } + + recordForcedContainerState(containerLocation, currentContents); + InventoryChangeListener.inventoryTransaction(USERNAME, containerLocation, baselineCandidate); + } + + private boolean isAttributableToGolem(CopperGolem golem, OpenInteraction interaction, ItemStack[] currentContents) { + boolean isCopperChest = BukkitAdapter.ADAPTER.isCopperChest(interaction.containerType); + HeldItemSnapshot heldNow = getHeldItemSnapshot(golem); + + if (isCopperChest) { + if (heldNow.material == null || heldNow.amount <= 0) { + return false; + } + + int baselineTotal = sumAllItems(interaction.baselineState); + int currentTotal = sumAllItems(currentContents); + int totalRemoved = baselineTotal - currentTotal; + if (totalRemoved != heldNow.amount) { + return false; + } + + int removedSameMaterial = sumMaterialAmount(interaction.baselineState, heldNow.material) - sumMaterialAmount(currentContents, heldNow.material); + return removedSameMaterial == heldNow.amount; + } + + Material heldMaterial = interaction.heldMaterial; + if (heldMaterial == null || interaction.heldAmount <= 0) { + return false; + } + + if (!containsOnlyMaterial(currentContents, heldMaterial)) { + return false; + } + + int baselineCount = sumMaterialAmount(interaction.baselineState, heldMaterial); + int currentCount = sumMaterialAmount(currentContents, heldMaterial); + int added = currentCount - baselineCount; + if (added <= 0) { + return false; + } + + int heldAfter = (heldNow.material == heldMaterial ? heldNow.amount : 0); + int removedFromGolem = interaction.heldAmount - heldAfter; + return added == removedFromGolem; + } + + private ItemStack getHeldItemStack(CopperGolem copperGolem) { + if (copperGolem == null) { + return null; + } + EntityEquipment equipment = copperGolem.getEquipment(); + if (equipment == null) { + return null; + } + ItemStack mainHand = equipment.getItemInMainHand(); + if (isEmptyItem(mainHand)) { + return null; + } + return mainHand.clone(); + } + + private ItemStack[] reconstructCopperChestBaseline(ItemStack[] currentContents, ItemStack heldNowStack) { + if (currentContents == null || heldNowStack == null || isEmptyItem(heldNowStack) || heldNowStack.getAmount() <= 0) { return null; } - return ItemUtils.getContainerState(inventory.getContents()); + ItemStack[] baseline = ItemUtils.getContainerState(currentContents); + if (baseline == null) { + return null; + } + + int addAmount = heldNowStack.getAmount(); + int maxStack = heldNowStack.getMaxStackSize(); + + for (int i = 0; i < baseline.length; i++) { + ItemStack item = baseline[i]; + if (item == null) { + continue; + } + if (item.isSimilar(heldNowStack)) { + if (item.getAmount() + addAmount > maxStack) { + return null; + } + item.setAmount(item.getAmount() + addAmount); + return baseline; + } + } + + for (int i = 0; i < baseline.length; i++) { + ItemStack item = baseline[i]; + if (isEmptyItem(item)) { + ItemStack placed = heldNowStack.clone(); + placed.setAmount(addAmount); + baseline[i] = placed; + return baseline; + } + } + + return null; + } + + private void recordForcedContainerState(Location location, ItemStack[] contents) { + if (location == null) { + return; + } + + ItemStack[] snapshot = ItemUtils.getContainerState(contents); + if (snapshot == null) { + return; + } + + String loggingContainerId = USERNAME + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ(); + List forceList = ConfigHandler.forceContainer.get(loggingContainerId); + if (forceList == null) { + forceList = Collections.synchronizedList(new ArrayList<>()); + ConfigHandler.forceContainer.put(loggingContainerId, forceList); + } + forceList.add(snapshot); + } + + private void cleanupOpenInteractions(long nowMillis) { + long last = lastCleanupMillis; + if (nowMillis - last < CLEANUP_INTERVAL_MILLIS) { + return; + } + lastCleanupMillis = nowMillis; + + for (Map.Entry entry : openInteractions.entrySet()) { + UUID golemId = entry.getKey(); + OpenInteraction interaction = entry.getValue(); + if (interaction == null || nowMillis - interaction.openedAtMillis > OPEN_INTERACTION_TIMEOUT_MILLIS || plugin.getServer().getEntity(golemId) == null) { + openInteractions.remove(golemId, interaction); + } + } + + for (Map.Entry entry : recentEmptyCopperChestSkips.entrySet()) { + UUID golemId = entry.getKey(); + RecentEmptyCopperChestSkip skip = entry.getValue(); + if (skip == null || nowMillis - skip.skippedAtMillis > EMPTY_COPPER_CHEST_SKIP_TTL_MILLIS || plugin.getServer().getEntity(golemId) == null) { + recentEmptyCopperChestSkips.remove(golemId, skip); + } + } } private boolean hasInventoryChanged(ItemStack[] previousState, ItemStack[] currentState) { @@ -158,30 +449,6 @@ public final class CopperGolemChestListener implements Listener { return false; } - private boolean shouldMonitorInteraction(Material blockType, ItemStack[] baselineState, Material heldMaterial) { - boolean isCopperChest = BukkitAdapter.ADAPTER.isCopperChest(blockType); - boolean isStandardChest = blockType == Material.CHEST || blockType == Material.TRAPPED_CHEST; - boolean golemHoldingItem = heldMaterial != null && heldMaterial != Material.AIR; - - if (!isCopperChest && !isStandardChest) { - return false; - } - - if (isCopperChest) { - return !golemHoldingItem && !isInventoryEmpty(baselineState); - } - - if (!golemHoldingItem) { - return false; - } - - if (isInventoryEmpty(baselineState)) { - return true; - } - - return containsMaterial(baselineState, heldMaterial); - } - private boolean isInventoryEmpty(ItemStack[] state) { if (state == null) { return true; @@ -200,106 +467,122 @@ public final class CopperGolemChestListener implements Listener { return item == null || item.getType().isAir() || item.getAmount() <= 0; } - private Material getHeldItemMaterial(CopperGolem copperGolem) { - if (copperGolem == null) { - return null; - } - EntityEquipment equipment = copperGolem.getEquipment(); - if (equipment == null) { - return null; - } - ItemStack mainHand = equipment.getItemInMainHand(); - if (isEmptyItem(mainHand)) { - return null; - } - return mainHand.getType(); - } - - private boolean containsMaterial(ItemStack[] state, Material material) { + private boolean containsOnlyMaterial(ItemStack[] state, Material material) { if (state == null || material == null) { return false; } for (ItemStack item : state) { - if (item != null && item.getType() == material && item.getAmount() > 0) { - return true; + if (isEmptyItem(item)) { + continue; + } + if (item.getType() != material) { + return false; } - } - return false; - } - - private boolean isThrottled(TransactionKey transactionKey) { - Long eligibleAt = throttleUntil.get(transactionKey); - if (eligibleAt == null) { - return false; - } - long now = System.currentTimeMillis(); - if (eligibleAt <= now) { - throttleUntil.remove(transactionKey, eligibleAt); - return false; } return true; } - private final class PendingTransaction implements Runnable { + private boolean hasSpaceForMaterial(ItemStack[] state, Material material) { + if (state == null || material == null) { + return false; + } - private final TransactionKey transactionKey; - private final Location targetLocation; - private ItemStack[] baselineState; - private BukkitTask task; - private long ticksElapsed; - private long ticksSinceLastTrigger; + int maxStackSize = material.getMaxStackSize(); + for (ItemStack item : state) { + if (isEmptyItem(item)) { + return true; + } + if (item.getType() == material && item.getAmount() < maxStackSize) { + return true; + } + } - private PendingTransaction(TransactionKey transactionKey, Location targetLocation, ItemStack[] baselineState) { - this.transactionKey = transactionKey; - this.targetLocation = targetLocation; + return false; + } + + private int sumMaterialAmount(ItemStack[] state, Material material) { + if (state == null || material == null) { + return 0; + } + int total = 0; + for (ItemStack item : state) { + if (!isEmptyItem(item) && item.getType() == material) { + total += item.getAmount(); + } + } + return total; + } + + private int sumAllItems(ItemStack[] state) { + if (state == null) { + return 0; + } + int total = 0; + for (ItemStack item : state) { + if (!isEmptyItem(item)) { + total += item.getAmount(); + } + } + return total; + } + + private HeldItemSnapshot getHeldItemSnapshot(CopperGolem copperGolem) { + if (copperGolem == null) { + return HeldItemSnapshot.EMPTY; + } + EntityEquipment equipment = copperGolem.getEquipment(); + if (equipment == null) { + return HeldItemSnapshot.EMPTY; + } + ItemStack mainHand = equipment.getItemInMainHand(); + if (isEmptyItem(mainHand)) { + return HeldItemSnapshot.EMPTY; + } + return new HeldItemSnapshot(mainHand.getType(), mainHand.getAmount()); + } + + private static final class OpenInteraction { + + private final TransactionKey containerKey; + private final Location location; + private final Material containerType; + private final ItemStack[] baselineState; + private final Material heldMaterial; + private final int heldAmount; + private final long openedAtMillis; + + private OpenInteraction(TransactionKey containerKey, Location location, Material containerType, ItemStack[] baselineState, Material heldMaterial, int heldAmount, long openedAtMillis) { + this.containerKey = containerKey; + this.location = location; + this.containerType = containerType; this.baselineState = baselineState; + this.heldMaterial = heldMaterial; + this.heldAmount = heldAmount; + this.openedAtMillis = openedAtMillis; } + } - private void start() { - task = plugin.getServer().getScheduler().runTaskTimer(plugin, this, INITIAL_DELAY_TICKS, POLL_INTERVAL_TICKS); + private static final class RecentEmptyCopperChestSkip { + + private final TransactionKey containerKey; + private final long skippedAtMillis; + + private RecentEmptyCopperChestSkip(TransactionKey containerKey, long skippedAtMillis) { + this.containerKey = containerKey; + this.skippedAtMillis = skippedAtMillis; } + } - private void refresh(ItemStack[] newBaselineState) { - ticksSinceLastTrigger = 0L; - if (newBaselineState != null) { - baselineState = newBaselineState; - } - } + private static final class HeldItemSnapshot { - @Override - public void run() { - long increment = ticksElapsed == 0L ? INITIAL_DELAY_TICKS : POLL_INTERVAL_TICKS; - ticksElapsed += increment; - ticksSinceLastTrigger += increment; - if (ticksSinceLastTrigger > MAX_POLL_DURATION_TICKS) { - cancelAndRemove(); - return; - } + private static final HeldItemSnapshot EMPTY = new HeldItemSnapshot(null, 0); - ItemStack[] currentState = captureInventoryState(targetLocation); - if (currentState == null) { - cancelAndRemove(); - return; - } + private final Material material; + private final int amount; - boolean stateChanged = hasInventoryChanged(baselineState, currentState); - if (stateChanged) { - InventoryChangeListener.inventoryTransaction(USERNAME, targetLocation, baselineState); - baselineState = ItemUtils.getContainerState(currentState); - // cancelAndRemove(); - // return; - } - } - - private void cancel() { - if (task != null) { - task.cancel(); - } - } - - private void cancelAndRemove() { - cancel(); - pendingTransactions.remove(transactionKey, this); + private HeldItemSnapshot(Material material, int amount) { + this.material = material; + this.amount = amount; } }