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; - } -}