14 Commits
Author SHA1 Message Date
Intelli bf0e9cf661 CoreProtect Community Edition v23.2 2026-04-22 15:07:18 -06:00
Intelli 71b673756f Remove duplicate section in build.yml 2026-04-22 15:00:31 -06:00
Intelli 2f794d6398 Build with Java 25 2026-04-22 14:57:12 -06:00
Intelli f3a1d95b77 Merge branch 'master' of github.com:PlayPro/CoreProtect 2026-04-22 14:52:01 -06:00
Intelli 8c79b3a867 Update dependencies & bump supported MC version to 26.1 2026-04-22 14:51:54 -06:00
Alisa Gusmeroli d87ecaee42 fix issue so coloured bundles are rolled back properly (#872) 2026-03-23 14:59:07 -06:00
Alisa Gusmeroli dc4e9f8d60 Prevent logging of wind burst explosions and improve BlockExplodeListener (fixes #830) 2026-03-23 14:58:12 -06:00
Intelli c145fca68c Fix UnsupportedOperationException in Folia (fixes #842) 2026-02-27 11:01:29 -07:00
Intelli b0856b51a5 Update documentation 2026-01-22 16:58:23 -07:00
Alberto GusmeroliandIntelli 7386f4bd50 Add support for Shelf item logging (#833)
* add shelf logging

* bump supported version to 1.21.11

* cleanup

* Update pom.xml

Remove project branch definition from properties.

* fix incorrect instanceof check

---------

Co-authored-by: Intelli <6790859+Intelli@users.noreply.github.com>
2025-12-17 13:30:50 -07:00
Intelli 0f0ceccb7b Fix copper golem logging not working with double chests
Works around Paper bug where the entity is null when a golem closes a double chest
2025-12-15 16:11:03 -07:00
Intelli 893ce9af46 Update API docs 2025-12-15 13:50:50 -07:00
Intelli 609990e344 Finalized copper golem logging 2025-12-14 17:56:43 -07:00
Intelli 38166fd381 Re-enable experimental copper golem logging (WIP) 2025-12-14 14:56:30 -07:00
19 changed files with 922 additions and 248 deletions

No files matched your search

+3 -8
View File
@@ -13,16 +13,11 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '21'
java-version: '25'
distribution: 'temurin'
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
cache: 'maven'
- name: Build
run: mvn -B verify
+2 -2
View File
@@ -18,7 +18,7 @@ For a detailed description of the plugin, please visit [coreprotect.net](https:/
| CoreProtect Discord: | [discord.gg/b4DZ4jy](https://discord.gg/b4DZ4jy) |
| CoreProtect Patreon: | [patreon.com/coreprotect](https://www.patreon.com/coreprotect) |
| CoreProtect Documentation: | [docs.coreprotect.net](https://docs.coreprotect.net) |
| Downloads for MC 1.14 - 1.21: | [coreprotect.net/latest](https://coreprotect.net/latest/) |
| Downloads for MC 1.14 - 26.1: | [coreprotect.net/latest](https://coreprotect.net/latest/) |
| Downloads for MC 1.8 - 1.12: | [coreprotect.net/legacy](https://coreprotect.net/legacy/) |
bStats
@@ -41,7 +41,7 @@ Maven
<dependency>
<groupId>net.coreprotect</groupId>
<artifactId>coreprotect</artifactId>
<version>23.1</version>
<version>23.2</version>
<scope>provided</scope>
</dependency>
```
+3 -3
View File
@@ -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/).*
*Documentation for the API version 10 can be found [here](/api/version/v11/).*
+3 -3
View File
@@ -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:
+2
View File
@@ -46,6 +46,8 @@ The following permissions can be used to restrict functionality within the plugi
## Child Permissions
> **Note:** Child permissions are not presently supported by CoreProtect Hytale Edition.
* **coreprotect.lookup.block** *(default: op)*
Can be optionally used as a negative permission to prevent block lookups.
&nbsp;
+25 -25
View File
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>net.coreprotect</groupId>
<artifactId>CoreProtect</artifactId>
<version>23.1</version>
<version>23.2</version>
<properties>
<project.branch></project.branch>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -26,7 +26,7 @@
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<version>3.15.0</version>
<configuration>
<source>11</source>
<target>11</target>
@@ -47,7 +47,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<version>3.5.0</version>
<configuration>
<archive>
<manifestEntries>
@@ -59,7 +59,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<version>3.6.2</version>
<executions>
<execution>
<phase>package</phase>
@@ -105,7 +105,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.3</version>
<version>3.5.5</version>
<configuration>
<systemPropertyVariables>
<plugin.version>${project.version}</plugin.version>
@@ -116,7 +116,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.1</version>
<version>3.6.2</version>
<executions>
<execution>
<phase>validate</phase>
@@ -152,7 +152,7 @@
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<version>3.2.0</version>
<executions>
<execution>
<id>install-validation</id>
@@ -209,32 +209,32 @@
<dependencies>
<!--
paper-api: io.papermc.paper
paper-api (<= 1.16): com.destroystokyo.paper
paper-api (<= 1.16): com.destroystokyo.paper / 1.16.1-R0.1-SNAPSHOT
spigot-api: org.spigotmc
bukkit: org.bukkit
-->
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21.11-R0.1-SNAPSHOT</version>
<version>26.1.2.build.9-alpha</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.2</version>
<version>3.2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fastasyncworldedit</groupId>
<artifactId>FastAsyncWorldEdit-Core</artifactId>
<version>2.13.1</version>
<version>2.15.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fastasyncworldedit</groupId>
<artifactId>FastAsyncWorldEdit-Bukkit</artifactId>
<version>2.13.1</version>
<version>2.15.0</version>
<scope>provided</scope>
<exclusions>
<exclusion>
@@ -246,22 +246,22 @@
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
<version>7.0.2</version>
</dependency>
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.6.2</version>
<version>6.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
<version>2.25.4</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.19.0</version>
<version>2.25.4</version>
</dependency>
<dependency>
<groupId>com.github.DeadSilenceIV</groupId>
@@ -273,31 +273,31 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.1</version>
<version>5.14.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.1</version>
<version>5.14.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.1</version>
<version>5.14.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.16.1</version>
<version>5.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.16.1</version>
<version>5.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -309,25 +309,25 @@
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.1.0</version>
<version>3.53.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.12</version>
<version>2.0.17</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.16.1</version>
<version>1.18.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.16.1</version>
<version>1.18.8</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -22,6 +22,8 @@ import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.LivingEntity;
import org.bukkit.event.Event;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerInteractEvent;
@@ -316,6 +318,17 @@ public class BukkitAdapter implements BukkitInterface {
return false;
}
@Override
public boolean shouldLogExplosion(Event event){
return true;
}
@Override
public Material getExplodedBlock(BlockExplodeEvent event){
// accoding to the Bukkit docs this will always return air
return event.getBlock().getType();
}
@Override
public void setGlowing(Sign sign, boolean isFront, boolean isGlowing) {
// Base implementation does nothing
@@ -361,13 +374,29 @@ public class BukkitAdapter implements BukkitInterface {
return false;
}
@Override
public boolean isBundle(Material material) {
return false;
}
@Override
public boolean isCopperChest(Material material) {
return false;
}
@Override
public boolean isShelf(Material material){
return false;
}
@Override
public Set<Material> copperChestMaterials() {
return EMPTY_SET;
}
@Override
public Set<Material> shelfMaterials() {
return EMPTY_SET;
}
}
@@ -14,6 +14,8 @@ import org.bukkit.entity.Arrow;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.event.Event;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerInteractEvent;
@@ -132,6 +134,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.
*
@@ -141,6 +154,17 @@ public interface BukkitInterface {
*/
boolean isBookshelfBook(Material material);
/**
* Checks if a material is a bundle.
*
* @param material
* The material to check
* @return true if the material is a bundle, false otherwise
*/
boolean isBundle(Material material);
/**
* Gets the seeds material for a plant material.
*
@@ -402,6 +426,27 @@ public interface BukkitInterface {
*/
boolean isSignFront(SignChangeEvent event);
/**
* Checks whether an explosion event should be logged or not. (i.e. wind charge explosions)
*
* @param event
* The explosion event (Block or Entity ExplodeEvent)
* @return true if the explosion should affect blocks
*/
boolean shouldLogExplosion(Event event);
/**
* Gets the material of the block that exploded
*
* @param event
* The block explosion event
* @return the material of the block that caused the explosion
*/
Material getExplodedBlock(BlockExplodeEvent event);
// --------------------------------------------------------------------------
// Registry methods
// --------------------------------------------------------------------------
@@ -441,4 +486,6 @@ public interface BukkitInterface {
Set<Material> copperChestMaterials();
Set<Material> shelfMaterials();
}
@@ -152,9 +152,15 @@ public class Bukkit_v1_17 extends BukkitAdapter {
return super.getItemMeta(itemMeta, list, metadata, slot);
}
@Override
public boolean isBundle(Material material) {
return material == Material.BUNDLE;
}
@Override
public boolean setItemMeta(Material rowType, ItemStack itemstack, List<Map<String, Object>> map) {
if (rowType == Material.BUNDLE) {
if (BukkitAdapter.ADAPTER.isBundle(rowType)) {
BundleMeta meta = (BundleMeta) itemstack.getItemMeta();
for (Map<String, Object> itemData : map) {
ItemStack itemStack = ItemUtils.unserializeItemStack(itemData);
@@ -13,6 +13,7 @@ import org.bukkit.block.ChiseledBookshelf;
import org.bukkit.block.Sign;
import org.bukkit.block.sign.Side;
import org.bukkit.entity.Arrow;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
@@ -297,4 +298,10 @@ public class Bukkit_v1_20 extends Bukkit_v1_19 {
return super.getArrowMeta(arrow, itemStack);
}
}
@Override
public Material getExplodedBlock(BlockExplodeEvent event){
// accoding to the Bukkit docs this will always return air
return event.getExplodedBlockState().getType();
}
}
@@ -6,13 +6,18 @@ import java.util.List;
import java.util.Set;
import org.bukkit.Bukkit;
import org.bukkit.ExplosionResult;
import org.bukkit.Keyed;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Tag;
import org.bukkit.entity.EntityType;
import org.bukkit.event.Event;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.inventory.InventoryType;
import net.coreprotect.model.BlockGroup;
/**
@@ -23,9 +28,11 @@ 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<Material> COPPER_CHESTS = new HashSet<>(Arrays.asList());
public static Set<Material> SHELVES = new HashSet<>(Arrays.asList());
public static Set<Material> BUNDLES = new HashSet<>(Arrays.asList());
/**
* Initializes the Bukkit_v1_21 adapter with 1.21-specific block groups and mappings.
@@ -34,9 +41,11 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
public Bukkit_v1_21() {
initializeBlockGroups();
initializeTrapdoorBlocks();
initializeBundles();
BlockGroup.INTERACT_BLOCKS.addAll(copperChestMaterials());
BlockGroup.CONTAINERS.addAll(copperChestMaterials());
BlockGroup.UPDATE_STATE.addAll(copperChestMaterials());
BlockGroup.CONTAINERS.addAll(shelfMaterials());
}
/**
@@ -66,6 +75,20 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
}
}
/**
* Initializes the bundles group to enable the ability to roll back dyed bundles correctly.
* It needs to check whether dyed bundles exist because they were added in 1.21.2.
*/
public void initializeBundles(){
if (BUNDLES.isEmpty()) {
Material bundle = Material.getMaterial("RED_BUNDLE");
if (bundle != null) {
BUNDLES.addAll(Tag.ITEMS_BUNDLES.getValues());
}
BUNDLES.add(Material.BUNDLE);
}
}
/**
* Helper method to add a block to a block group if it's not already present.
*
@@ -111,6 +134,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 +201,17 @@ 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 boolean isBundle(Material material) {
return Tag.ITEMS_BUNDLES.getValues().contains(material);
}
@Override
public Set<Material> copperChestMaterials() {
if (COPPER_CHESTS.isEmpty()) {
@@ -198,4 +233,30 @@ public class Bukkit_v1_21 extends Bukkit_v1_20 implements BukkitInterface {
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;
}
@Override
public boolean shouldLogExplosion(Event event){
ExplosionResult result = null;
if (event instanceof EntityExplodeEvent){
result = ((EntityExplodeEvent)event).getExplosionResult();
} else if (event instanceof BlockExplodeEvent){
result = ((BlockExplodeEvent)event).getExplosionResult();
}
return !(result == ExplosionResult.KEEP ||
result == ExplosionResult.TRIGGER_BLOCK
);
}
}
@@ -52,8 +52,8 @@ public class ConfigHandler extends Queue {
public static final String COMMUNITY_EDITION = "Community Edition";
public static final String JAVA_VERSION = "11.0";
public static final String MINECRAFT_VERSION = "1.16";
public static final String PATCH_VERSION = "23.1";
public static final String LATEST_VERSION = "1.21.11";
public static final String PATCH_VERSION = "23.2";
public static final String LATEST_VERSION = "26.1";
public static String path = "plugins/CoreProtect/";
public static String sqlite = "database.db";
public static String host = "127.0.0.1";
@@ -75,7 +75,7 @@ public final class ListenerHandler {
try {
Class.forName("io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent"); // Paper 1.21.10+
// pluginManager.registerEvents(new CopperGolemChestListener(plugin), plugin);
pluginManager.registerEvents(new CopperGolemChestListener(plugin), plugin);
}
catch (Exception e) {
// Ignore registration failures to remain compatible with older servers.
@@ -146,18 +146,25 @@ public final class BlockExplodeListener extends Queue implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
protected void onBlockExplode(BlockExplodeEvent event) {
Block eventBlock = event.getBlock();
World world = eventBlock.getLocation().getWorld();
Material eventMaterial = BukkitAdapter.ADAPTER.getExplodedBlock(event);
World world = event.getBlock().getLocation().getWorld();
if (!BukkitAdapter.ADAPTER.shouldLogExplosion(event)){
return;
}
String user = "";
if (!eventBlock.getType().equals(Material.AIR) && !eventBlock.getType().equals(Material.CAVE_AIR)) {
user = eventBlock.getType().name().toLowerCase(Locale.ROOT);
}
if (user.contains("tnt")) {
user = "#tnt";
}
else if (user.contains("end_crystal")) {
user = "#end_crystal";
if (!eventMaterial.equals(Material.AIR) && !eventMaterial.equals(Material.CAVE_AIR)) {
user = eventMaterial.name().toLowerCase(Locale.ROOT);
if (user.contains("respawn_anchor")) {
user = "#respawn_anchor";
}
else if (user.contains("_bed")) {
user = "#bed";
}
}
if (!user.startsWith("#")) {
user = "#explosion";
}
@@ -1,5 +1,6 @@
package net.coreprotect.listener.entity;
import org.bukkit.ExplosionResult;
import org.bukkit.World;
import org.bukkit.entity.Creeper;
import org.bukkit.entity.EnderCrystal;
@@ -15,6 +16,7 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityExplodeEvent;
import net.coreprotect.bukkit.BukkitAdapter;
import net.coreprotect.config.Config;
import net.coreprotect.consumer.Queue;
import net.coreprotect.listener.block.BlockExplodeListener;
@@ -24,7 +26,8 @@ public final class EntityExplodeListener extends Queue implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
protected void onEntityExplode(EntityExplodeEvent event) {
Entity entity = event.getEntity();
if (entity.getType().name().equals("WIND_CHARGE") || entity.getType().name().equals("BREEZE_WIND_CHARGE")) {
if (!BukkitAdapter.ADAPTER.shouldLogExplosion(event)){
return;
}
@@ -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();
@@ -1,133 +1,574 @@
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.Block;
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 org.bukkit.scheduler.BukkitTask;
import io.papermc.paper.event.entity.ItemTransportingEntityValidateTargetEvent;
import net.coreprotect.CoreProtect;
import net.coreprotect.bukkit.BukkitAdapter;
import net.coreprotect.config.Config;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.listener.player.InventoryChangeListener;
import net.coreprotect.thread.Scheduler;
import net.coreprotect.utility.ItemUtils;
public final class CopperGolemChestListener implements Listener {
private static final String COPPER_GOLEM_NAME = "COPPER_GOLEM";
private static final String USERNAME = "#copper_golem";
private static final long INITIAL_DELAY_TICKS = 5L;
private static final long POLL_INTERVAL_TICKS = 15L;
private static final long MAX_POLL_DURATION_TICKS = 600L;
private static final long MIN_THROTTLE_MILLIS = 2800L;
private static final long OPEN_INTERACTION_TIMEOUT_MILLIS = 20000L;
private static final long CLEANUP_INTERVAL_MILLIS = 60000L;
private static final long EMPTY_COPPER_CHEST_SKIP_TTL_MILLIS = 6000L;
private static final long CLOSE_FINALIZE_DELAY_TICKS = 1L;
private static final int CLOSE_FINALIZE_MAX_ATTEMPTS = 3;
private static final int CLOSE_FALLBACK_MAX_ATTEMPTS = 2;
private final CoreProtect plugin;
private final Map<TransactionKey, PendingTransaction> pendingTransactions = new ConcurrentHashMap<>();
private final Map<TransactionKey, Long> throttleUntil = new ConcurrentHashMap<>();
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 onValidate(ItemTransportingEntityValidateTargetEvent event) {
if (!event.isAllowed()) {
public void onGenericGameEvent(GenericGameEvent event) {
if (event == null) {
return;
}
GameEvent gameEvent = event.getEvent();
if (gameEvent != GameEvent.CONTAINER_OPEN && gameEvent != GameEvent.CONTAINER_CLOSE) {
return;
}
Entity entity = event.getEntity();
if (entity == null || entity.getType() == null || !COPPER_GOLEM_NAME.equals(entity.getType().name())) {
return;
if (gameEvent == GameEvent.CONTAINER_OPEN) {
if (!(entity instanceof CopperGolem)) {
return;
}
}
else {
if (entity != null && !(entity instanceof CopperGolem)) {
return;
}
}
CopperGolem copperGolem = (CopperGolem) entity;
Material heldMaterial = getHeldItemMaterial(copperGolem);
Block block = event.getBlock();
if (block == null) {
Location eventLocation = event.getLocation();
if (eventLocation == null || eventLocation.getWorld() == null) {
return;
}
BlockState blockState = block.getState();
if (!Config.getConfig(eventLocation.getWorld()).ITEM_TRANSACTIONS) {
return;
}
BlockState blockState = eventLocation.getBlock().getState();
if (!(blockState instanceof InventoryHolder)) {
return;
}
Location location = blockState.getLocation();
if (location == null || location.getWorld() == null) {
Location containerLocation = blockState.getLocation();
if (containerLocation == null || containerLocation.getWorld() == null) {
return;
}
if (!Config.getConfig(location.getWorld()).ITEM_TRANSACTIONS) {
Material containerType = blockState.getType();
boolean isCopperChest = isCopperChest(containerType);
boolean isStandardChest = containerType == Material.CHEST || containerType == Material.TRAPPED_CHEST;
if (!isCopperChest && !isStandardChest) {
return;
}
scheduleTransaction(copperGolem, blockState, heldMaterial);
}
private void scheduleTransaction(CopperGolem copperGolem, BlockState blockState, Material heldMaterial) {
Location location = blockState.getLocation();
if (location == null || copperGolem == null) {
return;
}
TransactionKey transactionKey = TransactionKey.of(location);
Location targetLocation = location.clone();
ItemStack[] baselineState = captureInventoryState(targetLocation);
if (baselineState == null) {
return;
}
if (!shouldMonitorInteraction(blockState.getType(), baselineState, heldMaterial)) {
return;
}
PendingTransaction existing = pendingTransactions.get(transactionKey);
if (existing != null) {
existing.refresh(baselineState);
return;
}
if (isThrottled(transactionKey)) {
return;
}
PendingTransaction scheduled = new PendingTransaction(transactionKey, targetLocation, baselineState);
pendingTransactions.put(transactionKey, scheduled);
throttleUntil.put(transactionKey, System.currentTimeMillis() + MIN_THROTTLE_MILLIS);
scheduled.start();
}
private ItemStack[] captureInventoryState(Location location) {
if (location == null || location.getWorld() == null) {
return null;
}
BlockState blockState = location.getBlock().getState();
if (!(blockState instanceof InventoryHolder)) {
return null;
}
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) {
Scheduler.scheduleSyncDelayedTask(plugin, () -> finalizeContainerClose(golemId, interaction, containerKey, attempt), interaction.location, (int) CLOSE_FINALIZE_DELAY_TICKS);
}
private void scheduleUntrackedCopperChestCloseFinalize(UUID golemId, Location containerLocation, Material containerType, int attempt) {
Scheduler.scheduleSyncDelayedTask(plugin, () -> finalizeUntrackedCopperChestClose(golemId, containerLocation, containerType, attempt), containerLocation, (int) 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;
}
return ItemUtils.getContainerState(inventory.getContents());
ItemStack[] baseline = ItemUtils.getContainerState(currentContents);
if (baseline == null) {
return null;
}
int addAmount = heldNowStack.getAmount();
int maxStack = heldNowStack.getMaxStackSize();
for (int i = 0; i < baseline.length; i++) {
ItemStack item = baseline[i];
if (item == null) {
continue;
}
if (item.isSimilar(heldNowStack)) {
if (item.getAmount() + addAmount > maxStack) {
return null;
}
item.setAmount(item.getAmount() + addAmount);
return baseline;
}
}
for (int i = 0; i < baseline.length; i++) {
ItemStack item = baseline[i];
if (isEmptyItem(item)) {
ItemStack placed = heldNowStack.clone();
placed.setAmount(addAmount);
baseline[i] = placed;
return baseline;
}
}
return null;
}
private void recordForcedContainerState(Location location, ItemStack[] contents) {
if (location == null) {
return;
}
ItemStack[] snapshot = ItemUtils.getContainerState(contents);
if (snapshot == null) {
return;
}
String loggingContainerId = USERNAME + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ();
List<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) {
@@ -158,30 +599,6 @@ public final class CopperGolemChestListener implements Listener {
return false;
}
private boolean shouldMonitorInteraction(Material blockType, ItemStack[] baselineState, Material heldMaterial) {
boolean isCopperChest = BukkitAdapter.ADAPTER.isCopperChest(blockType);
boolean isStandardChest = blockType == Material.CHEST || blockType == Material.TRAPPED_CHEST;
boolean golemHoldingItem = heldMaterial != null && heldMaterial != Material.AIR;
if (!isCopperChest && !isStandardChest) {
return false;
}
if (isCopperChest) {
return !golemHoldingItem && !isInventoryEmpty(baselineState);
}
if (!golemHoldingItem) {
return false;
}
if (isInventoryEmpty(baselineState)) {
return true;
}
return containsMaterial(baselineState, heldMaterial);
}
private boolean isInventoryEmpty(ItemStack[] state) {
if (state == null) {
return true;
@@ -200,106 +617,122 @@ public final class CopperGolemChestListener implements Listener {
return item == null || item.getType().isAir() || item.getAmount() <= 0;
}
private Material getHeldItemMaterial(CopperGolem copperGolem) {
if (copperGolem == null) {
return null;
}
EntityEquipment equipment = copperGolem.getEquipment();
if (equipment == null) {
return null;
}
ItemStack mainHand = equipment.getItemInMainHand();
if (isEmptyItem(mainHand)) {
return null;
}
return mainHand.getType();
}
private boolean containsMaterial(ItemStack[] state, Material material) {
private boolean containsOnlyMaterial(ItemStack[] state, Material material) {
if (state == null || material == null) {
return false;
}
for (ItemStack item : state) {
if (item != null && item.getType() == material && item.getAmount() > 0) {
return true;
if (isEmptyItem(item)) {
continue;
}
if (item.getType() != material) {
return false;
}
}
return false;
}
private boolean isThrottled(TransactionKey transactionKey) {
Long eligibleAt = throttleUntil.get(transactionKey);
if (eligibleAt == null) {
return false;
}
long now = System.currentTimeMillis();
if (eligibleAt <= now) {
throttleUntil.remove(transactionKey, eligibleAt);
return false;
}
return true;
}
private final class PendingTransaction implements Runnable {
private boolean hasSpaceForMaterial(ItemStack[] state, Material material) {
if (state == null || material == null) {
return false;
}
private final TransactionKey transactionKey;
private final Location targetLocation;
private ItemStack[] baselineState;
private BukkitTask task;
private long ticksElapsed;
private long ticksSinceLastTrigger;
int maxStackSize = material.getMaxStackSize();
for (ItemStack item : state) {
if (isEmptyItem(item)) {
return true;
}
if (item.getType() == material && item.getAmount() < maxStackSize) {
return true;
}
}
private PendingTransaction(TransactionKey transactionKey, Location targetLocation, ItemStack[] baselineState) {
this.transactionKey = transactionKey;
this.targetLocation = targetLocation;
return false;
}
private int sumMaterialAmount(ItemStack[] state, Material material) {
if (state == null || material == null) {
return 0;
}
int total = 0;
for (ItemStack item : state) {
if (!isEmptyItem(item) && item.getType() == material) {
total += item.getAmount();
}
}
return total;
}
private int sumAllItems(ItemStack[] state) {
if (state == null) {
return 0;
}
int total = 0;
for (ItemStack item : state) {
if (!isEmptyItem(item)) {
total += item.getAmount();
}
}
return total;
}
private HeldItemSnapshot getHeldItemSnapshot(CopperGolem copperGolem) {
if (copperGolem == null) {
return HeldItemSnapshot.EMPTY;
}
EntityEquipment equipment = copperGolem.getEquipment();
if (equipment == null) {
return HeldItemSnapshot.EMPTY;
}
ItemStack mainHand = equipment.getItemInMainHand();
if (isEmptyItem(mainHand)) {
return HeldItemSnapshot.EMPTY;
}
return new HeldItemSnapshot(mainHand.getType(), mainHand.getAmount());
}
private static final class OpenInteraction {
private final TransactionKey containerKey;
private final Location location;
private final Material containerType;
private final ItemStack[] baselineState;
private final Material heldMaterial;
private final int heldAmount;
private final long openedAtMillis;
private OpenInteraction(TransactionKey containerKey, Location location, Material containerType, ItemStack[] baselineState, Material heldMaterial, int heldAmount, long openedAtMillis) {
this.containerKey = containerKey;
this.location = location;
this.containerType = containerType;
this.baselineState = baselineState;
this.heldMaterial = heldMaterial;
this.heldAmount = heldAmount;
this.openedAtMillis = openedAtMillis;
}
}
private void start() {
task = plugin.getServer().getScheduler().runTaskTimer(plugin, this, INITIAL_DELAY_TICKS, POLL_INTERVAL_TICKS);
private static final class RecentEmptyCopperChestSkip {
private final TransactionKey containerKey;
private final long skippedAtMillis;
private RecentEmptyCopperChestSkip(TransactionKey containerKey, long skippedAtMillis) {
this.containerKey = containerKey;
this.skippedAtMillis = skippedAtMillis;
}
}
private void refresh(ItemStack[] newBaselineState) {
ticksSinceLastTrigger = 0L;
if (newBaselineState != null) {
baselineState = newBaselineState;
}
}
private static final class HeldItemSnapshot {
@Override
public void run() {
long increment = ticksElapsed == 0L ? INITIAL_DELAY_TICKS : POLL_INTERVAL_TICKS;
ticksElapsed += increment;
ticksSinceLastTrigger += increment;
if (ticksSinceLastTrigger > MAX_POLL_DURATION_TICKS) {
cancelAndRemove();
return;
}
private static final HeldItemSnapshot EMPTY = new HeldItemSnapshot(null, 0);
ItemStack[] currentState = captureInventoryState(targetLocation);
if (currentState == null) {
cancelAndRemove();
return;
}
private final Material material;
private final int amount;
boolean stateChanged = hasInventoryChanged(baselineState, currentState);
if (stateChanged) {
InventoryChangeListener.inventoryTransaction(USERNAME, targetLocation, baselineState);
baselineState = ItemUtils.getContainerState(currentState);
// cancelAndRemove();
// return;
}
}
private void cancel() {
if (task != null) {
task.cancel();
}
}
private void cancelAndRemove() {
cancel();
pendingTransactions.remove(transactionKey, this);
private HeldItemSnapshot(Material material, int amount) {
this.material = material;
this.amount = amount;
}
}
@@ -342,4 +775,32 @@ public final class CopperGolemChestListener implements Listener {
}
}
private static final class OpenInteractionIndex {
private final UUID golemId;
private final long openedAtMillis;
private OpenInteractionIndex(UUID golemId, long openedAtMillis) {
this.golemId = golemId;
this.openedAtMillis = openedAtMillis;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof OpenInteractionIndex)) {
return false;
}
OpenInteractionIndex other = (OpenInteractionIndex) obj;
return openedAtMillis == other.openedAtMillis && golemId.equals(other.golemId);
}
@Override
public int hashCode() {
return Objects.hash(golemId, openedAtMillis);
}
}
}
@@ -26,14 +26,23 @@ public class VersionCheckService {
public static boolean performVersionChecks() {
try {
// Check Minecraft version compatibility
String[] bukkitVersion = Bukkit.getServer().getBukkitVersion().split("[-.]");
if (VersionUtils.newVersion(bukkitVersion[0] + "." + bukkitVersion[1], ConfigHandler.MINECRAFT_VERSION)) {
String numericVersion = Bukkit.getServer().getBukkitVersion().split("-", 2)[0];
String[] bukkitVersion = numericVersion.split("\\.");
if (bukkitVersion.length < 2 || !bukkitVersion[0].matches("\\d+") || !bukkitVersion[1].matches("\\d+")) {
Chat.console(Phrase.build(Phrase.VERSION_INCOMPATIBLE, "Minecraft", numericVersion));
return false;
}
String minimumVersion = bukkitVersion[0] + "." + bukkitVersion[1];
String currentVersion = minimumVersion + (bukkitVersion.length > 2 && bukkitVersion[2].matches("\\d+") ? "." + bukkitVersion[2] : "");
if (VersionUtils.newVersion(minimumVersion, ConfigHandler.MINECRAFT_VERSION)) {
Chat.console(Phrase.build(Phrase.VERSION_REQUIRED, "Minecraft", ConfigHandler.MINECRAFT_VERSION));
return false;
}
if (VersionUtils.newVersion(ConfigHandler.LATEST_VERSION, bukkitVersion[0] + "." + bukkitVersion[1] + (bukkitVersion.length > 2 && bukkitVersion[2].matches("\\d+") ? "." + bukkitVersion[2] : "")) && VersionUtils.isCommunityEdition()) {
Chat.console(Phrase.build(Phrase.VERSION_INCOMPATIBLE, "Minecraft", bukkitVersion[0] + "." + bukkitVersion[1] + (bukkitVersion.length > 2 ? "." + bukkitVersion[2] : "")));
if (VersionUtils.newVersion(ConfigHandler.LATEST_VERSION, currentVersion) && VersionUtils.isCommunityEdition()) {
Chat.console(Phrase.build(Phrase.VERSION_INCOMPATIBLE, "Minecraft", currentVersion));
return false;
}
@@ -60,7 +69,9 @@ public class VersionCheckService {
}
// Store Minecraft server version for later use
ConfigHandler.SERVER_VERSION = Integer.parseInt(bukkitVersion[1]);
int major = Integer.parseInt(bukkitVersion[0]);
int minor = Integer.parseInt(bukkitVersion[1]);
ConfigHandler.SERVER_VERSION = (major == 1 ? minor : major);
}
catch (Exception e) {
e.printStackTrace();
@@ -2,7 +2,6 @@ package net.coreprotect.thread;
import java.util.concurrent.TimeUnit;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.scheduler.BukkitTask;
@@ -21,10 +20,10 @@ public class Scheduler {
if (regionData instanceof Location) {
Location location = (Location) regionData;
if (delay == 0) {
Bukkit.getServer().getRegionScheduler().run(plugin, location, value -> task.run());
plugin.getServer().getRegionScheduler().run(plugin, location, value -> task.run());
}
else {
Bukkit.getServer().getRegionScheduler().runDelayed(plugin, location, value -> task.run(), delay);
plugin.getServer().getRegionScheduler().runDelayed(plugin, location, value -> task.run(), delay);
}
}
else if (regionData instanceof Entity) {
@@ -38,19 +37,19 @@ public class Scheduler {
}
else {
if (delay == 0) {
Bukkit.getServer().getGlobalRegionScheduler().run(plugin, value -> task.run());
plugin.getServer().getGlobalRegionScheduler().run(plugin, value -> task.run());
}
else {
Bukkit.getServer().getGlobalRegionScheduler().runDelayed(plugin, value -> task.run(), delay);
plugin.getServer().getGlobalRegionScheduler().runDelayed(plugin, value -> task.run(), delay);
}
}
}
else {
if (delay == 0) {
Bukkit.getServer().getScheduler().runTask(plugin, task);
plugin.getServer().getScheduler().runTask(plugin, task);
}
else {
Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, task, delay);
plugin.getServer().getScheduler().runTaskLater(plugin, task, delay);
}
}
}
@@ -59,36 +58,36 @@ public class Scheduler {
if (ConfigHandler.isFolia) {
if (regionData instanceof Location) {
Location location = (Location) regionData;
return Bukkit.getServer().getRegionScheduler().runAtFixedRate(plugin, location, value -> task.run(), delay, period);
return plugin.getServer().getRegionScheduler().runAtFixedRate(plugin, location, value -> task.run(), delay, period);
}
else if (regionData instanceof Entity) {
Entity entity = (Entity) regionData;
return entity.getScheduler().runAtFixedRate(plugin, value -> task.run(), task, delay, period);
}
else {
return Bukkit.getServer().getGlobalRegionScheduler().runAtFixedRate(plugin, value -> task.run(), delay, period);
return plugin.getServer().getGlobalRegionScheduler().runAtFixedRate(plugin, value -> task.run(), delay, period);
}
}
else {
return Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, task, delay, period);
return plugin.getServer().getScheduler().scheduleSyncRepeatingTask(plugin, task, delay, period);
}
}
public static void scheduleAsyncDelayedTask(CoreProtect plugin, Runnable task, int delay) {
if (ConfigHandler.isFolia) {
if (delay == 0) {
Bukkit.getServer().getAsyncScheduler().runNow(plugin, value -> task.run());
plugin.getServer().getAsyncScheduler().runNow(plugin, value -> task.run());
}
else {
Bukkit.getServer().getAsyncScheduler().runDelayed(plugin, value -> task.run(), (delay * 50L), TimeUnit.MILLISECONDS);
plugin.getServer().getAsyncScheduler().runDelayed(plugin, value -> task.run(), (delay * 50L), TimeUnit.MILLISECONDS);
}
}
else {
if (delay == 0) {
Bukkit.getServer().getScheduler().runTaskAsynchronously(plugin, task);
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, task);
}
else {
Bukkit.getServer().getScheduler().runTaskLaterAsynchronously(plugin, task, delay);
plugin.getServer().getScheduler().runTaskLaterAsynchronously(plugin, task, delay);
}
}
}