From df9c2c3dc6d19fd1e6041c53bc90ded46a077238 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 23 Sep 2026 17:16:50 +0300 Subject: [PATCH 1/5] Log chested horse inventory transactions --- docs/api/version/v13.md | 6 +- .../java/net/coreprotect/consumer/Queue.java | 34 ++++++- .../process/ContainerTransactionProcess.java | 46 +++++++-- .../coreprotect/consumer/process/Process.java | 99 +++++++++++++++---- .../player/InventoryChangeListener.java | 80 +++++++++++++-- .../player/PlayerInteractEntityListener.java | 3 +- .../entity/EntityContainerTransaction.java | 35 ++++++- 7 files changed, 261 insertions(+), 42 deletions(-) diff --git a/docs/api/version/v13.md b/docs/api/version/v13.md index b1e093f34..848d53a50 100644 --- a/docs/api/version/v13.md +++ b/docs/api/version/v13.md @@ -22,7 +22,7 @@ All API v12 methods remain available. API v13 adds the following: | Text prefix filters | Adds `textStartsWithAny` and `textStartsWithNone` for chat, command, and sign lookups. | | Entity-spawn action | Adds `CoreProtectAction.ENTITY_SPAWN` with ID `13`. Legacy lookup, rollback, and restore methods accept this action explicitly. | | Entity result parsing | `BlockResult` and `ParseResult` expose `getEntityType()` for entity kills and spawns. Their material and block-data getters return `null` for entity rows. | -| Tracked containers | Container and inventory lookups include transactions in tracked boats and minecarts. | +| Tracked containers | Container and inventory lookups include transactions in tracked boats and minecarts, plus chest-equipped donkeys, mules, llamas, and trader llamas. | | Pre-log events | Adds `CoreProtectPreLogEvent.Action.ENTITY_SPAWN` and `ENTITY_INTERACTION`. | The existing `blockLookup(Block, LookupOptions)` overload retains entity events for compatibility. The new `blockLookup(LookupOptions)` overload returns only block events; use `entityLookup` for entities. @@ -338,7 +338,7 @@ The in-game `a:block` filters present placed and destroyed boats/minecarts as bl Searches item additions to and removals from containers. The location/time overload searches an exact location. The options overload also supports radius, world, user, material, action, and pagination filters. -Both methods include tracked boat and minecart container transactions. Their location filters match either the transaction's original location or the entity's persisted current/final location. Typed results for these tracked containers expose the **persisted current/final coordinates and world**. Ordinary block containers expose the transaction location. +Both methods include tracked boat and minecart container transactions, plus transactions for chest-equipped donkeys, mules, llamas, and trader llamas. Their location filters match either the transaction's original location or the entity's persisted current/final location. Typed results for these tracked containers expose the **persisted current/final coordinates and world**. Ordinary block containers expose the transaction location. Tracked positions are checkpointed when transactions are logged and during relevant entity lifecycle changes. Lookups do not wait for a live-entity scan. `containerActions` applies to both block containers and tracked entity containers. @@ -360,7 +360,7 @@ Searches transactions affecting player inventories. The default combines block p Results normalize the action to removal from (`0`) or addition to (`1`) the player's inventory. Use `getTransactionActionId()` and `getSourceId()` together to identify the underlying event. For example, a container removal is an inventory addition. -Tracked boat and minecart transactions follow the location rules described under container lookups and remain part of the public `container` source. +Tracked entity-container transactions follow the location rules described under container lookups and remain part of the public `container` source. ### Chat and command lookups diff --git a/src/main/java/net/coreprotect/consumer/Queue.java b/src/main/java/net/coreprotect/consumer/Queue.java index 00c957400..8bd8b89a7 100755 --- a/src/main/java/net/coreprotect/consumer/Queue.java +++ b/src/main/java/net/coreprotect/consumer/Queue.java @@ -343,11 +343,43 @@ public static void queueEntityContainerTransaction(String user, java.util.UUID e queueEntityContainerTransaction(user, new EntityContainerTransaction(entityUuid, currentLocation, oldContents, newContents)); } + public static void queueEntityContainerTransaction(String user, Entity entity, ItemStack[] oldContents, ItemStack[] newContents) { + if (entity == null) { + return; + } + Location currentLocation = entity.getLocation(); + EntityInteractionOrigin origin = null; + boolean promotion = false; + if (!EntitySpawnTracking.isTracked(entity)) { + origin = EntitySpawnTracking.getOrCreateInteractionOrigin(entity); + if (origin == null || currentLocation.getWorld() == null) { + return; + } + promotion = EntitySpawnTracking.beginDatabaseIdentityPromotion(entity); + if (!promotion) { + return; + } + } + EntityContainerTransaction transaction = new EntityContainerTransaction(entity.getUniqueId(), origin, currentLocation, oldContents, newContents).withIdentityPromotion(promotion); + queueEntityContainerTransaction(user, transaction); + } + public static void queueEntityContainerTransaction(String user, EntityContainerTransaction transaction) { if (user == null || user.isEmpty() || transaction == null) { + if (transaction != null && transaction.hasIdentityPromotion()) { + EntitySpawnTracking.cancelDatabaseIdentityPromotion(transaction.getEntityUuid(), transaction.getCurrentLocation()); + } return; } - queueStandardData(new Object[] { null, Process.ENTITY_CONTAINER_TRANSACTION, null, 0, null, 0, 0, null }, new String[] { user, null }, transaction, false, Consumer.reserveConsumer()); + boolean queued = false; + try { + queued = queueStandardData(new Object[] { null, Process.ENTITY_CONTAINER_TRANSACTION, null, 0, null, 0, 0, null }, new String[] { user, null }, transaction, false, Consumer.reserveConsumer()); + } + finally { + if (!queued && transaction.hasIdentityPromotion()) { + EntitySpawnTracking.cancelDatabaseIdentityPromotion(transaction.getEntityUuid(), transaction.getCurrentLocation()); + } + } } public static void queueEntityInteraction(String user, Entity entity, EntityInteractionAction action) { diff --git a/src/main/java/net/coreprotect/consumer/process/ContainerTransactionProcess.java b/src/main/java/net/coreprotect/consumer/process/ContainerTransactionProcess.java index 1e029fc61..7ca2163ad 100644 --- a/src/main/java/net/coreprotect/consumer/process/ContainerTransactionProcess.java +++ b/src/main/java/net/coreprotect/consumer/process/ContainerTransactionProcess.java @@ -15,6 +15,8 @@ import net.coreprotect.consumer.Queue; import net.coreprotect.database.ConsumerWriteBatch; import net.coreprotect.database.logger.ContainerLogger; +import net.coreprotect.database.statement.EntityInteractionStatement; +import net.coreprotect.database.statement.EntitySpawnStatement; import net.coreprotect.model.entity.EntityContainerTransaction; import net.coreprotect.model.entity.EntitySpawnIdentity; import net.coreprotect.utility.ErrorReporter; @@ -22,23 +24,53 @@ class ContainerTransactionProcess { - static boolean processEntity(ConsumerWriteBatch preparedStmtContainer, int batchCount, String user, Object object, EntitySpawnIdentity identity) throws Exception { - if (!(object instanceof EntityContainerTransaction) || identity == null) { - return false; + static EntityProcessResult processEntity(ConsumerWriteBatch preparedStmtContainer, int batchCount, String user, Object object, EntitySpawnIdentity identity) throws Exception { + if (!(object instanceof EntityContainerTransaction)) { + return null; } - if (ConfigHandler.databaseType.isColumnar()) { - preparedStmtContainer.executeAtomically("entity_container_transaction", () -> ContainerLogger.logEntity(preparedStmtContainer, batchCount, user, identity, (EntityContainerTransaction) object)); + EntityContainerTransaction transaction = (EntityContainerTransaction) object; + if (identity == null && (!transaction.hasIdentityPromotion() || transaction.getOrigin() == null)) { + return null; + } + + EntitySpawnIdentity[] resolvedIdentity = { identity }; + boolean[] identityActive = { identity != null }; + boolean createdIdentity = identity == null; + if (transaction.hasIdentityPromotion()) { + preparedStmtContainer.executeAtomically("entity_container_transaction", () -> { + if (resolvedIdentity[0] == null) { + resolvedIdentity[0] = EntitySpawnStatement.insertIdentity(preparedStmtContainer, transaction.getTime(), transaction.getEntityUuid(), transaction.getOrigin(), transaction.getCurrentLocation()); + } + identityActive[0] = EntityInteractionStatement.checkpoint(preparedStmtContainer, resolvedIdentity[0], transaction.getCurrentLocation()); + ContainerLogger.logEntity(preparedStmtContainer, batchCount, user, resolvedIdentity[0], transaction); + }); + } + else if (ConfigHandler.databaseType.isColumnar()) { + preparedStmtContainer.executeAtomically("entity_container_transaction", () -> ContainerLogger.logEntity(preparedStmtContainer, batchCount, user, resolvedIdentity[0], transaction)); } else { try { - ContainerLogger.logEntity(preparedStmtContainer, batchCount, user, identity, (EntityContainerTransaction) object); + ContainerLogger.logEntity(preparedStmtContainer, batchCount, user, resolvedIdentity[0], transaction); } catch (Exception e) { ErrorReporter.report(e); } } - return true; + return new EntityProcessResult(resolvedIdentity[0], identityActive[0], createdIdentity); + } + + static final class EntityProcessResult { + + final EntitySpawnIdentity identity; + final boolean identityActive; + final boolean createdIdentity; + + private EntityProcessResult(EntitySpawnIdentity identity, boolean identityActive, boolean createdIdentity) { + this.identity = identity; + this.identityActive = identityActive; + this.createdIdentity = createdIdentity; + } } static void process(ConsumerWriteBatch preparedStmtContainer, ConsumerWriteBatch preparedStmtItems, int batchCount, int processId, int id, Material type, int forceData, String user, Object object) { diff --git a/src/main/java/net/coreprotect/consumer/process/Process.java b/src/main/java/net/coreprotect/consumer/process/Process.java index af22e82ac..f3675fc20 100755 --- a/src/main/java/net/coreprotect/consumer/process/Process.java +++ b/src/main/java/net/coreprotect/consumer/process/Process.java @@ -328,19 +328,25 @@ else if (action == Process.ENTITY_SPAWN_UPDATE || action == Process.ENTITY_CONTA case Process.ENTITY_CONTAINER_TRANSACTION: EntityContainerTransaction transaction = (EntityContainerTransaction) object; EntitySpawnIdentity identity = entitySpawnIdentities.get(transaction.getEntityUuid()); - boolean processedEntityContainerTransaction; + ContainerTransactionProcess.EntityProcessResult entityContainerResult; try { - processedEntityContainerTransaction = ContainerTransactionProcess.processEntity(writeBatch, i, user, transaction, identity); + entityContainerResult = ContainerTransactionProcess.processEntity(writeBatch, i, user, transaction, identity); } catch (Exception e) { - pendingEntityContainerTransactions.add(new PendingEntityContainerTransaction(user, transaction, true)); + pendingEntityContainerTransactions.add(new PendingEntityContainerTransaction(user, transaction, false, true)); throw e; } - if (!processedEntityContainerTransaction) { - pendingEntityContainerTransactions.add(new PendingEntityContainerTransaction(user, transaction, true)); + if (entityContainerResult == null) { + pendingEntityContainerTransactions.add(new PendingEntityContainerTransaction(user, transaction, false, true)); } - else if (ConfigHandler.databaseType.isColumnar()) { - pendingEntityContainerTransactions.add(new PendingEntityContainerTransaction(user, transaction, false)); + else { + entitySpawnIdentities.put(transaction.getEntityUuid(), entityContainerResult.identity); + if (entityContainerResult.createdIdentity) { + promotedEntityIdentities.add(transaction.getEntityUuid()); + } + if (ConfigHandler.databaseType.isColumnar() || transaction.hasIdentityPromotion()) { + pendingEntityContainerTransactions.add(new PendingEntityContainerTransaction(user, transaction, entityContainerResult.identityActive, false)); + } } break; case Process.ENTITY_INTERACTION: @@ -730,10 +736,17 @@ private static void retryEntityContainerTransaction(String user, EntityContainer Queue.queueEntityContainerTransaction(user, retry); } else { // only print exception on development branch + cancelEntityContainerPromotion(transaction); ErrorReporter.report(new IllegalStateException("Dropped entity container transaction without tracking row: " + transaction.getEntityUuid()), ConfigHandler.EDITION_BRANCH.contains("-dev")); } } + private static void cancelEntityContainerPromotion(EntityContainerTransaction transaction) { + if (transaction.hasIdentityPromotion()) { + EntitySpawnTracking.cancelDatabaseIdentityPromotion(transaction.getEntityUuid(), transaction.getCurrentLocation()); + } + } + private static void retryEntityInteraction(String user, EntityInteraction interaction) { EntityInteraction retry = interaction.retry(); if (retry != null) { @@ -1103,20 +1116,70 @@ private static void clearConsumerData(int processId, ArrayList consume } private static void completeEntityContainerTransactions(List transactions, TransactionOutcome outcome) { + if (outcome == TransactionOutcome.COMMITTED) { + confirmEntityContainerPromotions(transactions); + } + else if (outcome == TransactionOutcome.DISCARDED) { + verifyEntityContainerPromotions(transactions); + } if (outcome == TransactionOutcome.COMMITTED || outcome == TransactionOutcome.RETRY) { - for (PendingEntityContainerTransaction pending : transactions) { - if (outcome == TransactionOutcome.COMMITTED && !pending.retryRequired) { - continue; - } - try { - retryEntityContainerTransaction(pending.user, pending.transaction); + retryEntityContainerTransactions(transactions, outcome); + } + transactions.clear(); + } + + private static void confirmEntityContainerPromotions(List transactions) { + for (PendingEntityContainerTransaction pending : transactions) { + if (pending.retryRequired || !pending.transaction.hasIdentityPromotion()) { + continue; + } + try { + if (pending.identityActive) { + EntitySpawnTracking.confirmDatabaseIdentity(pending.transaction.getEntityUuid(), pending.transaction.getCurrentLocation()); } - catch (Exception e) { - ErrorReporter.report(e); + else { + EntitySpawnTracking.clearTracking(pending.transaction.getEntityUuid()); } } + catch (Exception e) { + ErrorReporter.report(e); + } + } + } + + private static void verifyEntityContainerPromotions(List transactions) { + Set verifiedIdentities = new HashSet<>(); + for (PendingEntityContainerTransaction pending : transactions) { + if (pending.retryRequired || !pending.transaction.hasIdentityPromotion() || !verifiedIdentities.add(pending.transaction.getEntityUuid())) { + continue; + } + try { + EntitySpawnTracking.verifyPendingDatabaseIdentity(pending.transaction.getEntityUuid(), pending.transaction.getCurrentLocation()); + } + catch (Exception e) { + verifiedIdentities.remove(pending.transaction.getEntityUuid()); + ErrorReporter.report(e); + } + } + for (PendingEntityContainerTransaction pending : transactions) { + if (!pending.retryRequired && verifiedIdentities.contains(pending.transaction.getEntityUuid())) { + cancelEntityContainerPromotion(pending.transaction); + } + } + } + + private static void retryEntityContainerTransactions(List transactions, TransactionOutcome outcome) { + for (PendingEntityContainerTransaction pending : transactions) { + if (outcome == TransactionOutcome.COMMITTED && !pending.retryRequired) { + continue; + } + try { + retryEntityContainerTransaction(pending.user, pending.transaction); + } + catch (Exception e) { + ErrorReporter.report(e); + } } - transactions.clear(); } private static void completeEntitySpawnLogs(List pendingEntitySpawnLogs, TransactionOutcome outcome) { @@ -1234,11 +1297,13 @@ private static final class PendingEntityContainerTransaction { private final String user; private final EntityContainerTransaction transaction; + private final boolean identityActive; private final boolean retryRequired; - private PendingEntityContainerTransaction(String user, EntityContainerTransaction transaction, boolean retryRequired) { + private PendingEntityContainerTransaction(String user, EntityContainerTransaction transaction, boolean identityActive, boolean retryRequired) { this.user = user; this.transaction = transaction; + this.identityActive = identityActive; this.retryRequired = retryRequired; } } diff --git a/src/main/java/net/coreprotect/listener/player/InventoryChangeListener.java b/src/main/java/net/coreprotect/listener/player/InventoryChangeListener.java index c85d98bb8..25678e9cd 100644 --- a/src/main/java/net/coreprotect/listener/player/InventoryChangeListener.java +++ b/src/main/java/net/coreprotect/listener/player/InventoryChangeListener.java @@ -16,6 +16,7 @@ import org.bukkit.block.BlockState; import org.bukkit.block.DoubleChest; import org.bukkit.block.Hopper; +import org.bukkit.entity.ChestedHorse; import org.bukkit.entity.Entity; import org.bukkit.entity.Item; import org.bukkit.entity.Player; @@ -25,8 +26,10 @@ import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.event.inventory.InventoryMoveItemEvent; +import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.inventory.InventoryPickupItemEvent; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.BlockInventoryHolder; @@ -54,6 +57,7 @@ public final class InventoryChangeListener extends Queue implements Listener { private static ConcurrentHashMap inventoryProcessing = new ConcurrentHashMap<>(); private static final Map pendingEntityTransactions = new HashMap<>(); + private static final Map openHorseContainers = new ConcurrentHashMap<>(); public static boolean inventoryTransaction(String user, Location location, ItemStack[] inventoryData) { if (location != null) { @@ -273,8 +277,11 @@ static void onInventoryInteractAsync(Player player, Inventory inventory, boolean return; } - Entity entityContainer = getTrackedEntityContainer(PaperAdapter.ADAPTER.getHolder(inventory, false)); + Entity entityContainer = getEntityContainer(PaperAdapter.ADAPTER.getHolder(inventory, false)); if (entityContainer != null) { + if (entityContainer instanceof ChestedHorse) { + return; + } captureEntityContainerTransaction(player.getName(), entityContainer, inventory); return; } @@ -455,7 +462,7 @@ private static boolean recordEntityContainerRemoval(Entity entity, ItemStack ite } private static void queueEntityContainerDelta(String user, Entity entity, ItemStack[] oldContents, ItemStack[] newContents) { - if (!EntitySpawnTracking.isTracked(entity)) { + if (!isEntityContainer(entity)) { return; } if (oldContents == null || newContents == null || ItemUtils.compareContainers(oldContents, newContents)) { @@ -463,20 +470,75 @@ private static void queueEntityContainerDelta(String user, Entity entity, ItemSt } Location currentLocation = entity.getLocation(); EntitySpawnTracking.checkpoint(entity, currentLocation); - Queue.queueEntityContainerTransaction(user, entity.getUniqueId(), currentLocation, oldContents, newContents); + Queue.queueEntityContainerTransaction(user, entity, oldContents, newContents); } - private static Entity getTrackedEntityContainer(InventoryHolder holder) { + private static Entity getEntityContainer(InventoryHolder holder) { if (!(holder instanceof Entity)) { return null; } Entity entity = (Entity) holder; - return EntitySpawnTracking.isPlacedEntity(entity) && EntitySpawnTracking.isTracked(entity) ? entity : null; + return isEntityContainer(entity) ? entity : null; + } + + public static boolean isEntityContainer(Entity entity) { + if (!(entity instanceof InventoryHolder)) { + return false; + } + if (EntitySpawnTracking.isPlacedEntity(entity)) { + return EntitySpawnTracking.isTracked(entity); + } + return entity instanceof ChestedHorse && ((ChestedHorse) entity).isCarryingChest(); } private static boolean isSupportedContainer(InventoryHolder holder) { - return holder instanceof BlockInventoryHolder || holder instanceof DoubleChest || getTrackedEntityContainer(holder) != null; + return holder instanceof BlockInventoryHolder || holder instanceof DoubleChest || getEntityContainer(holder) != null; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + protected void onInventoryOpen(InventoryOpenEvent event) { + if (!(event.getPlayer() instanceof Player)) { + return; + } + Inventory inventory = event.getInventory(); + InventoryHolder holder = PaperAdapter.ADAPTER.getHolder(inventory, false); + if (!(holder instanceof ChestedHorse)) { + return; + } + ChestedHorse horse = (ChestedHorse) holder; + if (!horse.isCarryingChest() || !Config.getConfig(horse.getWorld()).ITEM_TRANSACTIONS) { + return; + } + Player player = (Player) event.getPlayer(); + openHorseContainers.put(player.getUniqueId(), new OpenHorseContainer(player.getName(), horse, inventory.getContents())); + } + + @EventHandler(priority = EventPriority.MONITOR) + protected void onInventoryClose(InventoryCloseEvent event) { + OpenHorseContainer opened = openHorseContainers.remove(event.getPlayer().getUniqueId()); + if (opened != null) { + opened.log(event.getInventory().getContents()); + } + } + + private static final class OpenHorseContainer { + + private final String user; + private final ChestedHorse horse; + private final ItemStack[] contents; + + private OpenHorseContainer(String user, ChestedHorse horse, ItemStack[] contents) { + this.user = user; + this.horse = horse; + this.contents = ItemUtils.getContainerState(contents); + } + + private void log(ItemStack[] currentContents) { + if (horse.isValid()) { + queueEntityContainerDelta(user, horse, contents, currentContents); + } + } } private static final class PendingEntityContainerTransaction { @@ -737,8 +799,8 @@ protected void onInventoryMoveItemEvent(InventoryMoveItemEvent event) { return; } - Entity sourceEntity = getTrackedEntityContainer(sourceHolder); - Entity destinationEntity = getTrackedEntityContainer(destinationHolder); + Entity sourceEntity = getEntityContainer(sourceHolder); + Entity destinationEntity = getEntityContainer(destinationHolder); if (sourceEntity != null || destinationEntity != null) { processEntityInventoryMove(location, sourceHolder, destinationHolder, sourceEntity, destinationEntity, event.getItem(), hopperTransactions); return; @@ -856,7 +918,7 @@ protected void onInventoryPickupItem(InventoryPickupItemEvent event) { } InventoryHolder holder = PaperAdapter.ADAPTER.getHolder(inventory, false); - Entity entityContainer = getTrackedEntityContainer(holder); + Entity entityContainer = getEntityContainer(holder); if (!(holder instanceof Hopper) && (entityContainer == null || !Validate.isHopper(holder))) { return; } diff --git a/src/main/java/net/coreprotect/listener/player/PlayerInteractEntityListener.java b/src/main/java/net/coreprotect/listener/player/PlayerInteractEntityListener.java index cffc33b04..1afd2a920 100644 --- a/src/main/java/net/coreprotect/listener/player/PlayerInteractEntityListener.java +++ b/src/main/java/net/coreprotect/listener/player/PlayerInteractEntityListener.java @@ -16,7 +16,6 @@ import org.bukkit.event.player.PlayerArmorStandManipulateEvent; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import net.coreprotect.bukkit.BukkitAdapter; @@ -47,7 +46,7 @@ protected void onPlayerInteractEntity(PlayerInteractEntityEvent event) { Player player = event.getPlayer(); final Entity entity = event.getRightClicked(); // change item in ItemFrame, etc - if (Boolean.TRUE.equals(ConfigHandler.inspecting.get(player.getName())) && EntitySpawnTracking.isPlacedEntity(entity) && entity instanceof InventoryHolder && Config.getConfig(player.getWorld()).ITEM_TRANSACTIONS) { + if (Boolean.TRUE.equals(ConfigHandler.inspecting.get(player.getName())) && InventoryChangeListener.isEntityContainer(entity) && Config.getConfig(player.getWorld()).ITEM_TRANSACTIONS) { String playerUuid = player.getUniqueId().toString(); long now = System.currentTimeMillis(); Object[] previousInspection = PlayerInteractListener.lastInspectorEvent.get(playerUuid); diff --git a/src/main/java/net/coreprotect/model/entity/EntityContainerTransaction.java b/src/main/java/net/coreprotect/model/entity/EntityContainerTransaction.java index 4f81287d5..5215d5829 100644 --- a/src/main/java/net/coreprotect/model/entity/EntityContainerTransaction.java +++ b/src/main/java/net/coreprotect/model/entity/EntityContainerTransaction.java @@ -12,30 +12,44 @@ public final class EntityContainerTransaction { private static final int MAX_IDENTITY_RESOLUTION_ATTEMPTS = 3; private final UUID entityUuid; + private final EntityInteractionOrigin origin; private final Location currentLocation; private final ItemStack[] oldContents; private final ItemStack[] newContents; + private final int time; private final int identityResolutionAttempts; + private final boolean identityPromotion; public EntityContainerTransaction(UUID entityUuid, Location currentLocation, ItemStack[] oldContents, ItemStack[] newContents) { - this(entityUuid, currentLocation, oldContents, newContents, 0); + this(entityUuid, null, currentLocation, oldContents, newContents, (int) (System.currentTimeMillis() / 1000L), 0, false); } - private EntityContainerTransaction(UUID entityUuid, Location currentLocation, ItemStack[] oldContents, ItemStack[] newContents, int identityResolutionAttempts) { + public EntityContainerTransaction(UUID entityUuid, EntityInteractionOrigin origin, Location currentLocation, ItemStack[] oldContents, ItemStack[] newContents) { + this(entityUuid, origin, currentLocation, oldContents, newContents, (int) (System.currentTimeMillis() / 1000L), 0, false); + } + + private EntityContainerTransaction(UUID entityUuid, EntityInteractionOrigin origin, Location currentLocation, ItemStack[] oldContents, ItemStack[] newContents, int time, int identityResolutionAttempts, boolean identityPromotion) { if (entityUuid == null || currentLocation == null || currentLocation.getWorld() == null || oldContents == null || newContents == null) { throw new IllegalArgumentException("Invalid entity container transaction"); } this.entityUuid = entityUuid; + this.origin = origin; this.currentLocation = currentLocation.clone(); this.oldContents = ItemUtils.getContainerState(oldContents); this.newContents = ItemUtils.getContainerState(newContents); + this.time = time; this.identityResolutionAttempts = identityResolutionAttempts; + this.identityPromotion = identityPromotion; } public UUID getEntityUuid() { return entityUuid; } + public EntityInteractionOrigin getOrigin() { + return origin; + } + public Location getCurrentLocation() { return currentLocation.clone(); } @@ -48,11 +62,26 @@ public ItemStack[] getNewContents() { return ItemUtils.getContainerState(newContents); } + public int getTime() { + return time; + } + + public boolean hasIdentityPromotion() { + return identityPromotion; + } + + public EntityContainerTransaction withIdentityPromotion(boolean promotion) { + if (identityPromotion == promotion) { + return this; + } + return new EntityContainerTransaction(entityUuid, origin, currentLocation, oldContents, newContents, time, identityResolutionAttempts, promotion); + } + public EntityContainerTransaction retry() { if (!canRetry()) { return null; } - return new EntityContainerTransaction(entityUuid, currentLocation, oldContents, newContents, identityResolutionAttempts + 1); + return new EntityContainerTransaction(entityUuid, origin, currentLocation, oldContents, newContents, time, identityResolutionAttempts + 1, identityPromotion); } public boolean canRetry() { From cac901df487e819d5afe31137eba9a866422bddc Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 6 Oct 2026 09:17:12 +0300 Subject: [PATCH 2/5] Fix chested horse transaction attribution, replay and lifecycle handling --- .gitignore | 1 - pom.xml | 12 + .../coreprotect/consumer/process/Process.java | 10 +- .../rollback/EntitySpawnRollbackHandler.java | 55 +++- .../listener/entity/EntityDeathListener.java | 2 + .../entity/TrackedEntityRemoveListener.java | 2 +- .../player/InventoryChangeListener.java | 109 ++++--- .../process/HorseIdentityPromotionTest.java | 102 +++++++ .../rollback/HorseContainerReplayTest.java | 81 ++++++ .../player/HorseTransactionCaptureTest.java | 265 ++++++++++++++++++ .../coreprotect/testing/TestItemStack.java | 55 ++++ 11 files changed, 615 insertions(+), 79 deletions(-) create mode 100644 src/test/java/net/coreprotect/consumer/process/HorseIdentityPromotionTest.java create mode 100644 src/test/java/net/coreprotect/database/rollback/HorseContainerReplayTest.java create mode 100644 src/test/java/net/coreprotect/listener/player/HorseTransactionCaptureTest.java create mode 100644 src/test/java/net/coreprotect/testing/TestItemStack.java diff --git a/.gitignore b/.gitignore index 26bf9102f..580487532 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,6 @@ nbproject target dependency-reduced-pom.xml -src/test maven_output.txt # Wrapper files diff --git a/pom.xml b/pom.xml index 9a6a9bec3..425cdd042 100644 --- a/pom.xml +++ b/pom.xml @@ -210,6 +210,18 @@ + + org.junit.jupiter + junit-jupiter + 5.13.4 + test + + + org.mockito + mockito-core + 5.20.0 + test +