Merge branch 'master' of github.com:PlayPro/CoreProtect

# Conflicts:
#	README.md
#	pom.xml
#	src/main/java/net/coreprotect/config/ConfigHandler.java
This commit is contained in:
Roman Zhuravlev 2026-01-15 13:43:09 +05:00
commit df6f20ef97
14 changed files with 923 additions and 16 deletions

View file

@ -15,7 +15,7 @@ Maven
<dependency> <dependency>
<groupId>org.zhdev.griefus</groupId> <groupId>org.zhdev.griefus</groupId>
<artifactId>griefus</artifactId> <artifactId>griefus</artifactId>
<version>23.0</version> <version>23.1</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
``` ```

View file

@ -4,8 +4,8 @@ The CoreProtect API enables you to log your own block changes, perform lookups,
| API Details | | | API Details | |
| --- | --- | | --- | --- |
| **API Version:** | 10 | | **API Version:** | 11 |
| **Plugin Version:** | v22.4+ | | **Plugin Version:** | v23.1+ |
| **Maven:** | [maven.playpro.com](https://maven.playpro.com) | | **Maven:** | [maven.playpro.com](https://maven.playpro.com) |
*Documentation for the API version 10 can be found [here](/api/version/v10/).* *Documentation for the API version 10 can be found [here](/api/version/v11/).*

View file

@ -5,7 +5,7 @@ The CoreProtect API enables you to log your own block changes, perform lookups,
| API Details | | | API Details | |
| --- | --- | | --- | --- |
| **API Version:** | 11 | | **API Version:** | 11 |
| **Plugin Version:** | v24.0+ | | **Plugin Version:** | v23.1+ |
| **Maven:** | [maven.playpro.com](https://maven.playpro.com) | | **Maven:** | [maven.playpro.com](https://maven.playpro.com) |
--- ---
@ -28,8 +28,8 @@ CoreProtectPreLogEvent(String user)
## Getting Started ## Getting Started
Ensure you're using CoreProtect 24.0 or higher. Add it as an external jar to your plugin in your IDE. 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, 24.0). 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: The first thing you need to do is get access to CoreProtect. You can do this by using code similar to the following:

View file

@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.zhdev.griefus</groupId> <groupId>org.zhdev.griefus</groupId>
<artifactId>griefus</artifactId> <artifactId>griefus</artifactId>
<version>23.0-SNAPSHOT</version> <version>23.1-SNAPSHOT</version>
<properties> <properties>
<project.branch>master</project.branch> <project.branch>master</project.branch>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View file

@ -366,8 +366,18 @@ public class BukkitAdapter implements BukkitInterface {
return false; return false;
} }
@Override
public boolean isShelf(Material material){
return false;
}
@Override @Override
public Set<Material> copperChestMaterials() { public Set<Material> copperChestMaterials() {
return EMPTY_SET; return EMPTY_SET;
} }
@Override
public Set<Material> shelfMaterials() {
return EMPTY_SET;
}
} }

View file

@ -132,6 +132,17 @@ public interface BukkitInterface {
*/ */
boolean isChiseledBookshelf(Material material); 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. * Checks if a material is a bookshelf book.
* *
@ -441,4 +452,6 @@ public interface BukkitInterface {
Set<Material> copperChestMaterials(); Set<Material> copperChestMaterials();
Set<Material> shelfMaterials();
} }

View file

