From 5276dc57d23c601624eefaffc85c711b42f9d734 Mon Sep 17 00:00:00 2001 From: Intelli Date: Tue, 18 Nov 2025 17:17:59 -0700 Subject: [PATCH 01/11] Added logging support for copper golem chest transactions --- pom.xml | 2 +- .../coreprotect/listener/ListenerHandler.java | 9 ++ .../listener/CopperGolemChestListener.java | 108 ++++++++++++++++++ .../worldedit/WorldEditBlockState.java | 5 + 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java diff --git a/pom.xml b/pom.xml index 8ad0cbe..e888e33 100755 --- a/pom.xml +++ b/pom.xml @@ -216,7 +216,7 @@ io.papermc.paper paper-api - 1.21.1-R0.1-SNAPSHOT + 1.21.10-R0.1-SNAPSHOT provided diff --git a/src/main/java/net/coreprotect/listener/ListenerHandler.java b/src/main/java/net/coreprotect/listener/ListenerHandler.java index 338b532..64beee4 100644 --- a/src/main/java/net/coreprotect/listener/ListenerHandler.java +++ b/src/main/java/net/coreprotect/listener/ListenerHandler.java @@ -55,6 +55,7 @@ import net.coreprotect.listener.world.LeavesDecayListener; import net.coreprotect.listener.world.PortalCreateListener; import net.coreprotect.listener.world.StructureGrowListener; import net.coreprotect.paper.listener.BlockPreDispenseListener; +import net.coreprotect.paper.listener.CopperGolemChestListener; import net.coreprotect.paper.listener.PaperChatListener; public final class ListenerHandler { @@ -72,6 +73,14 @@ public final class ListenerHandler { BlockPreDispenseListener.useBlockPreDispenseEvent = false; } + try { + Class.forName("io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent"); // Paper 1.21.10+ + pluginManager.registerEvents(new CopperGolemChestListener(plugin), plugin); + } + catch (Exception e) { + // Ignore registration failures to remain compatible with older servers. + } + // Block Listeners pluginManager.registerEvents(new BlockBreakListener(), plugin); pluginManager.registerEvents(new BlockBurnListener(), plugin); diff --git a/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java new file mode 100644 index 0000000..b6874ee --- /dev/null +++ b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java @@ -0,0 +1,108 @@ +package net.coreprotect.paper.listener; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.entity.Entity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.scheduler.BukkitTask; + +import io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent; +import net.coreprotect.CoreProtect; +import net.coreprotect.config.Config; +import net.coreprotect.listener.player.InventoryChangeListener; + +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 DELAY_TICKS = 50L; + + private final CoreProtect plugin; + private final Map pendingTransactions = new ConcurrentHashMap<>(); + + public CopperGolemChestListener(CoreProtect plugin) { + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onValidate(ItemTransportingEntityValidateTargetEvent event) { + if (!event.isAllowed()) { + return; + } + + Entity entity = event.getEntity(); + if (entity == null || entity.getType() == null || !COPPER_GOLEM_NAME.equals(entity.getType().name())) { + return; + } + + Block block = event.getBlock(); + if (block == null) { + return; + } + + BlockState blockState = block.getState(); + if (!(blockState instanceof InventoryHolder)) { + return; + } + + Location location = blockState.getLocation(); + if (location == null || location.getWorld() == null) { + return; + } + + if (!Config.getConfig(location.getWorld()).ITEM_TRANSACTIONS) { + return; + } + + scheduleTransaction(entity, location); + } + + private void scheduleTransaction(Entity entity, Location location) { + UUID entityId = entity.getUniqueId(); + PendingTransaction pendingTransaction = pendingTransactions.remove(entityId); + if (pendingTransaction != null) { + pendingTransaction.cancel(); + } + + Location targetLocation = location.clone(); + PendingTransaction scheduled = new PendingTransaction(); + BukkitTask task = plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + if (!pendingTransactions.remove(entityId, scheduled)) { + return; + } + + Entity trackedEntity = plugin.getServer().getEntity(entityId); + if (trackedEntity == null || !trackedEntity.isValid()) { + return; + } + + InventoryChangeListener.inventoryTransaction(USERNAME, targetLocation, null); + }, DELAY_TICKS); + + scheduled.setTask(task); + pendingTransactions.put(entityId, scheduled); + } + + private static final class PendingTransaction { + + private BukkitTask task; + + private void cancel() { + if (task != null) { + task.cancel(); + } + } + + private void setTask(BukkitTask task) { + this.task = task; + } + } +} diff --git a/src/main/java/net/coreprotect/worldedit/WorldEditBlockState.java b/src/main/java/net/coreprotect/worldedit/WorldEditBlockState.java index 89007f1..23a9a81 100644 --- a/src/main/java/net/coreprotect/worldedit/WorldEditBlockState.java +++ b/src/main/java/net/coreprotect/worldedit/WorldEditBlockState.java @@ -196,4 +196,9 @@ public final class WorldEditBlockState implements BlockState { return null; } + @Override + public boolean isSuffocating() { + return false; + } + } From 1f60dcc3cfda7b3b785d964127adae25f257d14b Mon Sep 17 00:00:00 2001 From: Intelli Date: Wed, 19 Nov 2025 15:12:33 -0700 Subject: [PATCH 02/11] Improve synthetic username handling for golem logging (WIP) --- .../coreprotect/consumer/process/Process.java | 6 ++-- .../database/logger/ContainerLogger.java | 21 ++++++++---- .../listener/CopperGolemChestListener.java | 7 ++-- .../utility/SyntheticUsernames.java | 32 +++++++++++++++++++ 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 src/main/java/net/coreprotect/utility/SyntheticUsernames.java diff --git a/src/main/java/net/coreprotect/consumer/process/Process.java b/src/main/java/net/coreprotect/consumer/process/Process.java index 22c7932..9eb5a84 100755 --- a/src/main/java/net/coreprotect/consumer/process/Process.java +++ b/src/main/java/net/coreprotect/consumer/process/Process.java @@ -15,6 +15,7 @@ import net.coreprotect.config.ConfigHandler; import net.coreprotect.consumer.Consumer; import net.coreprotect.database.Database; import net.coreprotect.database.statement.UserStatement; +import net.coreprotect.utility.SyntheticUsernames; public class Process { @@ -99,8 +100,9 @@ public class Process { if (data != null) { String user = data[0]; String uuid = data[1]; - if (user != null && ConfigHandler.playerIdCache.get(user.toLowerCase(Locale.ROOT)) == null) { - UserStatement.loadId(connection, user, uuid); + String normalizedUser = SyntheticUsernames.normalize(user); + if (normalizedUser != null && ConfigHandler.playerIdCache.get(normalizedUser.toLowerCase(Locale.ROOT)) == null) { + UserStatement.loadId(connection, normalizedUser, uuid); } } } diff --git a/src/main/java/net/coreprotect/database/logger/ContainerLogger.java b/src/main/java/net/coreprotect/database/logger/ContainerLogger.java index d0aa141..5f4d25b 100644 --- a/src/main/java/net/coreprotect/database/logger/ContainerLogger.java +++ b/src/main/java/net/coreprotect/database/logger/ContainerLogger.java @@ -25,6 +25,7 @@ import net.coreprotect.event.CoreProtectPreLogEvent; import net.coreprotect.utility.BlockUtils; import net.coreprotect.utility.ItemUtils; import net.coreprotect.utility.MaterialUtils; +import net.coreprotect.utility.SyntheticUsernames; import net.coreprotect.utility.WorldUtils; import net.coreprotect.utility.serialize.ItemMetaHandler; @@ -57,7 +58,13 @@ public class ContainerLogger extends Queue { return; } - String loggingContainerId = player.toLowerCase(Locale.ROOT) + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ(); + String uniqueUser = player; + String canonicalUser = SyntheticUsernames.normalize(uniqueUser); + if (canonicalUser == null) { + canonicalUser = uniqueUser; + } + + String loggingContainerId = uniqueUser.toLowerCase(Locale.ROOT) + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ(); List oldList = ConfigHandler.oldContainer.get(loggingContainerId); ItemStack[] oi1 = oldList.get(0); ItemStack[] oldInventory = ItemUtils.getContainerState(oi1); @@ -67,7 +74,7 @@ public class ContainerLogger extends Queue { } // Check if this is a dispenser with no actual changes - if (player.equals("#dispenser") && ItemUtils.compareContainers(oldInventory, newInventory)) { + if ("#dispenser".equals(canonicalUser) && ItemUtils.compareContainers(oldInventory, newInventory)) { // No changes detected, mark this dispenser in the dispenserNoChange map // Extract the location key from the loggingContainerId // Format: #dispenser.x.y.z @@ -95,7 +102,7 @@ public class ContainerLogger extends Queue { // If we reach here, the dispenser event resulted in changes // Remove any pending event for this dispenser - if (player.equals("#dispenser")) { + if ("#dispenser".equals(canonicalUser)) { String[] parts = loggingContainerId.split("\\."); if (parts.length >= 4) { int x = Integer.parseInt(parts[1]); @@ -191,12 +198,12 @@ public class ContainerLogger extends Queue { ItemUtils.mergeItems(type, newInventory); if (type != Material.ENDER_CHEST) { - logTransaction(preparedStmtContainer, batchCount, player, type, faceData, oldInventory, 0, location); - logTransaction(preparedStmtContainer, batchCount, player, type, faceData, newInventory, 1, location); + logTransaction(preparedStmtContainer, batchCount, canonicalUser, type, faceData, oldInventory, 0, location); + logTransaction(preparedStmtContainer, batchCount, canonicalUser, type, faceData, newInventory, 1, location); } else { // pass ender chest transactions to item logger - ItemLogger.logTransaction(preparedStmtItems, batchCount, 0, player, location, oldInventory, ItemLogger.ITEM_REMOVE_ENDER); - ItemLogger.logTransaction(preparedStmtItems, batchCount, 0, player, location, newInventory, ItemLogger.ITEM_ADD_ENDER); + ItemLogger.logTransaction(preparedStmtItems, batchCount, 0, canonicalUser, location, oldInventory, ItemLogger.ITEM_REMOVE_ENDER); + ItemLogger.logTransaction(preparedStmtItems, batchCount, 0, canonicalUser, location, newInventory, ItemLogger.ITEM_ADD_ENDER); } oldList.remove(0); diff --git a/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java index b6874ee..6e0ab1e 100644 --- a/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java +++ b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java @@ -12,18 +12,20 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; 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.config.Config; import net.coreprotect.listener.player.InventoryChangeListener; +import net.coreprotect.utility.SyntheticUsernames; 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 DELAY_TICKS = 50L; + private static final long DELAY_TICKS = 60L; private final CoreProtect plugin; private final Map pendingTransactions = new ConcurrentHashMap<>(); @@ -67,6 +69,7 @@ public final class CopperGolemChestListener implements Listener { private void scheduleTransaction(Entity entity, Location location) { UUID entityId = entity.getUniqueId(); + String username = SyntheticUsernames.qualifyWithUuid(USERNAME, entityId); PendingTransaction pendingTransaction = pendingTransactions.remove(entityId); if (pendingTransaction != null) { pendingTransaction.cancel(); @@ -84,7 +87,7 @@ public final class CopperGolemChestListener implements Listener { return; } - InventoryChangeListener.inventoryTransaction(USERNAME, targetLocation, null); + InventoryChangeListener.inventoryTransaction(username, targetLocation, null); }, DELAY_TICKS); scheduled.setTask(task); diff --git a/src/main/java/net/coreprotect/utility/SyntheticUsernames.java b/src/main/java/net/coreprotect/utility/SyntheticUsernames.java new file mode 100644 index 0000000..b9b1903 --- /dev/null +++ b/src/main/java/net/coreprotect/utility/SyntheticUsernames.java @@ -0,0 +1,32 @@ +package net.coreprotect.utility; + +import java.util.UUID; + +public final class SyntheticUsernames { + + private static final String UUID_SUFFIX = "_uuid:"; + + private SyntheticUsernames() { + } + + public static String normalize(String user) { + if (user == null) { + return null; + } + + int index = user.indexOf(UUID_SUFFIX); + if (index == -1) { + return user; + } + + return user.substring(0, index); + } + + public static String qualifyWithUuid(String base, UUID uuid) { + if (base == null || uuid == null) { + return base; + } + + return base + UUID_SUFFIX + uuid; + } +} From de5a064c8d7d4e023cc760c538474a28f069c7d4 Mon Sep 17 00:00:00 2001 From: Intelli Date: Thu, 20 Nov 2025 15:05:12 -0700 Subject: [PATCH 03/11] Fixes for copper golem logging (WIP) --- .../coreprotect/consumer/process/Process.java | 5 +- .../database/logger/ContainerLogger.java | 21 +- .../listener/CopperGolemChestListener.java | 286 ++++++++++++++++-- .../utility/SyntheticUsernames.java | 32 -- 4 files changed, 269 insertions(+), 75 deletions(-) delete mode 100644 src/main/java/net/coreprotect/utility/SyntheticUsernames.java diff --git a/src/main/java/net/coreprotect/consumer/process/Process.java b/src/main/java/net/coreprotect/consumer/process/Process.java index 9eb5a84..350f24d 100755 --- a/src/main/java/net/coreprotect/consumer/process/Process.java +++ b/src/main/java/net/coreprotect/consumer/process/Process.java @@ -100,9 +100,8 @@ public class Process { if (data != null) { String user = data[0]; String uuid = data[1]; - String normalizedUser = SyntheticUsernames.normalize(user); - if (normalizedUser != null && ConfigHandler.playerIdCache.get(normalizedUser.toLowerCase(Locale.ROOT)) == null) { - UserStatement.loadId(connection, normalizedUser, uuid); + if (user != null && ConfigHandler.playerIdCache.get(user.toLowerCase(Locale.ROOT)) == null) { + UserStatement.loadId(connection, user, uuid); } } } diff --git a/src/main/java/net/coreprotect/database/logger/ContainerLogger.java b/src/main/java/net/coreprotect/database/logger/ContainerLogger.java index 5f4d25b..bb9a718 100644 --- a/src/main/java/net/coreprotect/database/logger/ContainerLogger.java +++ b/src/main/java/net/coreprotect/database/logger/ContainerLogger.java @@ -25,7 +25,6 @@ import net.coreprotect.event.CoreProtectPreLogEvent; import net.coreprotect.utility.BlockUtils; import net.coreprotect.utility.ItemUtils; import net.coreprotect.utility.MaterialUtils; -import net.coreprotect.utility.SyntheticUsernames; import net.coreprotect.utility.WorldUtils; import net.coreprotect.utility.serialize.ItemMetaHandler; @@ -58,13 +57,7 @@ public class ContainerLogger extends Queue { return; } - String uniqueUser = player; - String canonicalUser = SyntheticUsernames.normalize(uniqueUser); - if (canonicalUser == null) { - canonicalUser = uniqueUser; - } - - String loggingContainerId = uniqueUser.toLowerCase(Locale.ROOT) + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ(); + String loggingContainerId = player.toLowerCase(Locale.ROOT) + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ(); List oldList = ConfigHandler.oldContainer.get(loggingContainerId); ItemStack[] oi1 = oldList.get(0); ItemStack[] oldInventory = ItemUtils.getContainerState(oi1); @@ -74,7 +67,7 @@ public class ContainerLogger extends Queue { } // Check if this is a dispenser with no actual changes - if ("#dispenser".equals(canonicalUser) && ItemUtils.compareContainers(oldInventory, newInventory)) { + if ("#dispenser".equals(player) && ItemUtils.compareContainers(oldInventory, newInventory)) { // No changes detected, mark this dispenser in the dispenserNoChange map // Extract the location key from the loggingContainerId // Format: #dispenser.x.y.z @@ -102,7 +95,7 @@ public class ContainerLogger extends Queue { // If we reach here, the dispenser event resulted in changes // Remove any pending event for this dispenser - if ("#dispenser".equals(canonicalUser)) { + if ("#dispenser".equals(player)) { String[] parts = loggingContainerId.split("\\."); if (parts.length >= 4) { int x = Integer.parseInt(parts[1]); @@ -198,12 +191,12 @@ public class ContainerLogger extends Queue { ItemUtils.mergeItems(type, newInventory); if (type != Material.ENDER_CHEST) { - logTransaction(preparedStmtContainer, batchCount, canonicalUser, type, faceData, oldInventory, 0, location); - logTransaction(preparedStmtContainer, batchCount, canonicalUser, type, faceData, newInventory, 1, location); + logTransaction(preparedStmtContainer, batchCount, player, type, faceData, oldInventory, 0, location); + logTransaction(preparedStmtContainer, batchCount, player, type, faceData, newInventory, 1, location); } else { // pass ender chest transactions to item logger - ItemLogger.logTransaction(preparedStmtItems, batchCount, 0, canonicalUser, location, oldInventory, ItemLogger.ITEM_REMOVE_ENDER); - ItemLogger.logTransaction(preparedStmtItems, batchCount, 0, canonicalUser, location, newInventory, ItemLogger.ITEM_ADD_ENDER); + ItemLogger.logTransaction(preparedStmtItems, batchCount, 0, player, location, oldInventory, ItemLogger.ITEM_REMOVE_ENDER); + ItemLogger.logTransaction(preparedStmtItems, batchCount, 0, player, location, newInventory, ItemLogger.ITEM_ADD_ENDER); } oldList.remove(0); diff --git a/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java index 6e0ab1e..96167d4 100644 --- a/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java +++ b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java @@ -1,34 +1,44 @@ package net.coreprotect.paper.listener; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; 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.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.listener.player.InventoryChangeListener; -import net.coreprotect.utility.SyntheticUsernames; +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 DELAY_TICKS = 60L; + 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 final CoreProtect plugin; - private final Map pendingTransactions = new ConcurrentHashMap<>(); + private final Map pendingTransactions = new ConcurrentHashMap<>(); + private final Map throttleUntil = new ConcurrentHashMap<>(); public CopperGolemChestListener(CoreProtect plugin) { this.plugin = plugin; @@ -44,6 +54,8 @@ public final class CopperGolemChestListener implements Listener { 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) { @@ -64,39 +76,220 @@ public final class CopperGolemChestListener implements Listener { return; } - scheduleTransaction(entity, location); + scheduleTransaction(copperGolem, blockState, heldMaterial); } - private void scheduleTransaction(Entity entity, Location location) { - UUID entityId = entity.getUniqueId(); - String username = SyntheticUsernames.qualifyWithUuid(USERNAME, entityId); - PendingTransaction pendingTransaction = pendingTransactions.remove(entityId); - if (pendingTransaction != null) { - pendingTransaction.cancel(); + 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(); - PendingTransaction scheduled = new PendingTransaction(); - BukkitTask task = plugin.getServer().getScheduler().runTaskLater(plugin, () -> { - if (!pendingTransactions.remove(entityId, scheduled)) { - return; - } + ItemStack[] baselineState = captureInventoryState(targetLocation); + if (baselineState == null) { + return; + } + if (!shouldMonitorInteraction(blockState.getType(), baselineState, heldMaterial)) { + return; + } - Entity trackedEntity = plugin.getServer().getEntity(entityId); - if (trackedEntity == null || !trackedEntity.isValid()) { - return; - } + PendingTransaction existing = pendingTransactions.get(transactionKey); + if (existing != null) { + existing.refresh(baselineState); + return; + } - InventoryChangeListener.inventoryTransaction(username, targetLocation, null); - }, DELAY_TICKS); + if (isThrottled(transactionKey)) { + return; + } - scheduled.setTask(task); - pendingTransactions.put(entityId, scheduled); + PendingTransaction scheduled = new PendingTransaction(transactionKey, targetLocation, baselineState); + pendingTransactions.put(transactionKey, scheduled); + throttleUntil.put(transactionKey, System.currentTimeMillis() + MIN_THROTTLE_MILLIS); + scheduled.start(); } - private static final class PendingTransaction { + 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; + Inventory inventory = inventoryHolder.getInventory(); + if (inventory == null) { + return null; + } + + return ItemUtils.getContainerState(inventory.getContents()); + } + + private boolean hasInventoryChanged(ItemStack[] previousState, ItemStack[] currentState) { + if (previousState == null || currentState == null) { + return true; + } + if (previousState.length != currentState.length) { + return true; + } + + for (int i = 0; i < previousState.length; i++) { + ItemStack previousItem = previousState[i]; + ItemStack currentItem = currentState[i]; + + if (previousItem == null && currentItem == null) { + continue; + } + + if (previousItem == null || currentItem == null) { + return true; + } + + if (!previousItem.equals(currentItem)) { + return true; + } + } + + 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; + } + + for (ItemStack item : state) { + if (!isEmptyItem(item)) { + return false; + } + } + + return true; + } + + private boolean isEmptyItem(ItemStack item) { + 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) { + if (state == null || material == null) { + return false; + } + for (ItemStack item : state) { + if (item != null && item.getType() == material && item.getAmount() > 0) { + return true; + } + } + 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 final TransactionKey transactionKey; + private final Location targetLocation; + private ItemStack[] baselineState; private BukkitTask task; + private long ticksElapsed; + private long ticksSinceLastTrigger; + + private PendingTransaction(TransactionKey transactionKey, Location targetLocation, ItemStack[] baselineState) { + this.transactionKey = transactionKey; + this.targetLocation = targetLocation; + this.baselineState = baselineState; + } + + private void start() { + task = plugin.getServer().getScheduler().runTaskTimer(plugin, this, INITIAL_DELAY_TICKS, POLL_INTERVAL_TICKS); + } + + private void refresh(ItemStack[] newBaselineState) { + ticksSinceLastTrigger = 0L; + if (newBaselineState != null) { + baselineState = newBaselineState; + } + } + + @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; + } + + ItemStack[] currentState = captureInventoryState(targetLocation); + if (currentState == null) { + cancelAndRemove(); + return; + } + + boolean stateChanged = hasInventoryChanged(baselineState, currentState); + if (stateChanged) { + InventoryChangeListener.inventoryTransaction(USERNAME, targetLocation, baselineState); + baselineState = ItemUtils.getContainerState(currentState); + // cancelAndRemove(); + // return; + } + } private void cancel() { if (task != null) { @@ -104,8 +297,49 @@ public final class CopperGolemChestListener implements Listener { } } - private void setTask(BukkitTask task) { - this.task = task; + private void cancelAndRemove() { + cancel(); + pendingTransactions.remove(transactionKey, this); } } + + private static final class TransactionKey { + + private final UUID worldId; + private final int x; + private final int y; + private final int z; + + private TransactionKey(UUID worldId, int x, int y, int z) { + this.worldId = worldId; + this.x = x; + this.y = y; + this.z = z; + } + + private static TransactionKey of(Location location) { + if (location == null || location.getWorld() == null) { + throw new IllegalArgumentException("Location must have world"); + } + return new TransactionKey(location.getWorld().getUID(), location.getBlockX(), location.getBlockY(), location.getBlockZ()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof TransactionKey)) { + return false; + } + TransactionKey other = (TransactionKey) obj; + return worldId.equals(other.worldId) && x == other.x && y == other.y && z == other.z; + } + + @Override + public int hashCode() { + return Objects.hash(worldId, x, y, z); + } + + } } diff --git a/src/main/java/net/coreprotect/utility/SyntheticUsernames.java b/src/main/java/net/coreprotect/utility/SyntheticUsernames.java deleted file mode 100644 index b9b1903..0000000 --- a/src/main/java/net/coreprotect/utility/SyntheticUsernames.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.coreprotect.utility; - -import java.util.UUID; - -public final class SyntheticUsernames { - - private static final String UUID_SUFFIX = "_uuid:"; - - private SyntheticUsernames() { - } - - public static String normalize(String user) { - if (user == null) { - return null; - } - - int index = user.indexOf(UUID_SUFFIX); - if (index == -1) { - return user; - } - - return user.substring(0, index); - } - - public static String qualifyWithUuid(String base, UUID uuid) { - if (base == null || uuid == null) { - return base; - } - - return base + UUID_SUFFIX + uuid; - } -} From d3091f94011952c25612a7cf723a66765006c380 Mon Sep 17 00:00:00 2001 From: Intelli Date: Wed, 26 Nov 2025 15:23:09 -0700 Subject: [PATCH 04/11] Fix missed import removal --- src/main/java/net/coreprotect/consumer/process/Process.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/net/coreprotect/consumer/process/Process.java b/src/main/java/net/coreprotect/consumer/process/Process.java index 350f24d..22c7932 100755 --- a/src/main/java/net/coreprotect/consumer/process/Process.java +++ b/src/main/java/net/coreprotect/consumer/process/Process.java @@ -15,7 +15,6 @@ import net.coreprotect.config.ConfigHandler; import net.coreprotect.consumer.Consumer; import net.coreprotect.database.Database; import net.coreprotect.database.statement.UserStatement; -import net.coreprotect.utility.SyntheticUsernames; public class Process { From 071254534535e3a19a2673aa8468193a21c19f6e Mon Sep 17 00:00:00 2001 From: Intelli Date: Sun, 14 Dec 2025 13:44:01 -0700 Subject: [PATCH 05/11] Bump supported MC version to 1.21.11 --- pom.xml | 2 +- src/main/java/net/coreprotect/config/ConfigHandler.java | 2 +- src/main/java/net/coreprotect/listener/ListenerHandler.java | 2 +- src/main/java/net/coreprotect/thread/Scheduler.java | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index e888e33..e4bc6ed 100755 --- a/pom.xml +++ b/pom.xml @@ -216,7 +216,7 @@ io.papermc.paper paper-api - 1.21.10-R0.1-SNAPSHOT + 1.21.11-R0.1-SNAPSHOT provided diff --git a/src/main/java/net/coreprotect/config/ConfigHandler.java b/src/main/java/net/coreprotect/config/ConfigHandler.java index bf59112..40c69cc 100644 --- a/src/main/java/net/coreprotect/config/ConfigHandler.java +++ b/src/main/java/net/coreprotect/config/ConfigHandler.java @@ -53,7 +53,7 @@ public class ConfigHandler extends Queue { public static final String JAVA_VERSION = "11.0"; public static final String MINECRAFT_VERSION = "1.16"; public static final String PATCH_VERSION = "23.0"; - public static final String LATEST_VERSION = "1.21.10"; + public static final String LATEST_VERSION = "1.21.11"; public static String path = "plugins/CoreProtect/"; public static String sqlite = "database.db"; public static String host = "127.0.0.1"; diff --git a/src/main/java/net/coreprotect/listener/ListenerHandler.java b/src/main/java/net/coreprotect/listener/ListenerHandler.java index 64beee4..361efb6 100644 --- a/src/main/java/net/coreprotect/listener/ListenerHandler.java +++ b/src/main/java/net/coreprotect/listener/ListenerHandler.java @@ -75,7 +75,7 @@ public final class ListenerHandler { try { Class.forName("io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent"); // Paper 1.21.10+ - pluginManager.registerEvents(new CopperGolemChestListener(plugin), plugin); + // pluginManager.registerEvents(new CopperGolemChestListener(plugin), plugin); } catch (Exception e) { // Ignore registration failures to remain compatible with older servers. diff --git a/src/main/java/net/coreprotect/thread/Scheduler.java b/src/main/java/net/coreprotect/thread/Scheduler.java index eb16a9a..b94c555 100644 --- a/src/main/java/net/coreprotect/thread/Scheduler.java +++ b/src/main/java/net/coreprotect/thread/Scheduler.java @@ -7,7 +7,6 @@ import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.scheduler.BukkitTask; -import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import net.coreprotect.CoreProtect; import net.coreprotect.config.ConfigHandler; @@ -116,8 +115,8 @@ public class Scheduler { public static void cancelTask(Object task) { if (ConfigHandler.isFolia) { - if (task instanceof ScheduledTask) { - ScheduledTask scheduledTask = (ScheduledTask) task; + if (task instanceof io.papermc.paper.threadedregions.scheduler.ScheduledTask) { + io.papermc.paper.threadedregions.scheduler.ScheduledTask scheduledTask = (io.papermc.paper.threadedregions.scheduler.ScheduledTask) task; scheduledTask.cancel(); } } From 2979af360c610faf950eb37262aebb1a0525cc5a Mon Sep 17 00:00:00 2001 From: Intelli Date: Sun, 14 Dec 2025 14:00:11 -0700 Subject: [PATCH 06/11] CoreProtect Community Edition v23.1 release --- README.md | 2 +- pom.xml | 2 +- src/main/java/net/coreprotect/config/ConfigHandler.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 17b7720..dfe1af3 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Maven net.coreprotect coreprotect - 23.0 + 23.1 provided ``` diff --git a/pom.xml b/pom.xml index e4bc6ed..bb0a96a 100755 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 net.coreprotect CoreProtect - 23.0 + 23.1 UTF-8 diff --git a/src/main/java/net/coreprotect/config/ConfigHandler.java b/src/main/java/net/coreprotect/config/ConfigHandler.java index 40c69cc..0275d0d 100644 --- a/src/main/java/net/coreprotect/config/ConfigHandler.java +++ b/src/main/java/net/coreprotect/config/ConfigHandler.java @@ -52,7 +52,7 @@ public class ConfigHandler extends Queue { public static final String COMMUNITY_EDITION = "Community Edition"; public static final String JAVA_VERSION = "11.0"; public static final String MINECRAFT_VERSION = "1.16"; - public static final String PATCH_VERSION = "23.0"; + public static final String PATCH_VERSION = "23.1"; public static final String LATEST_VERSION = "1.21.11"; public static String path = "plugins/CoreProtect/"; public static String sqlite = "database.db"; From 38166fd381ffd37ee69b543d301be37406d55dfb Mon Sep 17 00:00:00 2001 From: Intelli Date: Sun, 14 Dec 2025 14:56:30 -0700 Subject: [PATCH 07/11] Re-enable experimental copper golem logging (WIP) --- src/main/java/net/coreprotect/listener/ListenerHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/coreprotect/listener/ListenerHandler.java b/src/main/java/net/coreprotect/listener/ListenerHandler.java index 361efb6..64beee4 100644 --- a/src/main/java/net/coreprotect/listener/ListenerHandler.java +++ b/src/main/java/net/coreprotect/listener/ListenerHandler.java @@ -75,7 +75,7 @@ public final class ListenerHandler { try { Class.forName("io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent"); // Paper 1.21.10+ - // pluginManager.registerEvents(new CopperGolemChestListener(plugin), plugin); + pluginManager.registerEvents(new CopperGolemChestListener(plugin), plugin); } catch (Exception e) { // Ignore registration failures to remain compatible with older servers. From 609990e344c8675105de5f419cc596d4b33631dd Mon Sep 17 00:00:00 2001 From: Intelli Date: Sun, 14 Dec 2025 17:56:43 -0700 Subject: [PATCH 08/11] 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; } } From 893ce9af4644788245fbf487d38687c1e3c9cdf8 Mon Sep 17 00:00:00 2001 From: Intelli Date: Mon, 15 Dec 2025 13:50:50 -0700 Subject: [PATCH 09/11] Update API docs --- docs/api/index.md | 6 +++--- docs/api/version/v11.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api/index.md b/docs/api/index.md index 976697b..5d25173 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -4,8 +4,8 @@ The CoreProtect API enables you to log your own block changes, perform lookups, | API Details | | | --- | --- | -| **API Version:** | 10 | -| **Plugin Version:** | v22.4+ | +| **API Version:** | 11 | +| **Plugin Version:** | v23.1+ | | **Maven:** | [maven.playpro.com](https://maven.playpro.com) | -*Documentation for the API version 10 can be found [here](/api/version/v10/).* \ No newline at end of file +*Documentation for the API version 10 can be found [here](/api/version/v11/).* \ No newline at end of file diff --git a/docs/api/version/v11.md b/docs/api/version/v11.md index 8541c13..4453dad 100644 --- a/docs/api/version/v11.md +++ b/docs/api/version/v11.md @@ -5,7 +5,7 @@ The CoreProtect API enables you to log your own block changes, perform lookups, | API Details | | | --- | --- | | **API Version:** | 11 | -| **Plugin Version:** | v24.0+ | +| **Plugin Version:** | v23.1+ | | **Maven:** | [maven.playpro.com](https://maven.playpro.com) | --- @@ -28,8 +28,8 @@ CoreProtectPreLogEvent(String user) ## Getting Started -Ensure you're using CoreProtect 24.0 or higher. Add it as an external jar to your plugin in your IDE. -Alternatively, if using Maven, you can add it via the repository https://maven.playpro.com (net.coreprotect, 24.0). +Ensure you're using CoreProtect 23.1 or higher. Add it as an external jar to your plugin in your IDE. +Alternatively, if using Maven, you can add it via the repository https://maven.playpro.com (net.coreprotect, 23.1). The first thing you need to do is get access to CoreProtect. You can do this by using code similar to the following: From 0f0ceccb7b80768b42f3c3cb7fb37412352a8278 Mon Sep 17 00:00:00 2001 From: Intelli Date: Mon, 15 Dec 2025 16:11:03 -0700 Subject: [PATCH 10/11] Fix copper golem logging not working with double chests Works around Paper bug where the entity is null when a golem closes a double chest --- .../listener/CopperGolemChestListener.java | 215 ++++++++++++++++-- 1 file changed, 196 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java index 097d2dc..5541dc0 100644 --- a/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java +++ b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -12,6 +13,7 @@ import org.bukkit.GameEvent; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.BlockState; +import org.bukkit.block.DoubleChest; import org.bukkit.entity.CopperGolem; import org.bukkit.entity.Entity; import org.bukkit.event.EventHandler; @@ -43,6 +45,8 @@ public final class CopperGolemChestListener implements Listener { private final CoreProtect plugin; private final Map openInteractions = new ConcurrentHashMap<>(); private final Map recentEmptyCopperChestSkips = new ConcurrentHashMap<>(); + private final Map openInteractionIndexByContainerKey = new ConcurrentHashMap<>(); + private final Map> emptySkipGolemsByContainerKey = new ConcurrentHashMap<>(); private volatile long lastCleanupMillis; public CopperGolemChestListener(CoreProtect plugin) { @@ -61,8 +65,15 @@ public final class CopperGolemChestListener implements Listener { } Entity entity = event.getEntity(); - if (!(entity instanceof CopperGolem)) { - return; + if (gameEvent == GameEvent.CONTAINER_OPEN) { + if (!(entity instanceof CopperGolem)) { + return; + } + } + else { + if (entity != null && !(entity instanceof CopperGolem)) { + return; + } } Location eventLocation = event.getLocation(); @@ -85,7 +96,7 @@ public final class CopperGolemChestListener implements Listener { } Material containerType = blockState.getType(); - boolean isCopperChest = BukkitAdapter.ADAPTER.isCopperChest(containerType); + boolean isCopperChest = isCopperChest(containerType); boolean isStandardChest = containerType == Material.CHEST || containerType == Material.TRAPPED_CHEST; if (!isCopperChest && !isStandardChest) { return; @@ -94,17 +105,51 @@ public final class CopperGolemChestListener implements Listener { long now = System.currentTimeMillis(); cleanupOpenInteractions(now); - CopperGolem golem = (CopperGolem) entity; - TransactionKey containerKey = TransactionKey.of(containerLocation); + InventoryHolder inventoryHolder = (InventoryHolder) blockState; + Inventory inventory = inventoryHolder.getInventory(); + Location canonicalLocation = getCanonicalContainerLocation(containerLocation, inventory); + TransactionKey containerKey = TransactionKey.of(canonicalLocation); if (gameEvent == GameEvent.CONTAINER_OPEN) { - handleContainerOpen(golem, containerLocation, containerKey, containerType, (InventoryHolder) blockState, now); + handleContainerOpen((CopperGolem) entity, canonicalLocation, containerKey, containerType, inventoryHolder, now); } else { - handleContainerClose(golem, containerLocation, containerKey, containerType, (InventoryHolder) blockState, now); + if (entity instanceof CopperGolem) { + handleContainerClose((CopperGolem) entity, canonicalLocation, containerKey, containerType, inventoryHolder, now); + } + else if (entity == null) { + handleContainerCloseWithoutEntity(containerKey, containerType, now); + } } } + static Location getCanonicalContainerLocation(Location containerLocation, Inventory inventory) { + if (containerLocation == null || containerLocation.getWorld() == null || inventory == null) { + return containerLocation; + } + + InventoryHolder holder = inventory.getHolder(); + if (!(holder instanceof DoubleChest)) { + return containerLocation; + } + + Location doubleChestLocation = ((DoubleChest) holder).getLocation(); + if (doubleChestLocation == null) { + return containerLocation; + } + + Location canonical = new Location(containerLocation.getWorld(), doubleChestLocation.getBlockX(), doubleChestLocation.getBlockY(), doubleChestLocation.getBlockZ()); + if (canonical.getWorld() == null) { + return containerLocation; + } + + return canonical; + } + + private static boolean isCopperChest(Material material) { + return BukkitAdapter.ADAPTER != null && BukkitAdapter.ADAPTER.isCopperChest(material); + } + private void handleContainerOpen(CopperGolem golem, Location containerLocation, TransactionKey containerKey, Material containerType, InventoryHolder inventoryHolder, long nowMillis) { Inventory inventory = inventoryHolder.getInventory(); if (inventory == null) { @@ -112,7 +157,7 @@ public final class CopperGolemChestListener implements Listener { } HeldItemSnapshot held = getHeldItemSnapshot(golem); - boolean isCopperChest = BukkitAdapter.ADAPTER.isCopperChest(containerType); + boolean isCopperChest = isCopperChest(containerType); if (isCopperChest) { if (held.material != null) { return; @@ -131,7 +176,12 @@ public final class CopperGolemChestListener implements Listener { if (isCopperChest) { if (isInventoryEmpty(contents)) { - recentEmptyCopperChestSkips.put(golem.getUniqueId(), new RecentEmptyCopperChestSkip(containerKey, nowMillis)); + UUID golemId = golem.getUniqueId(); + RecentEmptyCopperChestSkip previous = recentEmptyCopperChestSkips.put(golemId, new RecentEmptyCopperChestSkip(containerKey, nowMillis)); + if (previous != null) { + unindexEmptySkip(previous.containerKey, golemId); + } + indexEmptySkip(containerKey, golemId); return; } } @@ -153,18 +203,29 @@ public final class CopperGolemChestListener implements Listener { 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); + UUID golemId = golem.getUniqueId(); + RecentEmptyCopperChestSkip removedSkip = recentEmptyCopperChestSkips.remove(golemId); + if (removedSkip != null) { + unindexEmptySkip(removedSkip.containerKey, golemId); + } + + OpenInteraction previous = openInteractions.put(golemId, interaction); + if (previous != null) { + openInteractionIndexByContainerKey.remove(previous.containerKey, new OpenInteractionIndex(golemId, previous.openedAtMillis)); + } + openInteractionIndexByContainerKey.put(containerKey, new OpenInteractionIndex(golemId, nowMillis)); } 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)) { + if (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); + if (recentEmptyCopperChestSkips.remove(golemId, emptySkip)) { + unindexEmptySkip(emptySkip.containerKey, golemId); + } return; } @@ -176,7 +237,9 @@ public final class CopperGolemChestListener implements Listener { } if (nowMillis - interaction.openedAtMillis > OPEN_INTERACTION_TIMEOUT_MILLIS) { - openInteractions.remove(golemId, interaction); + if (openInteractions.remove(golemId, interaction)) { + openInteractionIndexByContainerKey.remove(interaction.containerKey, new OpenInteractionIndex(golemId, interaction.openedAtMillis)); + } return; } @@ -185,9 +248,79 @@ public final class CopperGolemChestListener implements Listener { } openInteractions.remove(golemId, interaction); + openInteractionIndexByContainerKey.remove(interaction.containerKey, new OpenInteractionIndex(golemId, interaction.openedAtMillis)); scheduleCloseFinalize(golemId, interaction, containerKey, 1); } + private void handleContainerCloseWithoutEntity(TransactionKey containerKey, Material containerType, long nowMillis) { + if (containerKey == null) { + return; + } + + if (isCopperChest(containerType)) { + clearRecentEmptyCopperChestSkipsByKey(containerKey); + } + + OpenInteractionIndex index = openInteractionIndexByContainerKey.get(containerKey); + if (index == null) { + return; + } + + UUID golemId = index.golemId; + OpenInteraction interaction = openInteractions.get(golemId); + if (interaction == null) { + openInteractionIndexByContainerKey.remove(containerKey, index); + return; + } + if (interaction.openedAtMillis != index.openedAtMillis || !interaction.containerKey.equals(containerKey)) { + openInteractionIndexByContainerKey.remove(containerKey, index); + return; + } + if (nowMillis - interaction.openedAtMillis > OPEN_INTERACTION_TIMEOUT_MILLIS) { + if (openInteractions.remove(golemId, interaction)) { + openInteractionIndexByContainerKey.remove(containerKey, index); + } + return; + } + + if (openInteractions.remove(golemId, interaction)) { + openInteractionIndexByContainerKey.remove(containerKey, index); + scheduleCloseFinalize(golemId, interaction, containerKey, 1); + } + } + + private void clearRecentEmptyCopperChestSkipsByKey(TransactionKey containerKey) { + Set golemIds = emptySkipGolemsByContainerKey.remove(containerKey); + if (golemIds == null || golemIds.isEmpty()) { + return; + } + + for (UUID golemId : golemIds) { + RecentEmptyCopperChestSkip skip = recentEmptyCopperChestSkips.get(golemId); + if (skip != null && skip.containerKey.equals(containerKey)) { + recentEmptyCopperChestSkips.remove(golemId, skip); + } + } + } + + private void indexEmptySkip(TransactionKey containerKey, UUID golemId) { + emptySkipGolemsByContainerKey.computeIfAbsent(containerKey, key -> ConcurrentHashMap.newKeySet()).add(golemId); + } + + private void unindexEmptySkip(TransactionKey containerKey, UUID golemId) { + if (containerKey == null || golemId == null) { + return; + } + Set golemIds = emptySkipGolemsByContainerKey.get(containerKey); + if (golemIds == null) { + return; + } + golemIds.remove(golemId); + if (golemIds.isEmpty()) { + emptySkipGolemsByContainerKey.remove(containerKey, golemIds); + } + } + 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); } @@ -240,7 +373,7 @@ public final class CopperGolemChestListener implements Listener { return; } - if (!BukkitAdapter.ADAPTER.isCopperChest(containerType)) { + if (!isCopperChest(containerType)) { return; } @@ -259,7 +392,7 @@ public final class CopperGolemChestListener implements Listener { } BlockState blockState = containerLocation.getBlock().getState(); - if (!(blockState instanceof InventoryHolder) || !BukkitAdapter.ADAPTER.isCopperChest(blockState.getType())) { + if (!(blockState instanceof InventoryHolder) || !isCopperChest(blockState.getType())) { return; } @@ -283,7 +416,7 @@ public final class CopperGolemChestListener implements Listener { } private boolean isAttributableToGolem(CopperGolem golem, OpenInteraction interaction, ItemStack[] currentContents) { - boolean isCopperChest = BukkitAdapter.ADAPTER.isCopperChest(interaction.containerType); + boolean isCopperChest = isCopperChest(interaction.containerType); HeldItemSnapshot heldNow = getHeldItemSnapshot(golem); if (isCopperChest) { @@ -389,11 +522,23 @@ public final class CopperGolemChestListener implements Listener { } String loggingContainerId = USERNAME + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ(); + List forceList = ConfigHandler.forceContainer.get(loggingContainerId); + List oldList = ConfigHandler.oldContainer.get(loggingContainerId); + + boolean hasPendingBaseline = oldList != null && !oldList.isEmpty(); + boolean hasStaleForceSnapshots = forceList != null && !forceList.isEmpty() && (forceList.get(0) == null || forceList.get(0).length != snapshot.length); + + if (!hasPendingBaseline || hasStaleForceSnapshots) { + ConfigHandler.forceContainer.remove(loggingContainerId); + forceList = null; + } + if (forceList == null) { forceList = Collections.synchronizedList(new ArrayList<>()); ConfigHandler.forceContainer.put(loggingContainerId, forceList); } + forceList.add(snapshot); } @@ -408,7 +553,9 @@ public final class CopperGolemChestListener implements Listener { 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); + if (openInteractions.remove(golemId, interaction) && interaction != null) { + openInteractionIndexByContainerKey.remove(interaction.containerKey, new OpenInteractionIndex(golemId, interaction.openedAtMillis)); + } } } @@ -416,7 +563,9 @@ public final class CopperGolemChestListener implements Listener { 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); + if (recentEmptyCopperChestSkips.remove(golemId, skip) && skip != null) { + unindexEmptySkip(skip.containerKey, golemId); + } } } } @@ -625,4 +774,32 @@ public final class CopperGolemChestListener implements Listener { } } + + private static final class OpenInteractionIndex { + + private final UUID golemId; + private final long openedAtMillis; + + private OpenInteractionIndex(UUID golemId, long openedAtMillis) { + this.golemId = golemId; + this.openedAtMillis = openedAtMillis; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof OpenInteractionIndex)) { + return false; + } + OpenInteractionIndex other = (OpenInteractionIndex) obj; + return openedAtMillis == other.openedAtMillis && golemId.equals(other.golemId); + } + + @Override + public int hashCode() { + return Objects.hash(golemId, openedAtMillis); + } + } } From 7386f4bd50cffc8c77590081769a4b52b2a7f12e Mon Sep 17 00:00:00 2001 From: Alberto Gusmeroli <96133902+guss-alberto@users.noreply.github.com> Date: Wed, 17 Dec 2025 21:30:50 +0100 Subject: [PATCH 11/11] Add support for Shelf item logging (#833) * add shelf logging * bump supported version to 1.21.11 * cleanup * Update pom.xml Remove project branch definition from properties. * fix incorrect instanceof check --------- Co-authored-by: Intelli <6790859+Intelli@users.noreply.github.com> --- .../net/coreprotect/bukkit/BukkitAdapter.java | 10 ++++ .../coreprotect/bukkit/BukkitInterface.java | 13 ++++++ .../net/coreprotect/bukkit/Bukkit_v1_21.java | 22 ++++++++- .../player/PlayerInteractListener.java | 46 +++++++++++++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/coreprotect/bukkit/BukkitAdapter.java b/src/main/java/net/coreprotect/bukkit/BukkitAdapter.java index 23f9207..dcd42ba 100644 --- a/src/main/java/net/coreprotect/bukkit/BukkitAdapter.java +++ b/src/main/java/net/coreprotect/bukkit/BukkitAdapter.java @@ -366,8 +366,18 @@ public class BukkitAdapter implements BukkitInterface { return false; } + @Override + public boolean isShelf(Material material){ + return false; + } + @Override public Set copperChestMaterials() { return EMPTY_SET; } + + @Override + public Set shelfMaterials() { + return EMPTY_SET; + } } diff --git a/src/main/java/net/coreprotect/bukkit/BukkitInterface.java b/src/main/java/net/coreprotect/bukkit/BukkitInterface.java index c66d7fc..9ccebed 100644 --- a/src/main/java/net/coreprotect/bukkit/BukkitInterface.java +++ b/src/main/java/net/coreprotect/bukkit/BukkitInterface.java @@ -132,6 +132,17 @@ public interface BukkitInterface { */ boolean isChiseledBookshelf(Material material); + + /** + * Checks if a material is a shelf of any wood kind. + * + * @param material + * The material to check + * @return true if the material is a shelf, false otherwise + */ + boolean isShelf(Material material); + + /** * Checks if a material is a bookshelf book. * @@ -441,4 +452,6 @@ public interface BukkitInterface { Set copperChestMaterials(); + Set shelfMaterials(); + } diff --git a/src/main/java/net/coreprotect/bukkit/Bukkit_v1_21.java b/src/main/java/net/coreprotect/bukkit/Bukkit_v1_21.java index 4d52be8..4d0e252 100644 --- a/src/main/java/net/coreprotect/bukkit/Bukkit_v1_21.java +++ b/src/main/java/net/coreprotect/bukkit/Bukkit_v1_21.java @@ -23,9 +23,10 @@ import net.coreprotect.model.BlockGroup; * - Registry handling for named objects * - Updated interaction blocks */ -public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface { +public class Bukkit_v1_21 extends Bukkit_v1_20 { public static Set COPPER_CHESTS = new HashSet<>(Arrays.asList()); + public static Set SHELVES = new HashSet<>(Arrays.asList()); /** * Initializes the Bukkit_v1_21 adapter with 1.21-specific block groups and mappings. @@ -37,6 +38,7 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface { BlockGroup.INTERACT_BLOCKS.addAll(copperChestMaterials()); BlockGroup.CONTAINERS.addAll(copperChestMaterials()); BlockGroup.UPDATE_STATE.addAll(copperChestMaterials()); + BlockGroup.CONTAINERS.addAll(shelfMaterials()); } /** @@ -111,6 +113,7 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface { return ((Keyed) value).getKey().toString(); } + /** * Gets a registry value from a key string and class. * Used for deserializing registry objects. @@ -177,6 +180,11 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface { return false; } + @Override + public boolean isShelf(Material material) { + return SHELVES.contains(material); + } + @Override public Set copperChestMaterials() { if (COPPER_CHESTS.isEmpty()) { @@ -198,4 +206,16 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface { return COPPER_CHESTS; } + + @Override + public Set shelfMaterials() { + if (SHELVES.isEmpty()) { + Material shelf = Material.getMaterial("OAK_SHELF"); + if (shelf != null) { + SHELVES.addAll(Tag.WOODEN_SHELVES.getValues()); + } + } + + return SHELVES; + } } diff --git a/src/main/java/net/coreprotect/listener/player/PlayerInteractListener.java b/src/main/java/net/coreprotect/listener/player/PlayerInteractListener.java index 680aa61..9f43ff5 100755 --- a/src/main/java/net/coreprotect/listener/player/PlayerInteractListener.java +++ b/src/main/java/net/coreprotect/listener/player/PlayerInteractListener.java @@ -18,12 +18,14 @@ import org.bukkit.block.Jukebox; import org.bukkit.block.Sign; import org.bukkit.block.data.Bisected; import org.bukkit.block.data.Bisected.Half; +import org.bukkit.block.data.SideChaining.ChainPart; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.Lightable; import org.bukkit.block.data.Waterlogged; import org.bukkit.block.data.type.Bed; import org.bukkit.block.data.type.Bed.Part; import org.bukkit.block.data.type.Cake; +import org.bukkit.block.data.type.Shelf; import org.bukkit.entity.EnderCrystal; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -37,6 +39,7 @@ import org.bukkit.inventory.BlockInventoryHolder; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; import net.coreprotect.CoreProtect; import net.coreprotect.bukkit.BukkitAdapter; @@ -465,6 +468,49 @@ public final class PlayerInteractListener extends Queue implements Listener { InventoryChangeListener.inventoryTransaction(player.getName(), blockState.getLocation(), null); } } + } else if (BukkitAdapter.ADAPTER.isShelf(type) ){ + BlockData blockState = block.getBlockData(); + if (blockState instanceof Shelf){ + Shelf shelf = (Shelf) blockState; + + // ignore clicking on the back face + if (event.getBlockFace() != shelf.getFacing()){ + return; + } + + if (shelf.getSideChain() == ChainPart.UNCONNECTED){ + InventoryChangeListener.inventoryTransaction(player.getName(), block.getLocation(), null); + } else { + Block center = block; + Vector direction = shelf.getFacing().getDirection(); + + if (shelf.getSideChain() == ChainPart.LEFT){ + center = center.getRelative(direction.getBlockZ(), 0, -direction.getBlockX()); + } else if (shelf.getSideChain() == ChainPart.RIGHT){ + center = center.getRelative(-direction.getBlockZ(), 0, direction.getBlockX()); + } + + BlockData centerBlockData = center.getBlockData(); + if (centerBlockData instanceof Shelf){ + // log center + InventoryChangeListener.inventoryTransaction(player.getName(), center.getLocation(), null); + + if (((Shelf)centerBlockData).getSideChain() != ChainPart.CENTER){ + // if it's not the center it's just a chain of 2 + InventoryChangeListener.inventoryTransaction(player.getName(), block.getLocation(), null); + } else { + Block left = center.getRelative(-direction.getBlockZ(), 0, direction.getBlockX()); + InventoryChangeListener.inventoryTransaction(player.getName(), left.getLocation(), null); + + Block right = center.getRelative(direction.getBlockZ(), 0, -direction.getBlockX()); + InventoryChangeListener.inventoryTransaction(player.getName(), right.getLocation(), null); + } + } else { + // fallback if invalid block is found just log clicked shelf + InventoryChangeListener.inventoryTransaction(player.getName(), block.getLocation(), null); + } + } + } } else if (BukkitAdapter.ADAPTER.isDecoratedPot(type)) { BlockState blockState = block.getState();