diff --git a/README.md b/README.md index 92a9e2d..a5ba2ea 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Maven org.zhdev.griefus griefus - 23.0 + 23.1 provided ``` 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 3ec7de6..eda373e 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: diff --git a/pom.xml b/pom.xml index ab617a0..efa173a 100755 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.zhdev.griefus griefus - 23.0-SNAPSHOT + 23.1-SNAPSHOT master UTF-8 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/config/ConfigHandler.java b/src/main/java/net/coreprotect/config/ConfigHandler.java index 3deac5d..e786cc3 100644 --- a/src/main/java/net/coreprotect/config/ConfigHandler.java +++ b/src/main/java/net/coreprotect/config/ConfigHandler.java @@ -51,8 +51,8 @@ public class ConfigHandler extends Queue { public static final String EDITION_NAME = VersionUtils.getPluginName(); 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 PATCH_VERSION = "23.1"; + public static final String LATEST_VERSION = "1.21.11"; public static String path = "plugins/Griefus/"; public static String sqlite = "database.db"; public static String host = "127.0.0.1"; diff --git a/src/main/java/net/coreprotect/database/logger/ContainerLogger.java b/src/main/java/net/coreprotect/database/logger/ContainerLogger.java index d0aa141..bb9a718 100644 --- a/src/main/java/net/coreprotect/database/logger/ContainerLogger.java +++ b/src/main/java/net/coreprotect/database/logger/ContainerLogger.java @@ -67,7 +67,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(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 @@ -95,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 (player.equals("#dispenser")) { + if ("#dispenser".equals(player)) { String[] parts = loggingContainerId.split("\\."); if (parts.length >= 4) { int x = Integer.parseInt(parts[1]); 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/listener/player/PlayerInteractListener.java b/src/main/java/net/coreprotect/listener/player/PlayerInteractListener.java index e186884..c638a85 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(); 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..5541dc0 --- /dev/null +++ b/src/main/java/net/coreprotect/paper/listener/CopperGolemChestListener.java @@ -0,0 +1,805 @@ +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.Set; +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.BlockState; +import org.bukkit.block.DoubleChest; +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 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 USERNAME = "#copper_golem"; + 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 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) { + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + 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 (gameEvent == GameEvent.CONTAINER_OPEN) { + if (!(entity instanceof CopperGolem)) { + return; + } + } + else { + if (entity != null && !(entity instanceof CopperGolem)) { + return; + } + } + + 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 containerLocation = blockState.getLocation(); + if (containerLocation == null || containerLocation.getWorld() == null) { + return; + } + + Material containerType = blockState.getType(); + boolean isCopperChest = isCopperChest(containerType); + boolean isStandardChest = containerType == Material.CHEST || containerType == Material.TRAPPED_CHEST; + if (!isCopperChest && !isStandardChest) { + return; + } + + long now = System.currentTimeMillis(); + cleanupOpenInteractions(now); + + InventoryHolder inventoryHolder = (InventoryHolder) blockState; + Inventory inventory = inventoryHolder.getInventory(); + Location canonicalLocation = getCanonicalContainerLocation(containerLocation, inventory); + TransactionKey containerKey = TransactionKey.of(canonicalLocation); + + if (gameEvent == GameEvent.CONTAINER_OPEN) { + handleContainerOpen((CopperGolem) entity, canonicalLocation, containerKey, containerType, inventoryHolder, now); + } + else { + 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) { + return; + } + + HeldItemSnapshot held = getHeldItemSnapshot(golem); + boolean isCopperChest = 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)) { + UUID golemId = golem.getUniqueId(); + RecentEmptyCopperChestSkip previous = recentEmptyCopperChestSkips.put(golemId, new RecentEmptyCopperChestSkip(containerKey, nowMillis)); + if (previous != null) { + unindexEmptySkip(previous.containerKey, golemId); + } + indexEmptySkip(containerKey, golemId); + 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); + 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 (isCopperChest(containerType)) { + RecentEmptyCopperChestSkip emptySkip = recentEmptyCopperChestSkips.get(golemId); + if (emptySkip != null && emptySkip.containerKey.equals(containerKey) && (nowMillis - emptySkip.skippedAtMillis) <= EMPTY_COPPER_CHEST_SKIP_TTL_MILLIS) { + if (recentEmptyCopperChestSkips.remove(golemId, emptySkip)) { + unindexEmptySkip(emptySkip.containerKey, golemId); + } + return; + } + + scheduleUntrackedCopperChestCloseFinalize(golemId, containerLocation.clone(), containerType, 1); + return; + } + + return; + } + + if (nowMillis - interaction.openedAtMillis > OPEN_INTERACTION_TIMEOUT_MILLIS) { + if (openInteractions.remove(golemId, interaction)) { + openInteractionIndexByContainerKey.remove(interaction.containerKey, new OpenInteractionIndex(golemId, interaction.openedAtMillis)); + } + return; + } + + if (!interaction.containerKey.equals(containerKey)) { + return; + } + + 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); + } + + 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 (!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) || !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 = 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; + } + + 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); + 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); + } + + 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) { + if (openInteractions.remove(golemId, interaction) && interaction != null) { + openInteractionIndexByContainerKey.remove(interaction.containerKey, new OpenInteractionIndex(golemId, interaction.openedAtMillis)); + } + } + } + + 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) { + if (recentEmptyCopperChestSkips.remove(golemId, skip) && skip != null) { + unindexEmptySkip(skip.containerKey, golemId); + } + } + } + } + + 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 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 boolean containsOnlyMaterial(ItemStack[] state, Material material) { + if (state == null || material == null) { + return false; + } + for (ItemStack item : state) { + if (isEmptyItem(item)) { + continue; + } + if (item.getType() != material) { + return false; + } + } + return true; + } + + private boolean hasSpaceForMaterial(ItemStack[] state, Material material) { + if (state == null || material == null) { + return false; + } + + int maxStackSize = material.getMaxStackSize(); + for (ItemStack item : state) { + if (isEmptyItem(item)) { + return true; + } + if (item.getType() == material && item.getAmount() < maxStackSize) { + return true; + } + } + + 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 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 static final class HeldItemSnapshot { + + private static final HeldItemSnapshot EMPTY = new HeldItemSnapshot(null, 0); + + private final Material material; + private final int amount; + + private HeldItemSnapshot(Material material, int amount) { + this.material = material; + this.amount = amount; + } + } + + 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); + } + + } + + 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); + } + } +} 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(); } } 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; + } + }