@ -23,9 +23,10 @@ import net.coreprotect.model.BlockGroup;
* - Registry handling for named objects * - Registry handling for named objects
* - Updated interaction blocks * - 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<Material> COPPER_CHESTS = new HashSet<>(Arrays.asList()); public static Set<Material> COPPER_CHESTS = new HashSet<>(Arrays.asList());
public static Set<Material> SHELVES = new HashSet<>(Arrays.asList());
/** /**
* Initializes the Bukkit_v1_21 adapter with 1.21-specific block groups and mappings. * 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.INTERACT_BLOCKS.addAll(copperChestMaterials());
BlockGroup.CONTAINERS.addAll(copperChestMaterials()); BlockGroup.CONTAINERS.addAll(copperChestMaterials());
BlockGroup.UPDATE_STATE.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(); return ((Keyed) value).getKey().toString();
} }
/** /**
* Gets a registry value from a key string and class. * Gets a registry value from a key string and class.
* Used for deserializing registry objects. * Used for deserializing registry objects.
@ -177,6 +180,11 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
return false; return false;
} }
@Override
public boolean isShelf(Material material) {
return SHELVES.contains(material);
}
@Override @Override
public Set<Material> copperChestMaterials() { public Set<Material> copperChestMaterials() {
if (COPPER_CHESTS.isEmpty()) { if (COPPER_CHESTS.isEmpty()) {
@ -198,4 +206,16 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
return COPPER_CHESTS; return COPPER_CHESTS;
} }
@Override
public Set<Material> shelfMaterials() {
if (SHELVES.isEmpty()) {
Material shelf = Material.getMaterial("OAK_SHELF");
if (shelf != null) {
SHELVES.addAll(Tag.WOODEN_SHELVES.getValues());
}
}
return SHELVES;
}
} }

View file

@ -51,8 +51,8 @@ public class ConfigHandler extends Queue {
public static final String EDITION_NAME = VersionUtils.getPluginName(); public static final String EDITION_NAME = VersionUtils.getPluginName();
public static final String JAVA_VERSION = "11.0"; public static final String JAVA_VERSION = "11.0";
public static final String MINECRAFT_VERSION = "1.16"; public static final String MINECRAFT_VERSION = "1.16";
public static final String PATCH_VERSION = "23.0"; public static final String PATCH_VERSION = "23.1";
public static final String LATEST_VERSION = "1.21.10"; public static final String LATEST_VERSION = "1.21.11";
public static String path = "plugins/Griefus/"; public static String path = "plugins/Griefus/";
public static String sqlite = "database.db"; public static String sqlite = "database.db";
public static String host = "127.0.0.1"; public static String host = "127.0.0.1";

View file

@ -67,7 +67,7 @@ public class ContainerLogger extends Queue {
} }
// Check if this is a dispenser with no actual changes // 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 // No changes detected, mark this dispenser in the dispenserNoChange map
// Extract the location key from the loggingContainerId // Extract the location key from the loggingContainerId
// Format: #dispenser.x.y.z // Format: #dispenser.x.y.z
@ -95,7 +95,7 @@ public class ContainerLogger extends Queue {
// If we reach here, the dispenser event resulted in changes // If we reach here, the dispenser event resulted in changes
// Remove any pending event for this dispenser // Remove any pending event for this dispenser
if (player.equals("#dispenser")) { if ("#dispenser".equals(player)) {
String[] parts = loggingContainerId.split("\\."); String[] parts = loggingContainerId.split("\\.");
if (parts.length >= 4) { if (parts.length >= 4) {
int x = Integer.parseInt(parts[1]); int x = Integer.parseInt(parts[1]);

View file

@ -55,6 +55,7 @@ import net.coreprotect.listener.world.LeavesDecayListener;
import net.coreprotect.listener.world.PortalCreateListener; import net.coreprotect.listener.world.PortalCreateListener;
import net.coreprotect.listener.world.StructureGrowListener; import net.coreprotect.listener.world.StructureGrowListener;
import net.coreprotect.paper.listener.BlockPreDispenseListener; import net.coreprotect.paper.listener.BlockPreDispenseListener;
import net.coreprotect.paper.listener.CopperGolemChestListener;
import net.coreprotect.paper.listener.PaperChatListener; import net.coreprotect.paper.listener.PaperChatListener;
public final class ListenerHandler { public final class ListenerHandler {
@ -72,6 +73,14 @@ public final class ListenerHandler {
BlockPreDispenseListener.useBlockPreDispenseEvent = false; 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 // Block Listeners
pluginManager.registerEvents(new BlockBreakListener(), plugin); pluginManager.registerEvents(new BlockBreakListener(), plugin);
pluginManager.registerEvents(new BlockBurnListener(), plugin); pluginManager.registerEvents(new BlockBurnListener(), plugin);

View file

@ -18,12 +18,14 @@ import org.bukkit.block.Jukebox;
import org.bukkit.block.Sign; import org.bukkit.block.Sign;
import org.bukkit.block.data.Bisected; import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.Bisected.Half; 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.BlockData;
import org.bukkit.block.data.Lightable; import org.bukkit.block.data.Lightable;
import org.bukkit.block.data.Waterlogged; import org.bukkit.block.data.Waterlogged;
import org.bukkit.block.data.type.Bed; import org.bukkit.block.data.type.Bed;
import org.bukkit.block.data.type.Bed.Part; import org.bukkit.block.data.type.Bed.Part;
import org.bukkit.block.data.type.Cake; import org.bukkit.block.data.type.Cake;
import org.bukkit.block.data.type.Shelf;
import org.bukkit.entity.EnderCrystal; import org.bukkit.entity.EnderCrystal;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -37,6 +39,7 @@ import org.bukkit.inventory.BlockInventoryHolder;
import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import net.coreprotect.CoreProtect; import net.coreprotect.CoreProtect;
import net.coreprotect.bukkit.BukkitAdapter; import net.coreprotect.bukkit.BukkitAdapter;
@ -465,6 +468,49 @@ public final class PlayerInteractListener extends Queue implements Listener {
InventoryChangeListener.inventoryTransaction(player.getName(), blockState.getLocation(), null); 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)) { else if (BukkitAdapter.ADAPTER.isDecoratedPot(type)) {
BlockState blockState = block.getState(); BlockState blockState = block.getState();

View file

@ -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<UUID, OpenInteraction> openInteractions = new ConcurrentHashMap<>();
private final Map<UUID, RecentEmptyCopperChestSkip> recentEmptyCopperChestSkips = new ConcurrentHashMap<>();
private final Map<TransactionKey, OpenInteractionIndex> openInteractionIndexByContainerKey = new ConcurrentHashMap<>();
private final Map<TransactionKey, Set<UUID>> 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<UUID> 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<UUID> 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<ItemStack[]> forceList = ConfigHandler.forceContainer.get(loggingContainerId);
List<ItemStack[]> 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<UUID, OpenInteraction> 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<UUID, RecentEmptyCopperChestSkip> 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);
}
}
}

View file

@ -7,7 +7,6 @@ import org.bukkit.Location;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.scheduler.BukkitTask; import org.bukkit.scheduler.BukkitTask;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
import net.coreprotect.CoreProtect; import net.coreprotect.CoreProtect;
import net.coreprotect.config.ConfigHandler; import net.coreprotect.config.ConfigHandler;
@ -116,8 +115,8 @@ public class Scheduler {
public static void cancelTask(Object task) { public static void cancelTask(Object task) {
if (ConfigHandler.isFolia) { if (ConfigHandler.isFolia) {
if (task instanceof ScheduledTask) { if (task instanceof io.papermc.paper.threadedregions.scheduler.ScheduledTask) {
ScheduledTask scheduledTask = (ScheduledTask) task; io.papermc.paper.threadedregions.scheduler.ScheduledTask scheduledTask = (io.papermc.paper.threadedregions.scheduler.ScheduledTask) task;
scheduledTask.cancel(); scheduledTask.cancel();
} }
} }

View file

@ -196,4 +196,9 @@ public final class WorldEditBlockState implements BlockState {
return null; return null;
} }
@Override
public boolean isSuffocating() {
return false;
}
} }