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