diff --git a/docs/testing-paper-26.2.md b/docs/testing-paper-26.2.md new file mode 100644 index 000000000..29e1a3944 --- /dev/null +++ b/docs/testing-paper-26.2.md @@ -0,0 +1,134 @@ +# Paper 26.2 Compatibility Test Matrix + +Use this matrix for the `support-paper-26.2` branch before running the build on a live server. Test on a copied world first. Do not run the first pass on a production SMP world. + +## Useful Commands + +```text +/co status +/co inspect +/co lookup r:20 t:10m +/co rollback u: t:10m r:20 +/co restore u: t:10m r:20 +/co rollback u:#explosion t:10m r:30 +/co restore u:#explosion t:10m r:30 +``` + +## Environment + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Run Paper `26.2.build.23-alpha` or the exact Paper 26.2 build selected for release validation. | `/co status` | CoreProtect reports enabled status on Paper 26.2. | Record exact Paper build. | +| [ ] | Run with the Java version used for release validation. Paper 26.2 API builds require Java 25 for compilation; runtime should use the Java version required by the Paper server build. | `/co status` | No Java linkage or class version errors appear. | Record `java -version`. | +| [ ] | Install the built jar from `target/CoreProtect-24.0.jar`. | `/plugins` | CoreProtect is listed and enabled. | Record jar checksum if needed. | +| [ ] | Create a fresh copied SQLite test world with default CoreProtect SQLite config. | `/co status` | Database connection is healthy. | Do not use the original production world. | +| [ ] | Optionally create a copied MySQL test world with a disposable CoreProtect database. | `/co status` | MySQL connection is healthy. | Skip only if MySQL is not part of deployment. | + +## Startup + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Start the Paper 26.2 server with CoreProtect installed. | `/plugins` | Server completes boot and CoreProtect appears green. | Capture startup log if it fails. | +| [ ] | Run status after boot. | `/co status` | Command returns normal CoreProtect status. | Note database mode and version shown. | +| [ ] | Inspect `logs/latest.log` after startup. | `/co status` | No CoreProtect exceptions, linkage errors, or adapter warnings. | Include stack trace if any. | +| [ ] | Restart the server once after initial startup. | `/plugins` | CoreProtect remains enabled after restart. | Confirms persistence path loads cleanly. | + +## Baseline CoreProtect Behavior + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Place a normal full block such as stone. | `/co lookup r:20 t:10m` | Placement is logged for the player. | | +| [ ] | Break the same normal block. | `/co lookup r:20 t:10m` | Break is logged for the player. | | +| [ ] | Insert an item into a chest and remove it. | `/co lookup r:20 t:10m` | Chest insert and remove transactions are logged. | | +| [ ] | Insert fuel and smeltable input into a furnace, then remove output. | `/co lookup r:20 t:10m` | Furnace item movement is logged. | | +| [ ] | Open and close a door and trapdoor. | `/co lookup r:20 t:10m` | Interactions are logged if interaction logging is enabled. | | +| [ ] | Place and remove water with a bucket. | `/co lookup r:20 t:10m` | Water place and remove are logged. | | +| [ ] | Place and remove lava with a bucket. | `/co lookup r:20 t:10m` | Lava place and remove are logged. | | +| [ ] | Kill a normal entity such as a cow. | `/co lookup r:20 t:10m` | Entity kill is logged with entity type. | | +| [ ] | Detonate TNT near disposable blocks. | `/co lookup r:20 t:10m` | TNT priming and explosion block breaks are logged. | | +| [ ] | Enable inspect and click recent test blocks or containers. | `/co inspect` | Inspect reports the expected recent actions. | Run `/co inspect` again to disable. | +| [ ] | Roll back the baseline player actions in the test radius. | `/co rollback u: t:10m r:20` | Blocks and containers revert as expected. | Replace `` with tester name. | +| [ ] | Restore the same baseline player actions. | `/co restore u: t:10m r:20` | Blocks and containers return as expected. | Replace `` with tester name. | +| [ ] | Restart the server, then repeat lookup and rollback on recent actions. | `/co lookup r:20 t:10m` | Lookup, rollback, and restore still work after restart. | Confirms persisted data reload. | + +## Minecraft 26.2 Blocks + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Place and break each sulfur block family variant available in Paper 26.2. | `/co lookup r:20 t:10m` | Each placement and break logs with the correct material. | Record any missing material names. | +| [ ] | Place and break each cinnabar block family variant available in Paper 26.2. | `/co lookup r:20 t:10m` | Each placement and break logs with the correct material. | Record any missing material names. | +| [ ] | Place and break sulfur and cinnabar slab variants, including top, bottom, and double states where available. | `/co lookup r:20 t:10m` | Slab block states are logged. | | +| [ ] | Place and break sulfur and cinnabar stair variants in several facing and shape states. | `/co lookup r:20 t:10m` | Stair block states are logged. | | +| [ ] | Place and break sulfur and cinnabar wall variants with connected and unconnected states. | `/co lookup r:20 t:10m` | Wall block states are logged. | | +| [ ] | Place, update, and break `POTENT_SULFUR`. | `/co lookup r:20 t:10m` | Placement, break, and state-changing updates are logged when events are exposed. | Note any state changes not exposed by Bukkit/Paper events. | +| [ ] | Place `SULFUR_SPIKE` on supported faces, remove support, and break it directly. | `/co lookup r:20 t:10m` | Spike placement, direct break, and support break results are logged. | Verify vertical attachment behavior. | +| [ ] | Roll back and restore all 26.2 block placements and breaks. | `/co rollback u: t:10m r:20` then `/co restore u: t:10m r:20` | Exact material and block states return. | Check orientation, waterlogging, slab halves, stairs, and walls. | +| [ ] | Trigger potent sulfur or geyser state changes if testable in the server build. | `/co lookup r:20 t:10m` | State changes are logged when CoreProtect receives block events for them. | Record server steps used to trigger the change. | + +## Sulfur Cube Spawn And Bucket + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Spawn a Sulfur Cube with `SULFUR_CUBE_SPAWN_EGG`. | `/co lookup r:20 t:10m` | Spawn egg placement is logged through entity placement handling. | | +| [ ] | Place a Sulfur Cube from `SULFUR_CUBE_BUCKET`. | `/co lookup r:20 t:10m` | Bucket placement is logged through entity placement handling. | | +| [ ] | Capture a Sulfur Cube into a bucket. | `/co lookup r:20 t:10m` | Empty bucket removal and Sulfur Cube bucket creation are logged. | | +| [ ] | Verify bucket transaction details after capture. | `/co lookup r:20 t:10m` | Item transaction history shows the bucket conversion. | | +| [ ] | Kill or remove a Sulfur Cube if entity kill logging is enabled. | `/co lookup r:20 t:10m` | Entity lookup shows `SULFUR_CUBE` if supported by the command path. | | +| [ ] | Roll back and restore Sulfur Cube entity placement/removal where supported. | `/co rollback u: t:10m r:20` then `/co restore u: t:10m r:20` | Entity restore works, including size, ageable state, fuse ticks, and from-bucket state when captured in metadata. | Entity rollback must be enabled in config. | + +## Sulfur Cube Absorbed Content + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Create a `SULFUR_CUBE_BUCKET` with absorbed content, then log it through capture or inventory movement. | `/co lookup r:20 t:10m` | Logged item data preserves the absorbed content component when the bucket item is restored. | Requires a reliable in-game way to create absorbed content. | +| [ ] | Roll back and restore a container or transaction involving the absorbed-content bucket. | `/co rollback u: t:10m r:20` then `/co restore u: t:10m r:20` | Restored bucket still contains the absorbed content item data. | Compare tooltip/NBT-equivalent behavior in game. | +| [ ] | Roll back and restore a live Sulfur Cube with absorbed entity content. | `/co rollback u: t:10m r:20` then `/co restore u: t:10m r:20` | Known limitation: absorbed live entity content is not restored because Paper exposes entity data components read-only here. | Do not fail the build for this limitation unless Paper exposes a safe setter. | + +## Sulfur Cube Shearing + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Player shears a Sulfur Cube with absorbed content. | `/co lookup r:20 t:10m` | Player-attributed shearing interaction and resulting dropped content are logged through existing patterns. | | +| [ ] | Player shears a Sulfur Cube with no absorbed content. | `/co lookup r:20 t:10m` | No error occurs and no invalid item transaction is logged. | Check `latest.log`. | +| [ ] | Dispenser shears a Sulfur Cube with absorbed content. | `/co lookup r:20 t:10m` | Block/dispenser-attributed shearing and dropped content are logged if the event exposes them. | | +| [ ] | Dispenser shears a Sulfur Cube with no absorbed content. | `/co lookup r:20 t:10m` | No error occurs and no invalid item transaction is logged. | Check `latest.log`. | +| [ ] | Cancel player shearing with another plugin or test protection rule if available. | `/co lookup r:20 t:10m` | Cancelled shearing is ignored by CoreProtect. | Optional if no cancellation plugin is available. | +| [ ] | Cancel dispenser shearing with another plugin or test protection rule if available. | `/co lookup r:20 t:10m` | Cancelled dispenser shearing is ignored by CoreProtect. | Optional if no cancellation plugin is available. | + +## Sulfur Cube Dispenser Interaction + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Dispenser inserts content into a Sulfur Cube. | `/co lookup r:20 t:10m` | Input item removal is logged if existing dispenser inventory logging does not already cover it. | Check for duplicate input logs. | +| [ ] | Dispenser swaps absorbed content on a Sulfur Cube. | `/co lookup r:20 t:10m` | Removed dispenser item and returned item/drop are logged when exposed by events. | | +| [ ] | Dispenser removes absorbed content from a Sulfur Cube if the server exposes this behavior. | `/co lookup r:20 t:10m` | Returned item/drop is logged when exposed by events. | Mark not applicable if Paper exposes no event/drop. | +| [ ] | Verify existing dispenser inventory logging during Sulfur Cube insertion. | `/co lookup r:20 t:10m` | No duplicate input item removal is recorded. | | +| [ ] | Verify returned drop attribution from dispenser interactions. | `/co lookup r:20 t:10m` | Returned drop is logged as `#dispenser` when emitted. | | + +## Sulfur Cube Explosion / TNT + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Create a TNT-absorbed Sulfur Cube using the server-supported 26.2 interaction. | `/co lookup r:20 t:10m` | Setup actions are logged where existing item/entity paths expose them. | Record exact setup steps. | +| [ ] | Trigger the TNT-absorbed Sulfur Cube explosion near disposable blocks. | `/co lookup r:20 t:10m` | Affected blocks are logged as `#explosion`. | Custom `#sulfur_cube` attribution is intentionally not implemented. | +| [ ] | Roll back affected explosion blocks. | `/co rollback u:#explosion t:10m r:30` | Blocks damaged by the Sulfur Cube explosion are restored. | | +| [ ] | Restore affected explosion blocks. | `/co restore u:#explosion t:10m r:30` | Explosion damage is reapplied as expected. | | +| [ ] | Inspect `latest.log` after the Sulfur Cube explosion. | `/co lookup r:20 t:10m` | No CoreProtect exceptions or null handling errors. | | + +## Storage + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Run the full matrix on SQLite. | `/co status` | SQLite test pass completes without CoreProtect errors. | Required. | +| [ ] | Restart SQLite server after full matrix, then run lookup and rollback spot checks. | `/co lookup r:20 t:10m` | Persisted SQLite data remains usable. | Required. | +| [ ] | Run the full matrix on MySQL if available. | `/co status` | MySQL test pass completes without CoreProtect errors. | Required only for MySQL deployments. | +| [ ] | Restart MySQL server after full matrix, then run lookup and rollback spot checks. | `/co lookup r:20 t:10m` | Persisted MySQL data remains usable. | Required only for MySQL deployments. | + +## Final Acceptance + +| Done | Setup / Action | CoreProtect command to verify | Expected result | Notes / failure | +| --- | --- | --- | --- | --- | +| [ ] | Confirm every required SQLite row above passed on a copied Paper 26.2 server. | `/co status` | Branch is safe for copied test servers. | Do not use production data for first validation. | +| [ ] | Confirm MySQL rows passed if MySQL is used by the target deployment. | `/co status` | Branch is safe for MySQL-backed copied test servers. | | +| [ ] | Review all notes and failures from this matrix. | `/co lookup r:20 t:10m` | No unresolved required failures remain. | | +| [ ] | Approve real SMP testing only after all required tests pass on copied worlds. | `/plugins` | CoreProtect remains enabled and stable. | Real SMP rollout is not approved before this point. | diff --git a/pom.xml b/pom.xml index 7cf6f1c94..72329c41b 100755 --- a/pom.xml +++ b/pom.xml @@ -224,7 +224,7 @@ io.papermc.paper paper-api - 26.1.2.build.9-alpha + 26.2.build.23-alpha provided diff --git a/src/main/java/net/coreprotect/bukkit/BukkitAdapter.java b/src/main/java/net/coreprotect/bukkit/BukkitAdapter.java index f4d5ae013..5a7ff0c71 100644 --- a/src/main/java/net/coreprotect/bukkit/BukkitAdapter.java +++ b/src/main/java/net/coreprotect/bukkit/BukkitAdapter.java @@ -64,6 +64,7 @@ public class BukkitAdapter implements BukkitInterface { public static final int BUKKIT_V1_20 = 20; public static final int BUKKIT_V1_21 = 21; public static final int BUKKIT_V26_0 = 2600; + public static final int BUKKIT_V26_2 = 2602; public static int getAdapterVersion(int major, int minor) { return major == 1 ? minor : (major * 100) + minor; @@ -95,6 +96,11 @@ public static void loadAdapter() { break; case BUKKIT_V1_21: case BUKKIT_V26_0: + ADAPTER = new Bukkit_v1_21(); + break; + case BUKKIT_V26_2: + ADAPTER = new Bukkit_26_2(); + break; default: ADAPTER = new Bukkit_v1_21(); break; diff --git a/src/main/java/net/coreprotect/bukkit/Bukkit_26_2.java b/src/main/java/net/coreprotect/bukkit/Bukkit_26_2.java new file mode 100644 index 000000000..19bdeca28 --- /dev/null +++ b/src/main/java/net/coreprotect/bukkit/Bukkit_26_2.java @@ -0,0 +1,37 @@ +package net.coreprotect.bukkit; + +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Speleothem; + +import net.coreprotect.model.BlockGroup; + +public class Bukkit_26_2 extends Bukkit_v1_21 { + + public Bukkit_26_2() { + initializeBlockGroups(); + } + + private void initializeBlockGroups() { + Material potentSulfur = Material.getMaterial("POTENT_SULFUR"); + if (potentSulfur != null) { + BlockGroup.UPDATE_STATE.add(potentSulfur); + } + + Material sulfurSpike = Material.getMaterial("SULFUR_SPIKE"); + if (sulfurSpike != null) { + BlockGroup.TRACK_TOP_BOTTOM.add(sulfurSpike); + } + } + + @Override + public boolean isAttached(Block block, Block scanBlock, BlockData blockData, int scanMin) { + if (blockData instanceof Speleothem) { + Speleothem speleothem = (Speleothem) blockData; + return scanBlock.getRelative(speleothem.getVerticalDirection().getOppositeFace()).getLocation().equals(block.getLocation()); + } + + return super.isAttached(block, scanBlock, blockData, scanMin); + } +} diff --git a/src/main/java/net/coreprotect/config/ConfigHandler.java b/src/main/java/net/coreprotect/config/ConfigHandler.java index d991e1a2a..830d7ef75 100644 --- a/src/main/java/net/coreprotect/config/ConfigHandler.java +++ b/src/main/java/net/coreprotect/config/ConfigHandler.java @@ -56,7 +56,7 @@ public enum CacheType { public static final String JAVA_VERSION = "11.0"; public static final String MINECRAFT_VERSION = "1.16.5"; public static final String PATCH_VERSION = "24.0"; - public static final String LATEST_VERSION = "26.1.2"; + public static final String LATEST_VERSION = "26.2"; public static String path = "plugins/CoreProtect/"; public static String sqlite = "database.db"; public static String host = "127.0.0.1"; diff --git a/src/main/java/net/coreprotect/database/rollback/RollbackUtil.java b/src/main/java/net/coreprotect/database/rollback/RollbackUtil.java index 2b59329e9..56fd61e95 100644 --- a/src/main/java/net/coreprotect/database/rollback/RollbackUtil.java +++ b/src/main/java/net/coreprotect/database/rollback/RollbackUtil.java @@ -40,6 +40,7 @@ import net.coreprotect.model.BlockGroup; import net.coreprotect.utility.ItemUtils; import net.coreprotect.utility.ErrorReporter; +import net.coreprotect.utility.serialize.SulfurCubeBucketData; public class RollbackUtil extends Lookup { @@ -382,6 +383,9 @@ else if (mapData.get("modifiers") != null) { itemstack.setItemMeta(itemMeta); } + else if (SulfurCubeBucketData.apply(itemstack, mapData)) { + // Sulfur cube bucket content is a Paper data component, not ItemMeta. + } else if (itemCount == 0) { ItemMeta meta = ItemUtils.deserializeItemMeta(itemstack.getItemMeta().getClass(), map.get(0)); itemstack.setItemMeta(meta); diff --git a/src/main/java/net/coreprotect/listener/ListenerHandler.java b/src/main/java/net/coreprotect/listener/ListenerHandler.java index ec803b838..d8141da26 100644 --- a/src/main/java/net/coreprotect/listener/ListenerHandler.java +++ b/src/main/java/net/coreprotect/listener/ListenerHandler.java @@ -14,6 +14,7 @@ import net.coreprotect.listener.block.BlockIgniteListener; import net.coreprotect.listener.block.BlockPistonListener; import net.coreprotect.listener.block.BlockPlaceListener; +import net.coreprotect.listener.block.BlockShearEntityListener; import net.coreprotect.listener.block.BlockSpreadListener; import net.coreprotect.listener.block.CampfireStartListener; import net.coreprotect.listener.block.TNTPrimeListener; @@ -34,12 +35,14 @@ import net.coreprotect.listener.entity.HangingBreakByEntityListener; import net.coreprotect.listener.entity.HangingBreakListener; import net.coreprotect.listener.entity.HangingPlaceListener; +import net.coreprotect.listener.entity.SulfurCubeDispenserListener; import net.coreprotect.listener.player.ArmorStandManipulateListener; import net.coreprotect.listener.player.CraftItemListener; import net.coreprotect.listener.player.FoodLevelChangeListener; import net.coreprotect.listener.player.InventoryChangeListener; import net.coreprotect.listener.player.InventoryClickListener; import net.coreprotect.listener.player.PlayerBucketEmptyListener; +import net.coreprotect.listener.player.PlayerBucketEntityListener; import net.coreprotect.listener.player.PlayerBucketFillListener; import net.coreprotect.listener.player.PlayerChatListener; import net.coreprotect.listener.player.PlayerCommandListener; @@ -51,6 +54,7 @@ import net.coreprotect.listener.player.PlayerJoinListener; import net.coreprotect.listener.player.PlayerPickupArrowListener; import net.coreprotect.listener.player.PlayerQuitListener; +import net.coreprotect.listener.player.PlayerShearEntityListener; import net.coreprotect.listener.player.PlayerTakeLecternBookListener; import net.coreprotect.listener.player.ProjectileLaunchListener; import net.coreprotect.listener.player.SignChangeListener; @@ -122,6 +126,13 @@ public ListenerHandler(CoreProtect plugin) { // Ignore registration failures to remain compatible with older servers. } } + try { + Class.forName("org.bukkit.event.block.BlockShearEntityEvent"); // Bukkit/Paper 1.21.5+ + pluginManager.registerEvents(new BlockShearEntityListener(), plugin); + } + catch (Exception e) { + // Ignore registration failures to remain compatible with older servers. + } // Entity Listeners pluginManager.registerEvents(new CreatureSpawnListener(), plugin); @@ -138,6 +149,13 @@ public ListenerHandler(CoreProtect plugin) { pluginManager.registerEvents(new HangingPlaceListener(), plugin); pluginManager.registerEvents(new HangingBreakListener(), plugin); pluginManager.registerEvents(new HangingBreakByEntityListener(), plugin); + try { + Class.forName("org.bukkit.event.entity.EntityDropItemEvent"); // Bukkit 1.13+ + pluginManager.registerEvents(new SulfurCubeDispenserListener(), plugin); + } + catch (Exception e) { + // Ignore registration failures to remain compatible with older servers. + } // Paper Listeners / Fallbacks (Player Listeners) try { @@ -162,6 +180,13 @@ public ListenerHandler(CoreProtect plugin) { pluginManager.registerEvents(new InventoryChangeListener(), plugin); pluginManager.registerEvents(new InventoryClickListener(), plugin); pluginManager.registerEvents(new PlayerBucketEmptyListener(), plugin); + try { + Class.forName("org.bukkit.event.player.PlayerBucketEntityEvent"); // Bukkit 1.16.5+ + pluginManager.registerEvents(new PlayerBucketEntityListener(), plugin); + } + catch (Exception e) { + // Ignore registration failures to remain compatible with older servers. + } pluginManager.registerEvents(new PlayerBucketFillListener(), plugin); pluginManager.registerEvents(new PlayerCommandListener(), plugin); pluginManager.registerEvents(new PlayerDeathListener(), plugin); @@ -172,6 +197,13 @@ public ListenerHandler(CoreProtect plugin) { pluginManager.registerEvents(new PlayerItemBreakListener(), plugin); pluginManager.registerEvents(new PlayerJoinListener(), plugin); pluginManager.registerEvents(new PlayerQuitListener(), plugin); + try { + Class.forName("org.bukkit.event.player.PlayerShearEntityEvent"); // Bukkit 1.19+ + pluginManager.registerEvents(new PlayerShearEntityListener(), plugin); + } + catch (Exception e) { + // Ignore registration failures to remain compatible with older servers. + } pluginManager.registerEvents(new SignChangeListener(), plugin); pluginManager.registerEvents(new PlayerTakeLecternBookListener(), plugin); pluginManager.registerEvents(new ProjectileLaunchListener(), plugin); diff --git a/src/main/java/net/coreprotect/listener/block/BlockShearEntityListener.java b/src/main/java/net/coreprotect/listener/block/BlockShearEntityListener.java new file mode 100644 index 000000000..3f1d334c5 --- /dev/null +++ b/src/main/java/net/coreprotect/listener/block/BlockShearEntityListener.java @@ -0,0 +1,16 @@ +package net.coreprotect.listener.block; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockShearEntityEvent; + +import net.coreprotect.listener.entity.SulfurCubeShearLogger; + +public final class BlockShearEntityListener implements Listener { + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + protected void onBlockShearEntity(BlockShearEntityEvent event) { + SulfurCubeShearLogger.logDrops(event.getEntity(), "#dispenser", SulfurCubeShearLogger.getDrops(event)); + } +} diff --git a/src/main/java/net/coreprotect/listener/entity/CreatureSpawnListener.java b/src/main/java/net/coreprotect/listener/entity/CreatureSpawnListener.java index 645da4cde..440368e4d 100644 --- a/src/main/java/net/coreprotect/listener/entity/CreatureSpawnListener.java +++ b/src/main/java/net/coreprotect/listener/entity/CreatureSpawnListener.java @@ -24,7 +24,7 @@ public final class CreatureSpawnListener extends Queue implements Listener { @EventHandler public void onCreatureSpawn(CreatureSpawnEvent event) { - if (event.isCancelled() || !event.getEntityType().equals(EntityType.ARMOR_STAND)) { + if (event.isCancelled() || !isTrackedPlacementEntity(event.getEntityType())) { return; } @@ -40,12 +40,23 @@ public void onCreatureSpawn(CreatureSpawnEvent event) { Map.Entry pair = it.next(); String name = pair.getKey(); Object[] data = pair.getValue(); - if ((data[1].equals(key) || data[2].equals(key)) && EntityUtils.getEntityMaterial(event.getEntityType()) == ((ItemStack) data[3]).getType()) { - Block gravityLocation = BlockUtil.gravityScan(location, Material.ARMOR_STAND, name); - Queue.queueBlockPlace(name, gravityLocation.getState(), location.getBlock().getType(), location.getBlock().getState(), ((ItemStack) data[3]).getType(), (int) event.getEntity().getLocation().getYaw(), 1, null); + Material placedMaterial = ((ItemStack) data[3]).getType(); + if ((data[1].equals(key) || data[2].equals(key)) && matchesPlacedEntityMaterial(event.getEntityType(), placedMaterial)) { + Block blockLocation = placedMaterial == Material.ARMOR_STAND ? BlockUtil.gravityScan(location, Material.ARMOR_STAND, name) : location.getBlock(); + int forceData = placedMaterial == Material.ARMOR_STAND ? (int) event.getEntity().getLocation().getYaw() : -1; + Queue.queueBlockPlace(name, blockLocation.getState(), location.getBlock().getType(), location.getBlock().getState(), placedMaterial, forceData, 1, null); it.remove(); } } } + private static boolean isTrackedPlacementEntity(EntityType type) { + return type == EntityType.ARMOR_STAND || EntityUtils.isSulfurCube(type); + } + + private static boolean matchesPlacedEntityMaterial(EntityType type, Material material) { + Material entityMaterial = EntityUtils.getEntityMaterial(type); + return entityMaterial == material || (EntityUtils.isSulfurCube(type) && EntityUtils.isSulfurCubePlacementMaterial(material)); + } + } diff --git a/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java b/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java index 7b703b6cf..135fa6f2f 100644 --- a/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java +++ b/src/main/java/net/coreprotect/listener/entity/EntityDeathListener.java @@ -78,6 +78,8 @@ import net.coreprotect.spigot.SpigotAdapter; import net.coreprotect.thread.CacheHandler; import net.coreprotect.thread.Scheduler; +import net.coreprotect.utility.EntityUtils; +import net.coreprotect.utility.entity.SulfurCubeEntityData; import net.coreprotect.utility.serialize.ItemMetaHandler; public final class EntityDeathListener extends Queue implements Listener { @@ -378,6 +380,9 @@ else if (entity instanceof MushroomCow) { else if (entity instanceof Skeleton) { info.add(null); } + else if (EntityUtils.isSulfurCube(entity.getType())) { + SulfurCubeEntityData.appendMetadata(entity, info); + } else if (entity instanceof Slime) { Slime slime = (Slime) entity; info.add(slime.getSize()); diff --git a/src/main/java/net/coreprotect/listener/entity/SulfurCubeDispenserListener.java b/src/main/java/net/coreprotect/listener/entity/SulfurCubeDispenserListener.java new file mode 100644 index 000000000..a4dfca54f --- /dev/null +++ b/src/main/java/net/coreprotect/listener/entity/SulfurCubeDispenserListener.java @@ -0,0 +1,111 @@ +package net.coreprotect.listener.entity; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Dispenser; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Item; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockDispenseEvent; +import org.bukkit.event.entity.EntityDropItemEvent; +import org.bukkit.inventory.ItemStack; + +import net.coreprotect.listener.player.PlayerDropItemListener; +import net.coreprotect.utility.EntityUtils; + +public final class SulfurCubeDispenserListener implements Listener { + + private static final long ATTRIBUTION_MS = 2000L; + private static final double TARGET_SEARCH_RADIUS = 1.25D; + private static final Map dispenserTargets = new ConcurrentHashMap<>(); + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + protected void onBlockDispense(BlockDispenseEvent event) { + ItemStack item = event.getItem(); + if (item == null || item.getType() == Material.AIR || item.getType() == Material.SHEARS) { + return; + } + + // Dispenser inventory changes are already logged by BlockPreDispenseListener/BlockDispenseListener. + // This only attributes returned item drops because the dispense event does not expose old/new cube content. + Entity target = getTargetSulfurCube(event.getBlock()); + if (target != null) { + clearExpiredAttributions(); + dispenserTargets.put(target.getUniqueId(), System.currentTimeMillis() + ATTRIBUTION_MS); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + protected void onEntityDropItem(EntityDropItemEvent event) { + Entity entity = event.getEntity(); + if (entity == null || !EntityUtils.isSulfurCube(entity.getType()) || !consumeRecentDispenserAttribution(entity.getUniqueId())) { + return; + } + + Item itemDrop = event.getItemDrop(); + if (itemDrop == null) { + return; + } + + PlayerDropItemListener.playerDropItem(itemDrop.getLocation(), "#dispenser", itemDrop.getItemStack()); + } + + private static Entity getTargetSulfurCube(Block dispenserBlock) { + BlockData blockData = dispenserBlock.getBlockData(); + if (!(blockData instanceof Dispenser)) { + return null; + } + + BlockFace facing = ((Dispenser) blockData).getFacing(); + Block targetBlock = dispenserBlock.getRelative(facing); + Location targetLocation = targetBlock.getLocation().add(0.5D, 0.5D, 0.5D); + World world = targetLocation.getWorld(); + if (world == null) { + return null; + } + + Entity closest = null; + double closestDistance = Double.MAX_VALUE; + for (Entity entity : world.getNearbyEntities(targetLocation, TARGET_SEARCH_RADIUS, TARGET_SEARCH_RADIUS, TARGET_SEARCH_RADIUS)) { + if (!EntityUtils.isSulfurCube(entity.getType())) { + continue; + } + + double distance = entity.getLocation().distanceSquared(targetLocation); + if (distance < closestDistance) { + closest = entity; + closestDistance = distance; + } + } + + return closest; + } + + private static boolean consumeRecentDispenserAttribution(UUID entityId) { + if (entityId == null) { + return false; + } + + Long expiresAt = dispenserTargets.remove(entityId); + if (expiresAt == null) { + return false; + } + + return expiresAt >= System.currentTimeMillis(); + } + + private static void clearExpiredAttributions() { + long currentTime = System.currentTimeMillis(); + dispenserTargets.entrySet().removeIf(entry -> entry.getValue() < currentTime); + } +} diff --git a/src/main/java/net/coreprotect/listener/entity/SulfurCubeShearLogger.java b/src/main/java/net/coreprotect/listener/entity/SulfurCubeShearLogger.java new file mode 100644 index 000000000..be964e873 --- /dev/null +++ b/src/main/java/net/coreprotect/listener/entity/SulfurCubeShearLogger.java @@ -0,0 +1,43 @@ +package net.coreprotect.listener.entity; + +import java.util.List; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.ItemStack; + +import net.coreprotect.listener.player.PlayerDropItemListener; +import net.coreprotect.utility.EntityUtils; + +public final class SulfurCubeShearLogger { + + private SulfurCubeShearLogger() { + throw new IllegalStateException("Utility class"); + } + + public static void logDrops(Entity entity, String user, List drops) { + if (entity == null || !EntityUtils.isSulfurCube(entity.getType()) || drops == null || drops.isEmpty()) { + return; + } + + Location location = entity.getLocation(); + for (ItemStack drop : drops) { + PlayerDropItemListener.playerDropItem(location, user, drop); + } + } + + @SuppressWarnings("unchecked") + public static List getDrops(Object event) { + if (event == null) { + return null; + } + + try { + Object drops = event.getClass().getMethod("getDrops").invoke(event); + return (drops instanceof List) ? (List) drops : null; + } + catch (ReflectiveOperationException | LinkageError | ClassCastException exception) { + return null; + } + } +} diff --git a/src/main/java/net/coreprotect/listener/player/PlayerBucketEntityListener.java b/src/main/java/net/coreprotect/listener/player/PlayerBucketEntityListener.java new file mode 100644 index 000000000..7c48d99ba --- /dev/null +++ b/src/main/java/net/coreprotect/listener/player/PlayerBucketEntityListener.java @@ -0,0 +1,88 @@ +package net.coreprotect.listener.player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerBucketEntityEvent; +import org.bukkit.inventory.ItemStack; + +import net.coreprotect.config.Config; +import net.coreprotect.config.ConfigHandler; +import net.coreprotect.consumer.Queue; +import net.coreprotect.utility.EntityUtils; + +public final class PlayerBucketEntityListener extends Queue implements Listener { + + @EventHandler(priority = EventPriority.LOWEST) + protected void onPlayerBucketEntityInspect(PlayerBucketEntityEvent event) { + if (!isSulfurCubeEvent(event)) { + return; + } + + Player player = event.getPlayer(); + String playerName = player.getName(); + if (ConfigHandler.inspecting.get(playerName) != null && ConfigHandler.inspecting.get(playerName)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + protected void onPlayerBucketEntity(PlayerBucketEntityEvent event) { + if (!isSulfurCubeEvent(event)) { + return; + } + + Player player = event.getPlayer(); + Location location = player.getLocation(); + if (location.getWorld() == null || !Config.getConfig(location.getWorld()).BUCKETS) { + return; + } + + ItemStack originalBucket = event.getOriginalBucket(); + ItemStack entityBucket = event.getEntityBucket(); + if (!isEmptyBucket(originalBucket) || entityBucket == null || !EntityUtils.isSulfurCubeBucket(entityBucket.getType())) { + return; + } + + logItemConversion(location, player.getName(), originalBucket, entityBucket); + } + + private static void logItemConversion(Location location, String player, ItemStack originalBucket, ItemStack entityBucket) { + if (!Config.getConfig(location.getWorld()).ITEM_TRANSACTIONS) { + return; + } + + String loggingItemId = player.toLowerCase(Locale.ROOT) + "." + location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ(); + int itemId = getItemId(loggingItemId); + + ItemStack removedItem = originalBucket.clone(); + removedItem.setAmount(1); + List removedItems = ConfigHandler.itemsDrop.getOrDefault(loggingItemId, new ArrayList<>()); + removedItems.add(removedItem); + ConfigHandler.itemsDrop.put(loggingItemId, removedItems); + + ItemStack addedItem = entityBucket.clone(); + addedItem.setAmount(1); + List addedItems = ConfigHandler.itemsPickup.getOrDefault(loggingItemId, new ArrayList<>()); + addedItems.add(addedItem); + ConfigHandler.itemsPickup.put(loggingItemId, addedItems); + + int time = (int) (System.currentTimeMillis() / 1000L) + 1; + Queue.queueItemTransaction(player, location.clone(), time, 0, itemId); + } + + private static boolean isEmptyBucket(ItemStack itemStack) { + return itemStack != null && itemStack.getType() == Material.BUCKET; + } + + private static boolean isSulfurCubeEvent(PlayerBucketEntityEvent event) { + return event != null && event.getEntity() != null && EntityUtils.isSulfurCube(event.getEntity().getType()); + } +} diff --git a/src/main/java/net/coreprotect/listener/player/PlayerInteractListener.java b/src/main/java/net/coreprotect/listener/player/PlayerInteractListener.java index 171c6d580..96f07bccf 100755 --- a/src/main/java/net/coreprotect/listener/player/PlayerInteractListener.java +++ b/src/main/java/net/coreprotect/listener/player/PlayerInteractListener.java @@ -60,6 +60,7 @@ import net.coreprotect.thread.Scheduler; import net.coreprotect.utility.Chat; import net.coreprotect.utility.Color; +import net.coreprotect.utility.EntityUtils; import net.coreprotect.utility.ItemUtils; import net.coreprotect.utility.WorldUtils; import net.coreprotect.utility.ErrorReporter; @@ -654,14 +655,11 @@ else if (type == Material.DRAGON_EGG) { if (event.useItemInHand() != Event.Result.DENY) { List entityBlockTypes = new ArrayList<>(Arrays.asList(Material.ARMOR_STAND, Material.END_CRYSTAL, Material.BOW, Material.CROSSBOW, Material.TRIDENT, Material.EXPERIENCE_BOTTLE, Material.SPLASH_POTION, Material.LINGERING_POTION, Material.ENDER_PEARL, Material.FIREWORK_ROCKET, Material.EGG, Material.SNOWBALL)); - try { - entityBlockTypes.add(Material.valueOf("WIND_CHARGE")); - entityBlockTypes.add(Material.valueOf("BLUE_EGG")); - entityBlockTypes.add(Material.valueOf("BROWN_EGG")); - } - catch (Exception e) { - // not running MC 1.21+ - } + addMaterialIfPresent(entityBlockTypes, "WIND_CHARGE"); + addMaterialIfPresent(entityBlockTypes, "BLUE_EGG"); + addMaterialIfPresent(entityBlockTypes, "BROWN_EGG"); + addMaterialIfPresent(entityBlockTypes, "SULFUR_CUBE_BUCKET"); + addMaterialIfPresent(entityBlockTypes, "SULFUR_CUBE_SPAWN_EGG"); ItemStack handItem = null; ItemStack mainHand = player.getInventory().getItemInMainHand(); ItemStack offHand = player.getInventory().getItemInOffHand(); @@ -722,7 +720,7 @@ else if (event.getHand().equals(EquipmentSlot.OFF_HAND) && offHand != null && en Location blockLocation = relativeBlockLocation.clone(); blockLocation.setY(blockLocation.getY() + 1); - if (handItem.getType() == Material.ARMOR_STAND || handItem.getType() == Material.FIREWORK_ROCKET) { + if (handItem.getType() == Material.ARMOR_STAND || handItem.getType() == Material.FIREWORK_ROCKET || EntityUtils.isSulfurCubePlacementMaterial(handItem.getType())) { if (block == null) { return; } @@ -763,4 +761,11 @@ else if (event.getAction().equals(Action.PHYSICAL)) { } } } + + private static void addMaterialIfPresent(List materials, String name) { + Material material = Material.getMaterial(name); + if (material != null) { + materials.add(material); + } + } } diff --git a/src/main/java/net/coreprotect/listener/player/PlayerShearEntityListener.java b/src/main/java/net/coreprotect/listener/player/PlayerShearEntityListener.java new file mode 100644 index 000000000..8683bf20f --- /dev/null +++ b/src/main/java/net/coreprotect/listener/player/PlayerShearEntityListener.java @@ -0,0 +1,16 @@ +package net.coreprotect.listener.player; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerShearEntityEvent; + +import net.coreprotect.listener.entity.SulfurCubeShearLogger; + +public final class PlayerShearEntityListener implements Listener { + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + protected void onPlayerShearEntity(PlayerShearEntityEvent event) { + SulfurCubeShearLogger.logDrops(event.getEntity(), event.getPlayer().getName(), SulfurCubeShearLogger.getDrops(event)); + } +} diff --git a/src/main/java/net/coreprotect/paper/PaperAdapter.java b/src/main/java/net/coreprotect/paper/PaperAdapter.java index ac2696e66..438f17e5d 100644 --- a/src/main/java/net/coreprotect/paper/PaperAdapter.java +++ b/src/main/java/net/coreprotect/paper/PaperAdapter.java @@ -32,6 +32,7 @@ public class PaperAdapter implements PaperInterface { public static final int PAPER_V1_20 = BukkitAdapter.BUKKIT_V1_20; public static final int PAPER_V1_21 = BukkitAdapter.BUKKIT_V1_21; public static final int PAPER_V26_0 = BukkitAdapter.BUKKIT_V26_0; + public static final int PAPER_V26_2 = BukkitAdapter.BUKKIT_V26_2; public static void loadAdapter() { int paperVersion = ConfigHandler.SERVER_VERSION; @@ -61,6 +62,9 @@ public static void loadAdapter() { PaperAdapter.ADAPTER = new Paper_v1_20(); break; case PAPER_V26_0: + case PAPER_V26_2: + PaperAdapter.ADAPTER = new Paper_26_0(); + break; default: PaperAdapter.ADAPTER = new Paper_26_0(); break; diff --git a/src/main/java/net/coreprotect/spigot/SpigotAdapter.java b/src/main/java/net/coreprotect/spigot/SpigotAdapter.java index b23c74689..eb5f10e7c 100644 --- a/src/main/java/net/coreprotect/spigot/SpigotAdapter.java +++ b/src/main/java/net/coreprotect/spigot/SpigotAdapter.java @@ -25,6 +25,7 @@ public class SpigotAdapter implements SpigotInterface { public static final int SPIGOT_V1_20 = BukkitAdapter.BUKKIT_V1_20; public static final int SPIGOT_V1_21 = BukkitAdapter.BUKKIT_V1_21; public static final int SPIGOT_V26_0 = BukkitAdapter.BUKKIT_V26_0; + public static final int SPIGOT_V26_2 = BukkitAdapter.BUKKIT_V26_2; public static void loadAdapter() { int spigotVersion = ConfigHandler.SERVER_VERSION; @@ -46,6 +47,9 @@ public static void loadAdapter() { case SPIGOT_V1_20: case SPIGOT_V1_21: case SPIGOT_V26_0: + case SPIGOT_V26_2: + SpigotAdapter.ADAPTER = new SpigotHandler(); + break; default: SpigotAdapter.ADAPTER = new SpigotHandler(); break; diff --git a/src/main/java/net/coreprotect/utility/EntityUtils.java b/src/main/java/net/coreprotect/utility/EntityUtils.java index f05a8b4b2..d9a0d874c 100644 --- a/src/main/java/net/coreprotect/utility/EntityUtils.java +++ b/src/main/java/net/coreprotect/utility/EntityUtils.java @@ -87,12 +87,38 @@ public static Material getEntityMaterial(EntityType type) { case "SNOWBALL": return Material.SNOWBALL; case "WIND_CHARGE": - return Material.valueOf("WIND_CHARGE"); + return Material.getMaterial("WIND_CHARGE"); + case "SULFUR_CUBE": + return getSulfurCubeBucketMaterial(); default: return BukkitAdapter.ADAPTER.getFrameType(type); } } + public static boolean isSulfurCube(EntityType type) { + return type != null && type.name().equals("SULFUR_CUBE"); + } + + public static Material getSulfurCubeBucketMaterial() { + return Material.getMaterial("SULFUR_CUBE_BUCKET"); + } + + public static Material getSulfurCubeSpawnEggMaterial() { + return Material.getMaterial("SULFUR_CUBE_SPAWN_EGG"); + } + + public static boolean isSulfurCubePlacementMaterial(Material material) { + return isMaterial(material, "SULFUR_CUBE_BUCKET") || isMaterial(material, "SULFUR_CUBE_SPAWN_EGG"); + } + + public static boolean isSulfurCubeBucket(Material material) { + return isMaterial(material, "SULFUR_CUBE_BUCKET"); + } + + private static boolean isMaterial(Material material, String name) { + return material != null && material.name().equals(name); + } + public static String getEntityName(int id) { // Internal ID pulled from DB String entityName = ""; diff --git a/src/main/java/net/coreprotect/utility/entity/EntityUtil.java b/src/main/java/net/coreprotect/utility/entity/EntityUtil.java index 18258b388..fd68dad62 100644 --- a/src/main/java/net/coreprotect/utility/entity/EntityUtil.java +++ b/src/main/java/net/coreprotect/utility/entity/EntityUtil.java @@ -69,6 +69,7 @@ import net.coreprotect.spigot.SpigotAdapter; import net.coreprotect.thread.CacheHandler; import net.coreprotect.thread.Scheduler; +import net.coreprotect.utility.EntityUtils; import net.coreprotect.utility.WorldUtils; import net.coreprotect.utility.ErrorReporter; @@ -314,6 +315,9 @@ else if (entity instanceof MushroomCow) { mushroomCow.setVariant(set); } } + else if (EntityUtils.isSulfurCube(entity.getType())) { + SulfurCubeEntityData.applyMetadata(entity, value, count); + } else if (entity instanceof Slime) { Slime slime = (Slime) entity; if (count == 0) { diff --git a/src/main/java/net/coreprotect/utility/entity/SulfurCubeEntityData.java b/src/main/java/net/coreprotect/utility/entity/SulfurCubeEntityData.java new file mode 100644 index 000000000..3801727a9 --- /dev/null +++ b/src/main/java/net/coreprotect/utility/entity/SulfurCubeEntityData.java @@ -0,0 +1,110 @@ +package net.coreprotect.utility.entity; + +import java.lang.reflect.Method; +import java.util.List; + +import org.bukkit.entity.Entity; + +public final class SulfurCubeEntityData { + + private static volatile EntityAccess entityAccess; + private static volatile boolean entityAccessUnavailable; + + private SulfurCubeEntityData() { + throw new IllegalStateException("Utility class"); + } + + public static void appendMetadata(Entity entity, List info) { + EntityAccess access = getEntityAccess(); + if (access == null || !access.sulfurCubeClass.isInstance(entity)) { + return; + } + + try { + info.add(access.getSize.invoke(entity)); + info.add(access.getFuseTicks.invoke(entity)); + info.add(access.isFromBucket.invoke(entity)); + } + catch (ReflectiveOperationException | LinkageError | ClassCastException | IllegalArgumentException exception) { + // Ignore missing or incompatible Paper 26.2 Sulfur Cube APIs. + } + } + + public static boolean applyMetadata(Entity entity, Object value, int count) { + EntityAccess access = getEntityAccess(); + if (access == null || !access.sulfurCubeClass.isInstance(entity)) { + return false; + } + + try { + if (count == 0 && value instanceof Number) { + access.setSize.invoke(entity, ((Number) value).intValue()); + } + else if (count == 1 && value instanceof Number) { + access.setFuseTicks.invoke(entity, ((Number) value).intValue()); + } + else if (count == 2 && value instanceof Boolean) { + access.setFromBucket.invoke(entity, value); + } + } + catch (ReflectiveOperationException | LinkageError | ClassCastException | IllegalArgumentException exception) { + // Ignore missing or incompatible Paper 26.2 Sulfur Cube APIs. + } + + return true; + } + + private static EntityAccess getEntityAccess() { + if (entityAccessUnavailable) { + return null; + } + + EntityAccess access = entityAccess; + if (access != null) { + return access; + } + + try { + Class sulfurCubeClass = Class.forName("org.bukkit.entity.SulfurCube"); + Class abstractCubeMobClass = Class.forName("org.bukkit.entity.AbstractCubeMob"); + Class bucketableClass = Class.forName("io.papermc.paper.entity.Bucketable"); + + access = new EntityAccess( + sulfurCubeClass, + abstractCubeMobClass.getMethod("getSize"), + abstractCubeMobClass.getMethod("setSize", int.class), + sulfurCubeClass.getMethod("getFuseTicks"), + sulfurCubeClass.getMethod("setFuseTicks", int.class), + bucketableClass.getMethod("isFromBucket"), + bucketableClass.getMethod("setFromBucket", boolean.class)); + entityAccess = access; + return access; + } + catch (ReflectiveOperationException | LinkageError exception) { + entityAccessUnavailable = true; + return null; + } + } + + private static final class EntityAccess { + + private final Class sulfurCubeClass; + private final Method getSize; + private final Method setSize; + private final Method getFuseTicks; + private final Method setFuseTicks; + private final Method isFromBucket; + private final Method setFromBucket; + + private EntityAccess(Class sulfurCubeClass, Method getSize, Method setSize, Method getFuseTicks, + Method setFuseTicks, Method isFromBucket, Method setFromBucket) { + this.sulfurCubeClass = sulfurCubeClass; + this.getSize = getSize; + this.setSize = setSize; + this.getFuseTicks = getFuseTicks; + this.setFuseTicks = setFuseTicks; + this.isFromBucket = isFromBucket; + this.setFromBucket = setFromBucket; + } + } +} diff --git a/src/main/java/net/coreprotect/utility/serialize/ItemMetaHandler.java b/src/main/java/net/coreprotect/utility/serialize/ItemMetaHandler.java index 943b041dd..5a72d88c0 100644 --- a/src/main/java/net/coreprotect/utility/serialize/ItemMetaHandler.java +++ b/src/main/java/net/coreprotect/utility/serialize/ItemMetaHandler.java @@ -260,6 +260,8 @@ else if (!BukkitAdapter.ADAPTER.getItemMeta(itemMeta, list, metadata, slot)) { } } + SulfurCubeBucketData.appendMetadata(item, metadata); + if (type != null && type.equals(Material.ARMOR_STAND)) { Map meta = new HashMap<>(); meta.put("slot", slot); diff --git a/src/main/java/net/coreprotect/utility/serialize/SulfurCubeBucketData.java b/src/main/java/net/coreprotect/utility/serialize/SulfurCubeBucketData.java new file mode 100644 index 000000000..8992cf00f --- /dev/null +++ b/src/main/java/net/coreprotect/utility/serialize/SulfurCubeBucketData.java @@ -0,0 +1,181 @@ +package net.coreprotect.utility.serialize; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +public final class SulfurCubeBucketData { + + private static final String SULFUR_CUBE_CONTENT_KEY = "sulfurCubeContent"; + private static volatile ComponentAccess componentAccess; + private static volatile boolean componentAccessUnavailable; + + private SulfurCubeBucketData() { + throw new IllegalStateException("Utility class"); + } + + public static void appendMetadata(ItemStack item, List>> metadata) { + if (!isSulfurCubeBucket(item)) { + return; + } + + byte[] absorbedItemData = getAbsorbedItemData(item); + if (absorbedItemData == null) { + return; + } + + Map map = new HashMap<>(); + map.put(SULFUR_CUBE_CONTENT_KEY, absorbedItemData); + + List> list = new ArrayList<>(); + list.add(map); + metadata.add(list); + } + + public static boolean apply(ItemStack item, Map mapData) { + if (!mapData.containsKey(SULFUR_CUBE_CONTENT_KEY)) { + return false; + } + + Object value = mapData.get(SULFUR_CUBE_CONTENT_KEY); + if (isSulfurCubeBucket(item) && value instanceof byte[]) { + ItemStack absorbedItem = deserializeItem((byte[]) value); + if (absorbedItem != null) { + setAbsorbedItem(item, absorbedItem); + } + } + + return true; + } + + private static byte[] getAbsorbedItemData(ItemStack item) { + ComponentAccess access = getComponentAccess(); + if (access == null) { + return null; + } + + try { + if (!Boolean.TRUE.equals(access.hasData.invoke(item, access.sulfurCubeContentType))) { + return null; + } + + Object content = access.getData.invoke(item, access.sulfurCubeContentType); + if (content == null) { + return null; + } + + Object absorbedItem = access.absorbedItem.invoke(content); + if (!(absorbedItem instanceof ItemStack)) { + return null; + } + + return (byte[]) access.serializeAsBytes.invoke(absorbedItem); + } + catch (ReflectiveOperationException | LinkageError | ClassCastException | IllegalArgumentException exception) { + return null; + } + } + + private static ItemStack deserializeItem(byte[] itemData) { + ComponentAccess access = getComponentAccess(); + if (access == null) { + return null; + } + + try { + Object item = access.deserializeBytes.invoke(null, itemData); + return (item instanceof ItemStack) ? (ItemStack) item : null; + } + catch (ReflectiveOperationException | LinkageError | ClassCastException | IllegalArgumentException exception) { + return null; + } + } + + private static void setAbsorbedItem(ItemStack item, ItemStack absorbedItem) { + ComponentAccess access = getComponentAccess(); + if (access == null) { + return; + } + + try { + Object content = access.sulfurCubeContent.invoke(null, absorbedItem); + access.setData.invoke(item, access.sulfurCubeContentType, content); + } + catch (ReflectiveOperationException | LinkageError | ClassCastException | IllegalArgumentException exception) { + // Ignore missing or incompatible Paper data component APIs. + } + } + + private static boolean isSulfurCubeBucket(ItemStack item) { + if (item == null) { + return false; + } + + Material type = item.getType(); + return type != null && "SULFUR_CUBE_BUCKET".equals(type.name()); + } + + private static ComponentAccess getComponentAccess() { + if (componentAccessUnavailable) { + return null; + } + + ComponentAccess access = componentAccess; + if (access != null) { + return access; + } + + try { + Class dataComponentTypes = Class.forName("io.papermc.paper.datacomponent.DataComponentTypes"); + Class dataComponentType = Class.forName("io.papermc.paper.datacomponent.DataComponentType"); + Class valuedDataComponentType = Class.forName("io.papermc.paper.datacomponent.DataComponentType$Valued"); + Class sulfurCubeContent = Class.forName("io.papermc.paper.datacomponent.item.SulfurCubeContent"); + + Object sulfurCubeContentType = dataComponentTypes.getField("SULFUR_CUBE_CONTENT").get(null); + access = new ComponentAccess( + sulfurCubeContentType, + ItemStack.class.getMethod("hasData", dataComponentType), + ItemStack.class.getMethod("getData", valuedDataComponentType), + ItemStack.class.getMethod("setData", valuedDataComponentType, Object.class), + sulfurCubeContent.getMethod("absorbedItem"), + sulfurCubeContent.getMethod("sulfurCubeContent", ItemStack.class), + ItemStack.class.getMethod("serializeAsBytes"), + ItemStack.class.getMethod("deserializeBytes", byte[].class)); + componentAccess = access; + return access; + } + catch (ReflectiveOperationException | LinkageError exception) { + componentAccessUnavailable = true; + return null; + } + } + + private static final class ComponentAccess { + + private final Object sulfurCubeContentType; + private final Method hasData; + private final Method getData; + private final Method setData; + private final Method absorbedItem; + private final Method sulfurCubeContent; + private final Method serializeAsBytes; + private final Method deserializeBytes; + + private ComponentAccess(Object sulfurCubeContentType, Method hasData, Method getData, Method setData, + Method absorbedItem, Method sulfurCubeContent, Method serializeAsBytes, Method deserializeBytes) { + this.sulfurCubeContentType = sulfurCubeContentType; + this.hasData = hasData; + this.getData = getData; + this.setData = setData; + this.absorbedItem = absorbedItem; + this.sulfurCubeContent = sulfurCubeContent; + this.serializeAsBytes = serializeAsBytes; + this.deserializeBytes = deserializeBytes; + } + } +}