Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/api/version/v13.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand All @@ -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

Expand Down
34 changes: 33 additions & 1 deletion src/main/java/net/coreprotect/consumer/Queue.java
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,62 @@
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;
import net.coreprotect.utility.HopperTransactionUtils;

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) {
Expand Down
105 changes: 88 additions & 17 deletions src/main/java/net/coreprotect/consumer/process/Process.java
Original file line number Diff line number Diff line change
Expand Up @@ -330,19 +330,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:
Expand Down Expand Up @@ -739,10 +745,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) {
Expand Down Expand Up @@ -998,6 +1011,9 @@ else if (action == ITEM_TRANSACTION) {
if (object instanceof EntityInteraction) {
cancelEntityInteractionPromotion((EntityInteraction) object);
}
if (object instanceof EntityContainerTransaction) {
cancelEntityContainerPromotion((EntityContainerTransaction) object);
}
EntitySpawnData spawnData = getEntitySpawnUpdate(object);
if (spawnData != null) {
int trackingRowId = spawnData.getTrackingRowId();
Expand Down Expand Up @@ -1117,20 +1133,73 @@ private static void clearConsumerData(int processId, ArrayList<Object[]> consume
}

private static void completeEntityContainerTransactions(List<PendingEntityContainerTransaction> 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<PendingEntityContainerTransaction> 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<PendingEntityContainerTransaction> transactions) {
Set<UUID> verifiedIdentities = new HashSet<>();
for (PendingEntityContainerTransaction pending : transactions) {
if (!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) {
try {
cancelEntityContainerPromotion(pending.transaction);
}
catch (Exception e) {
ErrorReporter.report(e);
}
}
}

private static void retryEntityContainerTransactions(List<PendingEntityContainerTransaction> 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<PendingEntitySpawnLog> pendingEntitySpawnLogs, TransactionOutcome outcome) {
Expand Down Expand Up @@ -1248,11 +1317,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;
}
}
Expand Down
Loading
Loading