Added entity interaction logging

This commit is contained in:
Intelli committed 2026-07-12 17:32:35 -06:00
1 parent c25621622e
commit 3d1594e116
31 files changed
+1332 -51

No files matched your search

@@ -10,6 +10,7 @@ import org.bukkit.entity.EntityType;
import net.coreprotect.api.SessionLookup;
import net.coreprotect.model.action.LookupActions;
import net.coreprotect.model.action.SessionActions;
import net.coreprotect.model.item.InventorySources;
import net.coreprotect.utility.BlockUtils;
import net.coreprotect.utility.BlockTypeUtils;
import net.coreprotect.utility.EntityUtils;
@@ -137,6 +138,9 @@ public class ParseResult implements CoreProtectResult {
}
private boolean isEntityAction(int actionId) {
return actionId == LookupActions.ENTITY_KILL || actionId == LookupActions.ENTITY_SPAWN;
if (actionId == LookupActions.ENTITY_KILL || actionId == LookupActions.ENTITY_SPAWN) {
return true;
}
return actionId == LookupActions.INTERACTION && parse.length > 14 && parse[13] != null && Integer.parseInt(parse[13]) == InventorySources.ENTITY_INTERACTION;
}
}
@@ -18,6 +18,7 @@ import org.bukkit.entity.Player;
import net.coreprotect.command.lookup.BlockLookupThread;
import net.coreprotect.command.lookup.ChestTransactionLookupThread;
import net.coreprotect.command.lookup.EntityInteractionLookupThread;
import net.coreprotect.command.lookup.StandardLookupThread;
import net.coreprotect.command.parser.ActionParser;
import net.coreprotect.command.parser.MessageFilterParser;
@@ -89,7 +90,7 @@ public class LookupCommand {
argAction.add(LookupActions.ENTITY_KILL);
argAction.add(LookupActions.ENTITY_SPAWN);
}
else if (!argEntityActionFilter.includesAnyEntity(argAction, true)) {
else if (!argEntityActionFilter.includesAnyEntity(argAction, true) && !argAction.contains(LookupActions.INTERACTION)) {
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_INCLUDE_COMBO));
return;
}
@@ -107,7 +108,7 @@ public class LookupCommand {
argAction.add(LookupActions.ENTITY_KILL);
argAction.add(LookupActions.ENTITY_SPAWN);
}
else if (!argEntityActionFilter.includesAnyEntity(argAction, true)) {
else if (!argEntityActionFilter.includesAnyEntity(argAction, true) && !argAction.contains(LookupActions.INTERACTION)) {
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.INVALID_INCLUDE_COMBO));
return;
}
@@ -367,7 +368,7 @@ public class LookupCommand {
Thread thread = new Thread(runnable);
thread.start();
}
else if (type == 2 || type == 3 || type == 7 || type == 8) {
else if (type == 2 || type == 3 || type == 7 || type == 8 || type == 9) {
boolean defaultRe = true;
int page = 1;
int re = 7;
@@ -409,6 +410,13 @@ public class LookupCommand {
}
// String bc = x+"."+y+"."+z+"."+wid+"."+rstring+"."+lookup_user;
if (type == 9) {
Runnable runnable = new EntityInteractionLookupThread(player, command, page, re);
Thread thread = new Thread(runnable);
thread.start();
return;
}
String lcommand = ConfigHandler.lookupCommand.get(player.getName());
String[] data = lcommand.split("\\.");
int x = Integer.parseInt(data[0]);
@@ -37,7 +37,7 @@ import net.coreprotect.utility.ErrorReporter;
public class PurgeCommand extends Consumer {
public static final List<String> PURGE_TABLES = Arrays.asList("sign", "container", "entity_container", "item", "skull", "session", "chat", "command", "entity", "block");
public static final List<String> PURGE_TABLES = Arrays.asList("sign", "container", "entity_container", "entity_interaction", "item", "skull", "session", "chat", "command", "entity", "block");
private static String findUnsupportedPurgeArgument(String[] args) {
boolean includeContinuation = false;
@@ -340,7 +340,7 @@ public class PurgeCommand extends Consumer {
Database.createDatabaseTables(purgePrefix, false, null, Config.getGlobal().MYSQL, true);
}
List<String> worldTables = Arrays.asList("sign", "container", "entity_container", "item", "session", "chat", "command", "block");
List<String> worldTables = Arrays.asList("sign", "container", "entity_container", "entity_interaction", "item", "session", "chat", "command", "block");
List<String> restrictTables = Arrays.asList("block");
List<String> excludeTables = Arrays.asList("database_lock"); // don't insert data into these tables
for (String table : ConfigHandler.databaseTables) {
@@ -375,7 +375,7 @@ public class PurgeCommand extends Consumer {
boolean purge = true;
String timeLimit = "";
if (table.equals("entity_spawn")) {
timeLimit = " WHERE removed=0 OR block_rowid IN(SELECT rowid FROM " + purgePrefix + "block) OR kill_rowid IN(SELECT rowid FROM " + purgePrefix + "entity) OR rowid IN(SELECT entity_spawn_rowid FROM " + purgePrefix + "entity_container)";
timeLimit = " WHERE removed=0 OR block_rowid IN(SELECT rowid FROM " + purgePrefix + "block) OR kill_rowid IN(SELECT rowid FROM " + purgePrefix + "entity) OR rowid IN(SELECT entity_spawn_rowid FROM " + purgePrefix + "entity_container) OR rowid IN(SELECT entity_spawn_rowid FROM " + purgePrefix + "entity_interaction)";
}
else if (PURGE_TABLES.contains(table)) {
String blockRestriction = "(";
@@ -387,7 +387,7 @@ public class PurgeCommand extends Consumer {
}
if (argWid > 0 && worldTables.contains(table)) {
if (table.equals("entity_container")) {
if (table.equals("entity_container") || table.equals("entity_interaction")) {
if (purge) {
String worldMatch = "(wid = '" + argWid + "' OR entity_spawn_rowid IN(SELECT rowid FROM " + ConfigHandler.prefix + "entity_spawn WHERE current_wid = '" + argWid + "'))";
timeLimit = " WHERE (" + worldMatch + " AND (time >= '" + timeEnd + "' OR time < '" + timeStart + "')) OR NOT " + worldMatch;
@@ -462,7 +462,7 @@ public class PurgeCommand extends Consumer {
String worldRestriction = "";
if (argWid > 0 && worldTables.contains(table)) {
if (table.equals("entity_container")) {
if (table.equals("entity_container") || table.equals("entity_interaction")) {
worldRestriction = " AND (wid = '" + argWid + "' OR entity_spawn_rowid IN(SELECT rowid FROM " + ConfigHandler.prefix + "entity_spawn WHERE current_wid = '" + argWid + "'))";
}
else {
@@ -534,7 +534,7 @@ public class PurgeCommand extends Consumer {
String worldRestriction = "";
if (argWid > 0 && worldTables.contains(table)) {
if (table.equals("entity_container")) {
if (table.equals("entity_container") || table.equals("entity_interaction")) {
worldRestriction = " AND (wid = '" + argWid + "' OR entity_spawn_rowid IN(SELECT rowid FROM " + ConfigHandler.prefix + "entity_spawn WHERE current_wid = '" + argWid + "'))";
}
else {
@@ -575,7 +575,19 @@ public class PurgeCommand extends Consumer {
preparedStmt.executeUpdate();
preparedStmt.close();
query = "DELETE FROM " + retainedPrefix + "entity_spawn WHERE removed=1 AND block_rowid IS NULL AND kill_rowid IS NULL AND NOT EXISTS (SELECT 1 FROM " + retainedPrefix + "entity_container WHERE " + retainedPrefix + "entity_container.entity_spawn_rowid=" + retainedPrefix + "entity_spawn.rowid)";
query = "DELETE FROM " + retainedPrefix + "entity_interaction WHERE NOT EXISTS (SELECT 1 FROM " + retainedPrefix + "entity_spawn WHERE " + retainedPrefix + "entity_spawn.rowid=" + retainedPrefix + "entity_interaction.entity_spawn_rowid)";
preparedStmt = connection.prepareStatement(query);
preparedStmt.executeUpdate();
removed = removed + preparedStmt.getUpdateCount();
preparedStmt.close();
query = "DELETE FROM " + retainedPrefix + "entity_container WHERE NOT EXISTS (SELECT 1 FROM " + retainedPrefix + "entity_spawn WHERE " + retainedPrefix + "entity_spawn.rowid=" + retainedPrefix + "entity_container.entity_spawn_rowid)";
preparedStmt = connection.prepareStatement(query);
preparedStmt.executeUpdate();
removed = removed + preparedStmt.getUpdateCount();
preparedStmt.close();
query = "DELETE FROM " + retainedPrefix + "entity_spawn WHERE removed=1 AND block_rowid IS NULL AND kill_rowid IS NULL AND NOT EXISTS (SELECT 1 FROM " + retainedPrefix + "entity_container WHERE " + retainedPrefix + "entity_container.entity_spawn_rowid=" + retainedPrefix + "entity_spawn.rowid) AND NOT EXISTS (SELECT 1 FROM " + retainedPrefix + "entity_interaction WHERE " + retainedPrefix + "entity_interaction.entity_spawn_rowid=" + retainedPrefix + "entity_spawn.rowid)";
preparedStmt = connection.prepareStatement(query);
preparedStmt.executeUpdate();
removed = removed + preparedStmt.getUpdateCount();
@@ -0,0 +1,55 @@
package net.coreprotect.command.lookup;
import java.sql.Connection;
import java.sql.Statement;
import java.util.List;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.database.Database;
import net.coreprotect.database.lookup.EntityInteractionLookup;
import net.coreprotect.language.Phrase;
import net.coreprotect.utility.Chat;
import net.coreprotect.utility.Color;
import net.coreprotect.utility.ErrorReporter;
public final class EntityInteractionLookupThread implements Runnable {
private final CommandSender player;
private final Command command;
private final int page;
private final int limit;
public EntityInteractionLookupThread(CommandSender player, Command command, int page, int limit) {
this.player = player;
this.command = command;
this.page = page;
this.limit = limit;
}
@Override
public void run() {
try (Connection connection = Database.getConnection(true)) {
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { true, System.currentTimeMillis() });
if (connection == null) {
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.DATABASE_BUSY));
return;
}
Integer entitySpawnRowId = ConfigHandler.lookupEntityInteraction.get(player.getName());
try (Statement statement = connection.createStatement()) {
List<String> results = EntityInteractionLookup.performLookup(command.getName(), statement, player, page, limit, entitySpawnRowId);
for (String result : results) {
Chat.sendComponent(player, result);
}
}
}
catch (Exception e) {
ErrorReporter.report(e);
}
finally {
ConfigHandler.lookupThrottle.put(player.getName(), new Object[] { false, System.currentTimeMillis() });
}
}
}
@@ -26,6 +26,7 @@ import com.google.common.base.Strings;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.database.Database;
import net.coreprotect.database.Lookup;
import net.coreprotect.database.lookup.EntityInteractionLookup;
import net.coreprotect.database.lookup.PlayerLookup;
import net.coreprotect.database.statement.EntitySpawnStatement;
import net.coreprotect.database.statement.UserStatement;
@@ -126,6 +127,7 @@ public class StandardLookupThread implements Runnable {
List<String> uuidList = new ArrayList<>();
Integer entityContainerId = actions.contains(5) ? ConfigHandler.lookupEntityContainer.get(player.getName()) : null;
ConfigHandler.lookupEntityInteraction.remove(player.getName());
if (!actions.contains(5)) {
ConfigHandler.lookupEntityContainer.remove(player.getName());
}
@@ -212,8 +214,9 @@ public class StandardLookupThread implements Runnable {
Set<UUID> loadedEntityCandidates = Collections.emptySet();
boolean includeEntitySpawns = entityActionFilter.includesAnySpawn(actions, true);
boolean includeEntityContainers = entityContainerId != null || actions.contains(LookupActions.CONTAINER) || LookupActions.isInventoryLookup(actions) || actions.isEmpty();
if ((includeEntitySpawns || includeEntityContainers) && radius != null && finalLocation != null && finalLocation.getWorld() != null) {
Set<UUID> databaseCandidates = includeEntityContainers ? EntitySpawnStatement.loadActiveUuids(connection, finalLocation, radius) : EntitySpawnStatement.loadActiveUuids(connection, finalLocation, radius, timeStart, timeEnd);
boolean includeEntityInteractions = actions.isEmpty() || actions.contains(LookupActions.INTERACTION);
if ((includeEntitySpawns || includeEntityContainers || includeEntityInteractions) && radius != null && finalLocation != null && finalLocation.getWorld() != null) {
Set<UUID> databaseCandidates = includeEntityContainers || includeEntityInteractions ? EntitySpawnStatement.loadActiveUuids(connection, finalLocation, radius) : EntitySpawnStatement.loadActiveUuids(connection, finalLocation, radius, timeStart, timeEnd);
EntitySpawnTracking.LoadedEntityRadius loadedEntities = EntitySpawnTracking.findLoadedEntities(finalLocation, radius, databaseCandidates);
loadedEntityUuids = loadedEntities.getInside();
loadedEntityCandidates = loadedEntities.getLoadedCandidates();
@@ -289,6 +292,9 @@ public class StandardLookupThread implements Runnable {
if (data.length > 14 && data[13] != null && data[14] != null && Integer.parseInt(data[13]) == InventorySources.ENTITY_CONTAINER) {
entitySpawnRowIds.add(Integer.parseInt(data[14]));
}
if (data.length > 14 && data[13] != null && data[14] != null && Integer.parseInt(data[13]) == InventorySources.ENTITY_INTERACTION) {
entitySpawnRowIds.add(Integer.parseInt(data[14]));
}
}
if (!entitySpawnRowIds.isEmpty()) {
entitySpawnRecords = EntitySpawnStatement.loadLocationRecords(connection, entitySpawnRowIds);
@@ -462,6 +468,7 @@ public class StandardLookupThread implements Runnable {
int dtype = Integer.parseInt(data[5]);
int ddata = Integer.parseInt(data[6]);
int daction = Integer.parseInt(data[7]);
boolean entityInteraction = data[13] != null && Integer.parseInt(data[13]) == InventorySources.ENTITY_INTERACTION;
boolean placedEntitySpawn = daction == LookupActions.ENTITY_SPAWN && EntitySpawnTracking.isPlacedEntityType(EntityUtils.getEntityType(dtype));
boolean placedEntityKill = daction == LookupActions.ENTITY_KILL && EntitySpawnTracking.isPlacedEntityType(EntityUtils.getEntityType(dtype));
int wid = Integer.parseInt(data[9]);
@@ -487,7 +494,10 @@ public class StandardLookupThread implements Runnable {
String dname = "";
boolean isPlayer = false;
if ((daction == LookupActions.ENTITY_KILL || daction == LookupActions.ENTITY_SPAWN) && !actions.contains(LookupActions.ITEM) && amount == -1) {
if (entityInteraction) {
dname = EntityInteractionLookup.entityName(dtype);
}
else if ((daction == LookupActions.ENTITY_KILL || daction == LookupActions.ENTITY_SPAWN) && !actions.contains(LookupActions.ITEM) && amount == -1) {
if (daction == LookupActions.ENTITY_KILL && dtype == 0) {
if (ConfigHandler.playerIdCacheReversed.get(ddata) == null) {
UserStatement.loadName(connection, ddata);
@@ -551,7 +561,12 @@ public class StandardLookupThread implements Runnable {
PluginChannelListener.getInstance().sendData(player, Integer.parseInt(time), phrase, selector, dplayer, dname, (tag.contains("+") ? 1 : -1), dataX, dataY, dataZ, wid, rbd, action.contains("container"), tag.contains("+"));
}
else {
if (daction == LookupActions.ENTITY_SPAWN) {
if (entityInteraction) {
phrase = Phrase.LOOKUP_ENTITY_INTERACTION;
selector = EntityInteractionLookup.actionSelector(ddata);
action = "a:click";
}
else if (daction == LookupActions.ENTITY_SPAWN) {
phrase = placedEntitySpawn ? Phrase.LOOKUP_BLOCK : Phrase.LOOKUP_ENTITY_SPAWN;
selector = Selector.FIRST;
tag = Color.GREEN + "+";
@@ -669,7 +684,7 @@ public class StandardLookupThread implements Runnable {
}
int trackingRowId = 0;
if (data[13] != null && Integer.parseInt(data[13]) == InventorySources.ENTITY_CONTAINER && data[14] != null) {
if (data[13] != null && (Integer.parseInt(data[13]) == InventorySources.ENTITY_CONTAINER || Integer.parseInt(data[13]) == InventorySources.ENTITY_INTERACTION) && data[14] != null) {
trackingRowId = Integer.parseInt(data[14]);
}
else if (data[6] != null && Integer.parseInt(data[7]) == LookupActions.ENTITY_SPAWN) {
@@ -696,7 +711,8 @@ public class StandardLookupThread implements Runnable {
currentZ = loadedLocation.getBlockZ();
}
return new EntityDisplayLocation(currentWorldId, currentX, currentY, currentZ, Integer.parseInt(data[9]), Integer.parseInt(data[2]), Integer.parseInt(data[3]), Integer.parseInt(data[4]));
boolean interactionOrigin = data[13] != null && Integer.parseInt(data[13]) == InventorySources.ENTITY_INTERACTION;
return new EntityDisplayLocation(currentWorldId, currentX, currentY, currentZ, Integer.parseInt(data[9]), Integer.parseInt(data[2]), Integer.parseInt(data[3]), Integer.parseInt(data[4]), interactionOrigin);
}
private static final class EntityDisplayLocation {
@@ -708,8 +724,9 @@ public class StandardLookupThread implements Runnable {
private final int originX;
private final int originY;
private final int originZ;
private final boolean interactionOrigin;
private EntityDisplayLocation(int worldId, int x, int y, int z, int originWorldId, int originX, int originY, int originZ) {
private EntityDisplayLocation(int worldId, int x, int y, int z, int originWorldId, int originX, int originY, int originZ, boolean interactionOrigin) {
this.worldId = worldId;
this.x = x;
this.y = y;
@@ -718,13 +735,15 @@ public class StandardLookupThread implements Runnable {
this.originX = originX;
this.originY = originY;
this.originZ = originZ;
this.interactionOrigin = interactionOrigin;
}
private String getOriginTooltip() {
if (worldId == originWorldId && x == originX && y == originY && z == originZ) {
return "";
}
return ChatUtils.getCoordinateTooltip(originWorldId, originX, originY, originZ, Phrase.build(Phrase.LOOKUP_ENTITY_ORIGIN), true);
Phrase originPhrase = interactionOrigin ? Phrase.LOOKUP_ENTITY_INTERACTION_ORIGIN : Phrase.LOOKUP_ENTITY_ORIGIN;
return ChatUtils.getCoordinateTooltip(originWorldId, originX, originY, originZ, Phrase.build(originPhrase), true);
}
}
}
@@ -136,6 +136,7 @@ public class ConfigHandler extends Queue {
public static Map<String, LookupOutputMode> lookupOutputMode = syncMap();
public static Map<String, String> lookupCommand = syncMap();
public static Map<String, Integer> lookupEntityContainer = syncMap();
public static Map<String, Integer> lookupEntityInteraction = syncMap();
public static Map<String, List<Object>> lookupBlist = syncMap();
public static Map<String, Map<Object, Boolean>> lookupElist = syncMap();
public static Map<String, List<String>> lookupEUserlist = syncMap();
@@ -17,6 +17,7 @@ import org.bukkit.block.data.Bisected.Half;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.block.data.type.Bed;
import org.bukkit.block.data.type.Bed.Part;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
@@ -30,13 +31,17 @@ import net.coreprotect.listener.block.BlockUtil;
import net.coreprotect.model.BlockGroup;
import net.coreprotect.model.entity.EntityContainerRollbackUpdate;
import net.coreprotect.model.entity.EntityContainerTransaction;
import net.coreprotect.model.entity.EntityInteraction;
import net.coreprotect.model.entity.EntityInteractionAction;
import net.coreprotect.model.entity.EntityInteractionOrigin;
import net.coreprotect.model.entity.EntitySpawnData;
import net.coreprotect.thread.Scheduler;
import net.coreprotect.thread.CacheHandler;
import net.coreprotect.thread.Scheduler;
import net.coreprotect.utility.BlockUtils;
import net.coreprotect.utility.EntitySpawnTracking;
import net.coreprotect.utility.EntityUtils;
import net.coreprotect.utility.WorldUtils;
import net.coreprotect.utility.ErrorReporter;
import net.coreprotect.utility.WorldUtils;
public class Queue {
@@ -331,6 +336,30 @@ public class Queue {
queueStandardData(new Object[] { null, Process.ENTITY_CONTAINER_TRANSACTION, null, 0, null, 0, 0, null }, new String[] { user, null }, transaction, false, Consumer.reserveConsumer());
}
public static void queueEntityInteraction(String user, Entity entity, EntityInteractionAction action) {
queueEntityInteraction(user, entity, action, null);
}
public static void queueEntityInteraction(String user, Entity entity, EntityInteractionAction action, byte[] metadata) {
if (user == null || user.trim().isEmpty() || entity == null || action == null) {
return;
}
Location currentLocation = entity.getLocation();
EntityInteractionOrigin origin = EntitySpawnTracking.getOrCreateInteractionOrigin(entity);
if (origin == null || currentLocation.getWorld() == null) {
return;
}
queueEntityInteraction(user, new EntityInteraction(entity.getUniqueId(), entity.getType(), origin, currentLocation, action, metadata));
}
public static void queueEntityInteraction(String user, EntityInteraction interaction) {
if (user == null || user.trim().isEmpty() || interaction == null) {
return;
}
queueStandardData(new Object[] { null, Process.ENTITY_INTERACTION, null, 0, null, 0, 0, null }, new String[] { user, null }, interaction, false, Consumer.reserveConsumer());
}
protected static void queueItemTransaction(String user, Location location, int time, int offset, int itemId) {
queueStandardData(new Object[] { null, Process.ITEM_TRANSACTION, null, offset, null, time, itemId, null }, new String[] { user, null }, location, false, Consumer.reserveConsumer());
}
@@ -22,10 +22,13 @@ import net.coreprotect.config.ConfigHandler;
import net.coreprotect.consumer.Consumer;
import net.coreprotect.consumer.Queue;
import net.coreprotect.database.Database;
import net.coreprotect.database.logger.EntityInteractionLogger;
import net.coreprotect.database.statement.EntityInteractionStatement;
import net.coreprotect.database.statement.EntitySpawnStatement;
import net.coreprotect.database.statement.UserStatement;
import net.coreprotect.model.entity.EntityContainerRollbackUpdate;
import net.coreprotect.model.entity.EntityContainerTransaction;
import net.coreprotect.model.entity.EntityInteraction;
import net.coreprotect.model.entity.EntitySpawnData;
import net.coreprotect.model.entity.EntitySpawnIdentity;
import net.coreprotect.model.rollback.RollbackUpdateTargets;
@@ -67,6 +70,7 @@ public class Process {
public static final int ENTITY_CONTAINER_TRANSACTION = 32;
public static final int ENTITY_CONTAINER_ROLLBACK_UPDATE = 33;
public static final int ENTITY_CONTAINER_TRANSITION_UPDATE = 34;
public static final int ENTITY_INTERACTION = 35;
public static int lastLockUpdate = 0;
private static volatile int currentConsumerSize = 0;
@@ -104,6 +108,9 @@ public class Process {
protected static void processConsumer(int processId, boolean lastRun) {
Map<UUID, Location> pendingEntitySpawnLogs = new LinkedHashMap<>();
Map<UUID, EntitySpawnIdentity> entitySpawnIdentities = new LinkedHashMap<>();
Map<UUID, Location> pendingEntityIdentityConfirmations = new LinkedHashMap<>();
Set<UUID> promotedEntityIdentities = new HashSet<>();
List<PendingEntityInteraction> pendingEntityInteractions = new ArrayList<>();
List<EntityContainerRollbackRetry> pendingEntityContainerRollbacks = new ArrayList<>();
EntitySpawnStatement.Updates entitySpawnUpdates = null;
ArrayList<Object[]> consumerData = null;
@@ -138,6 +145,7 @@ public class Process {
boolean hasEntitySpawnUpdates = false;
boolean hasEntityKills = false;
boolean hasEntityContainerTransactions = false;
boolean hasEntityInteractions = false;
Set<UUID> entityIdentityUuids = new HashSet<>();
Set<Integer> entityIdentityRowIds = new HashSet<>();
for (int index = 0; index < consumerDataSize; index++) {
@@ -150,6 +158,7 @@ public class Process {
hasEntitySpawnUpdates |= action == Process.ENTITY_SPAWN_UPDATE || action == Process.ENTITY_CONTAINER_TRANSITION_UPDATE;
hasEntityKills |= action == Process.ENTITY_KILL;
hasEntityContainerTransactions |= action == Process.ENTITY_CONTAINER_TRANSACTION;
hasEntityInteractions |= action == Process.ENTITY_INTERACTION;
if (action == Process.ENTITY_CONTAINER_TRANSACTION) {
Object object = consumerObject.get((int) data[0]);
@@ -157,6 +166,12 @@ public class Process {
entityIdentityUuids.add(((EntityContainerTransaction) object).getEntityUuid());
}
}
else if (action == Process.ENTITY_INTERACTION) {
Object object = consumerObject.get((int) data[0]);
if (object instanceof EntityInteraction) {
entityIdentityUuids.add(((EntityInteraction) object).getEntityUuid());
}
}
else if (action == Process.ENTITY_SPAWN_UPDATE || action == Process.ENTITY_CONTAINER_TRANSITION_UPDATE) {
Object object = consumerObject.get((int) data[0]);
EntitySpawnData spawnData = getEntitySpawnUpdate(object);
@@ -195,7 +210,7 @@ public class Process {
return;
}
if (hasEntityContainerTransactions) {
if (hasEntityContainerTransactions || hasEntityInteractions) {
entitySpawnIdentities.putAll(EntitySpawnStatement.loadIdentities(connection, entityIdentityUuids));
Map<Integer, EntitySpawnIdentity> identitiesByRowId = EntitySpawnStatement.loadIdentitiesByRowIds(connection, entityIdentityRowIds);
bindPendingEntitySpawnIdentities(consumerData, consumerObject, entitySpawnIdentities, identitiesByRowId);
@@ -216,8 +231,10 @@ public class Process {
PreparedStatement preparedStmtArt = Database.prepareStatement(connection, Database.ART, false);
PreparedStatement preparedStmtEntity = Database.prepareStatement(connection, Database.ENTITY_MAP, false);
PreparedStatement preparedStmtBlockdata = Database.prepareStatement(connection, Database.BLOCKDATA, false);
PreparedStatement preparedStmtEntitySpawns = hasEntitySpawnLogs ? Database.prepareStatement(connection, Database.ENTITY_SPAWN, true) : null;
PreparedStatement preparedStmtEntitySpawns = hasEntitySpawnLogs || hasEntityInteractions ? Database.prepareStatement(connection, Database.ENTITY_SPAWN, true) : null;
PreparedStatement preparedStmtEntityContainers = hasEntityContainerTransactions ? Database.prepareStatement(connection, Database.ENTITY_CONTAINER, false) : null;
PreparedStatement preparedStmtEntityInteractions = hasEntityInteractions ? Database.prepareStatement(connection, Database.ENTITY_INTERACTION, false) : null;
PreparedStatement preparedStmtEntityInteractionCheckpoints = hasEntityInteractions ? EntityInteractionStatement.prepareCheckpoint(connection) : null;
PreparedStatement preparedStmtEntitySpawnBlocks = hasEntitySpawnLogs ? Database.prepareStatement(connection, Database.BLOCK, true) : null;
PreparedStatement preparedStmtEntitySpawnLinks = hasEntitySpawnLogs ? EntitySpawnStatement.prepareBlockLink(connection) : null;
PreparedStatement preparedStmtEntityKillLinks = hasEntityKills ? EntitySpawnStatement.prepareKillLink(connection) : null;
@@ -272,6 +289,36 @@ public class Process {
retryEntityContainerTransaction(user, transaction);
}
break;
case Process.ENTITY_INTERACTION:
EntityInteraction interaction = (EntityInteraction) object;
EntityInteractionLogger.LogContext logContext = EntityInteractionLogger.prepare(user, interaction);
if (logContext == null) {
break;
}
EntitySpawnIdentity existingIdentity = entitySpawnIdentities.get(interaction.getEntityUuid());
EntitySpawnIdentity[] loggedIdentity = { existingIdentity };
try {
Database.executeSavepoint(statement, "entity_interaction_log", () -> {
if (loggedIdentity[0] == null) {
int time = (int) (System.currentTimeMillis() / 1000L);
loggedIdentity[0] = EntitySpawnStatement.insertIdentity(preparedStmtEntitySpawns, time, interaction.getEntityUuid(), interaction.getOrigin(), interaction.getCurrentLocation());
}
EntityInteractionLogger.log(preparedStmtEntityInteractions, preparedStmtEntityInteractionCheckpoints, loggedIdentity[0], interaction, logContext);
});
}
catch (Exception e) {
retryEntityInteraction(user, interaction);
throw e;
}
entitySpawnIdentities.put(interaction.getEntityUuid(), loggedIdentity[0]);
pendingEntityInteractions.add(new PendingEntityInteraction(user, interaction));
if (existingIdentity == null) {
promotedEntityIdentities.add(interaction.getEntityUuid());
}
pendingEntityIdentityConfirmations.put(interaction.getEntityUuid(), interaction.getCurrentLocation());
break;
case Process.ITEM_TRANSACTION:
ItemTransactionProcess.process(preparedStmtItems, i, processId, id, forceData, replaceData, blockData, user, object);
break;
@@ -379,6 +426,7 @@ public class Process {
entitySpawnUpdates.afterCommit(false);
}
retryEntityContainerRollbacks(pendingEntityContainerRollbacks, false);
completeEntityInteractions(pendingEntityInteractions, pendingEntityIdentityConfirmations, promotedEntityIdentities, entitySpawnIdentities, false);
discardProcessedConsumerData(processId, consumerData, users, consumerObject, i + 1);
deferConsumerRetry();
completeEntitySpawnLogs(pendingEntitySpawnLogs, false);
@@ -392,6 +440,7 @@ public class Process {
entitySpawnUpdates.afterCommit(committed);
}
retryEntityContainerRollbacks(pendingEntityContainerRollbacks, committed);
completeEntityInteractions(pendingEntityInteractions, pendingEntityIdentityConfirmations, promotedEntityIdentities, entitySpawnIdentities, committed);
completeEntitySpawnLogs(pendingEntitySpawnLogs, committed);
processedThrough = i + 1;
Thread.sleep(500);
@@ -420,6 +469,7 @@ public class Process {
}
finally {
retryEntityContainerRollbacks(pendingEntityContainerRollbacks, committed);
completeEntityInteractions(pendingEntityInteractions, pendingEntityIdentityConfirmations, promotedEntityIdentities, entitySpawnIdentities, committed);
completeEntitySpawnLogs(pendingEntitySpawnLogs, committed);
}
}
@@ -436,6 +486,10 @@ public class Process {
if (preparedStmtEntityContainers != null) {
preparedStmtEntityContainers.close();
}
if (preparedStmtEntityInteractions != null) {
preparedStmtEntityInteractions.close();
preparedStmtEntityInteractionCheckpoints.close();
}
preparedStmtItems.close();
preparedStmtWorlds.close();
preparedStmtChat.close();
@@ -448,8 +502,10 @@ public class Process {
preparedStmtBlockdata.close();
if (preparedStmtEntitySpawns != null) {
preparedStmtEntitySpawns.close();
preparedStmtEntitySpawnBlocks.close();
preparedStmtEntitySpawnLinks.close();
if (preparedStmtEntitySpawnBlocks != null) {
preparedStmtEntitySpawnBlocks.close();
preparedStmtEntitySpawnLinks.close();
}
}
if (preparedStmtEntityKillLinks != null) {
preparedStmtEntityKillLinks.close();
@@ -467,6 +523,7 @@ public class Process {
entitySpawnUpdates.afterCommit(false);
}
retryEntityContainerRollbacks(pendingEntityContainerRollbacks, false);
completeEntityInteractions(pendingEntityInteractions, pendingEntityIdentityConfirmations, promotedEntityIdentities, entitySpawnIdentities, false);
clearConsumerData(processId, consumerData, users, consumerObject);
consumerDataCleared = true;
}
@@ -486,6 +543,7 @@ public class Process {
}
}
}
completeEntityInteractions(pendingEntityInteractions, pendingEntityIdentityConfirmations, promotedEntityIdentities, entitySpawnIdentities, false);
completeEntitySpawnLogs(pendingEntitySpawnLogs, false);
if (consumerDataCleared) {
@@ -547,6 +605,40 @@ public class Process {
}
}
private static void retryEntityInteraction(String user, EntityInteraction interaction) {
EntityInteraction retry = interaction.retry();
if (retry != null) {
Queue.queueEntityInteraction(user, retry);
}
else {
ErrorReporter.report(new IllegalStateException("Dropped entity interaction after repeated persistence failures: " + interaction.getEntityUuid()), ConfigHandler.EDITION_BRANCH.contains("-dev"));
}
}
private static void completeEntityInteractions(List<PendingEntityInteraction> interactions, Map<UUID, Location> identityConfirmations, Set<UUID> promotedIdentities, Map<UUID, EntitySpawnIdentity> identities, boolean committed) {
if (committed) {
for (Map.Entry<UUID, Location> entry : identityConfirmations.entrySet()) {
EntitySpawnTracking.confirmDatabaseIdentity(entry.getKey(), entry.getValue());
}
}
else {
for (PendingEntityInteraction pending : interactions) {
try {
retryEntityInteraction(pending.user, pending.interaction);
}
catch (Exception e) {
ErrorReporter.report(e);
}
}
for (UUID uuid : promotedIdentities) {
identities.remove(uuid);
}
}
interactions.clear();
identityConfirmations.clear();
promotedIdentities.clear();
}
private static void retryEntityContainerRollbacks(List<EntityContainerRollbackRetry> updates, boolean committed) {
if (!committed) {
for (EntityContainerRollbackRetry update : updates) {
@@ -626,8 +718,11 @@ public class Process {
}
private static void completeEntitySpawnLogs(Map<UUID, Location> pendingEntitySpawnLogs, boolean committed) {
if (!committed) {
for (Map.Entry<UUID, Location> entry : pendingEntitySpawnLogs.entrySet()) {
for (Map.Entry<UUID, Location> entry : pendingEntitySpawnLogs.entrySet()) {
if (committed) {
EntitySpawnTracking.confirmDatabaseIdentity(entry.getKey(), entry.getValue());
}
else {
try {
EntitySpawnTracking.reverifyDatabaseRow(entry.getKey(), entry.getValue());
}
@@ -698,4 +793,15 @@ public class Process {
this.inventoryRollback = inventoryRollback;
}
}
private static final class PendingEntityInteraction {
private final String user;
private final EntityInteraction interaction;
private PendingEntityInteraction(String user, EntityInteraction interaction) {
this.user = user;
this.interaction = interaction;
}
}
}
@@ -50,6 +50,7 @@ public class Database extends Queue {
public static final int ITEM = 13;
public static final int ENTITY_SPAWN = 14;
public static final int ENTITY_CONTAINER = 15;
public static final int ENTITY_INTERACTION = 16;
private static final int ROLLED_BACK_UPDATE_BATCH_SIZE = 1000;
@@ -73,6 +74,7 @@ public class Database extends Queue {
SQL_QUERIES.put(BLOCKDATA, "INSERT INTO %sprefix%blockdata_map (id, data) VALUES (?, ?)");
SQL_QUERIES.put(ENTITY_SPAWN, "INSERT INTO %sprefix%entity_spawn (time, uuid, wid, current_wid, origin_x, origin_y, origin_z, x, y, z, yaw, pitch, data, removed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
SQL_QUERIES.put(ENTITY_CONTAINER, "INSERT INTO %sprefix%entity_container (time, user, entity_spawn_rowid, wid, x, y, z, type, data, amount, metadata, action, rolled_back) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
SQL_QUERIES.put(ENTITY_INTERACTION, "INSERT INTO %sprefix%entity_interaction (time, user, entity_spawn_rowid, wid, x, y, z, type, action, metadata, rolled_back) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
}
public static void beginTransaction(Statement statement, boolean isMySQL) throws SQLException {
@@ -404,7 +406,7 @@ public class Database extends Queue {
}
}
private static final List<String> DATABASE_TABLES = Arrays.asList("art_map", "block", "chat", "command", "container", "entity_container", "item", "database_lock", "entity", "entity_spawn", "entity_map", "material_map", "blockdata_map", "session", "sign", "skull", "user", "username_log", "version", "world");
private static final List<String> DATABASE_TABLES = Arrays.asList("art_map", "block", "chat", "command", "container", "entity_container", "entity_interaction", "item", "database_lock", "entity", "entity_spawn", "entity_map", "material_map", "blockdata_map", "session", "sign", "skull", "user", "username_log", "version", "world");
public static void createDatabaseTables(String prefix, boolean forcePrefix, Connection forceConnection, boolean mySQL, boolean purge) {
ConfigHandler.databaseTables.clear();
@@ -467,6 +469,10 @@ public class Database extends Queue {
index = ", INDEX(wid,x,z,time), INDEX(entity_spawn_rowid,time), INDEX(user,time), INDEX(type,time)";
statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "entity_container(rowid int NOT NULL AUTO_INCREMENT,PRIMARY KEY(rowid), time int, user int, entity_spawn_rowid int NOT NULL, wid int, x int, y int, z int, type int, data int, amount int, metadata mediumblob, action tinyint, rolled_back tinyint" + index + ") ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4");
// Entity interaction
index = ", INDEX(wid,x,z,time), INDEX(entity_spawn_rowid,time), INDEX(user,time), INDEX(type,time), INDEX(action,time)";
statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "entity_interaction(rowid int NOT NULL AUTO_INCREMENT,PRIMARY KEY(rowid), time int, user int, entity_spawn_rowid int NOT NULL, wid int, x int, y int, z int, type int, action tinyint, metadata mediumblob, rolled_back tinyint" + index + ") ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4");
// Item
index = ", INDEX(wid,x,z,time), INDEX(user,time), INDEX(type,time)";
statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "item(rowid int NOT NULL AUTO_INCREMENT,PRIMARY KEY(rowid), time int, user int, wid int, x int, y int, z int, type int, data mediumblob, amount int, action tinyint, rolled_back tinyint" + index + ") ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4");
@@ -532,6 +538,11 @@ public class Database extends Queue {
ensureMySQLIndex(statement, prefix + "entity_container", "entity_spawn_rowid", "time");
ensureMySQLIndex(statement, prefix + "entity_container", "user", "time");
ensureMySQLIndex(statement, prefix + "entity_container", "type", "time");
ensureMySQLIndex(statement, prefix + "entity_interaction", "wid", "x", "z", "time");
ensureMySQLIndex(statement, prefix + "entity_interaction", "entity_spawn_rowid", "time");
ensureMySQLIndex(statement, prefix + "entity_interaction", "user", "time");
ensureMySQLIndex(statement, prefix + "entity_interaction", "type", "time");
ensureMySQLIndex(statement, prefix + "entity_interaction", "action", "time");
ensureMySQLIndex(statement, prefix + "item", "wid", "x", "z", "time");
ensureMySQLIndex(statement, prefix + "item", "user", "time");
ensureMySQLIndex(statement, prefix + "item", "type", "time");
@@ -688,6 +699,9 @@ public class Database extends Queue {
if (!tableData.contains(prefix + "entity_container")) {
statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "entity_container (time INTEGER, user INTEGER, entity_spawn_rowid INTEGER NOT NULL, wid INTEGER, x INTEGER, y INTEGER, z INTEGER, type INTEGER, data INTEGER, amount INTEGER, metadata BLOB, action INTEGER, rolled_back INTEGER);");
}
if (!tableData.contains(prefix + "entity_interaction")) {
statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "entity_interaction (time INTEGER, user INTEGER, entity_spawn_rowid INTEGER NOT NULL, wid INTEGER, x INTEGER, y INTEGER, z INTEGER, type INTEGER, action INTEGER, metadata BLOB, rolled_back INTEGER);");
}
if (!tableData.contains(prefix + "item")) {
statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "item (time INTEGER, user INTEGER, wid INTEGER, x INTEGER, y INTEGER, z INTEGER, type INTEGER, data BLOB, amount INTEGER, action INTEGER, rolled_back INTEGER);");
}
@@ -752,6 +766,11 @@ public class Database extends Queue {
createSQLiteIndex(statement, indexData, attachDatabase, "entity_container_spawn_index", prefix + "entity_container(entity_spawn_rowid,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "entity_container_user_index", prefix + "entity_container(user,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "entity_container_type_index", prefix + "entity_container(type,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "entity_interaction_index", prefix + "entity_interaction(wid,x,z,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "entity_interaction_spawn_index", prefix + "entity_interaction(entity_spawn_rowid,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "entity_interaction_user_index", prefix + "entity_interaction(user,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "entity_interaction_type_index", prefix + "entity_interaction(type,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "entity_interaction_action_index", prefix + "entity_interaction(action,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "item_index", prefix + "item(wid,x,z,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "item_user_index", prefix + "item(user,time)");
createSQLiteIndex(statement, indexData, attachDatabase, "item_type_index", prefix + "item(type,time)");
@@ -54,7 +54,9 @@ public class Lookup extends Queue {
while (results.next()) {
int resultTable = results.getInt("tbl");
long count = results.getLong("count");
rowData[resultTable] = count;
if (rowData != null && resultTable >= 0 && resultTable < rowData.length) {
rowData[resultTable] = count;
}
rows += count;
}
results.close();
@@ -30,11 +30,11 @@ import net.coreprotect.model.action.LookupActions;
import net.coreprotect.model.action.SignActions;
import net.coreprotect.model.item.InventorySources;
import net.coreprotect.model.item.ItemTransactionActions;
import net.coreprotect.utility.EntityUtils;
import net.coreprotect.utility.EntitySpawnTracking;
import net.coreprotect.utility.EntityUtils;
import net.coreprotect.utility.ErrorReporter;
import net.coreprotect.utility.MaterialUtils;
import net.coreprotect.utility.WorldUtils;
import net.coreprotect.utility.ErrorReporter;
public class LookupRaw extends Queue {
@@ -208,7 +208,7 @@ public class LookupRaw extends Queue {
int resultWorldId = results.getInt("wid");
boolean hasTbl = false;
if ((lookup && actionList.size() == 0) || actionList.contains(LookupActions.CONTAINER) || actionList.contains(5) || actionList.contains(LookupActions.ITEM)) {
if ((lookup && actionList.size() == 0) || actionList.contains(LookupActions.INTERACTION) || actionList.contains(LookupActions.CONTAINER) || actionList.contains(5) || actionList.contains(LookupActions.ITEM)) {
resultData = results.getInt("data");
resultAmount = results.getInt("amount");
resultMeta = results.getBytes("metadata");
@@ -283,10 +283,13 @@ public class LookupRaw extends Queue {
}
boolean inventoryQuery = LookupActions.isInventoryLookup(actionList);
boolean standardActionLookup = !actionList.contains(LookupActions.CONTAINER) && !actionList.contains(5) && !actionList.contains(LookupActions.ITEM) && !actionList.contains(LookupActions.CHAT) && !actionList.contains(LookupActions.COMMAND) && !actionList.contains(LookupActions.SESSION) && !actionList.contains(LookupActions.USERNAME) && !actionList.contains(LookupActions.SIGN);
boolean includeEntityInteractions = !summary && lookup && standardActionLookup && (actionList.isEmpty() || actionList.contains(LookupActions.INTERACTION));
boolean includeEntityContainers = entityContainerId != null || actionList.contains(LookupActions.CONTAINER) || inventoryQuery || (lookup && actionList.isEmpty());
boolean includeEntitySpawnLocations = entityActionFilter.includesAnySpawn(actionList, lookup || Config.getGlobal().ROLLBACK_ENTITIES);
boolean entitySpawnLocation = restrictWorld && location != null && location.getWorld() != null && includeEntitySpawnLocations;
boolean entityContainerLocation = restrictWorld && location != null && location.getWorld() != null && includeEntityContainers;
boolean entityInteractionLocation = restrictWorld && location != null && location.getWorld() != null && includeEntityInteractions;
boolean entitySpawnRadius = entitySpawnLocation && radius != null;
boolean entityContainerRadius = entityContainerLocation && radius != null;
boolean currentEntityRadius = !lookup && (entitySpawnRadius || entityContainerRadius);
@@ -309,6 +312,7 @@ public class LookupRaw extends Queue {
String query = "";
String entitySpawnLocationQuery = "";
String entityContainerLocationQuery = "";
String entityInteractionLocationQuery = "";
String standardLocationQuery = "";
List<String> messageFilterBindings = new ArrayList<>();
@@ -550,7 +554,7 @@ public class LookupRaw extends Queue {
}
}
if (entitySpawnLocation || entityContainerLocation) {
if (entitySpawnLocation || entityContainerLocation || entityInteractionLocation) {
int wid = WorldUtils.getWorldId(location.getWorld().getName());
String originalLocation = "(wid=" + wid + (bounds.isEmpty() ? "" : " AND " + bounds) + ")";
String entityBounds = "";
@@ -593,6 +597,12 @@ public class LookupRaw extends Queue {
entityContainerLocationQuery = lookup ? "(" + originalLocation + " OR " + currentLocation + ")" : currentLocation;
}
if (entityInteractionLocation) {
String entitySpawnRows = "SELECT rowid FROM " + ConfigHandler.prefix + "entity_spawn WHERE (" + databaseLocation + ")";
String currentLocation = "entity_spawn_rowid IN(" + entitySpawnRows + ")";
entityInteractionLocationQuery = "(" + originalLocation + " OR " + currentLocation + ")";
}
standardLocationQuery = originalLocation;
queryBlock = queryBlock + " " + (entitySpawnLocation ? entitySpawnLocationQuery : originalLocation) + " AND";
}
@@ -616,8 +626,10 @@ public class LookupRaw extends Queue {
queryBlock = queryBlock + " wid=" + worldId + " AND (x = '" + x + "' OR x = '" + x2 + "') AND (z = '" + z + "' OR z = '" + z2 + "') AND y = '" + location.getBlockY() + "' AND";
}
String actionPredicate = "";
if (validAction) {
queryBlock = queryBlock + " " + buildActionPredicate(action, actionList, entityActionFilter) + " AND";
actionPredicate = buildActionPredicate(action, actionList, entityActionFilter);
queryBlock = queryBlock + " " + actionPredicate + " AND";
}
else if (inventoryQuery || actionExclude.length() > 0 || includeBlock.length() > 0 || includeEntity.length() > 0 || excludeBlock.length() > 0 || excludeEntity.length() > 0) {
queryBlock = queryBlock + " action NOT IN(-1) AND";
@@ -682,8 +694,19 @@ public class LookupRaw extends Queue {
if (excludeBlock.length() > 0 || excludeEntity.length() > 0) {
queryEntity = queryEntity.replace("type NOT IN(" + (excludeBlock.length() > 0 ? excludeBlock : "0") + ")", "type NOT IN(" + (excludeEntity.length() > 0 ? excludeEntity : "0") + ")");
}
String queryEntityInteraction = queryEntity;
if (!entitySpawnLocationQuery.isEmpty()) {
queryEntityInteraction = queryEntityInteraction.replace(entitySpawnLocationQuery, standardLocationQuery);
}
if (!entityInteractionLocationQuery.isEmpty()) {
queryEntityInteraction = queryEntityInteraction.replace(standardLocationQuery, entityInteractionLocationQuery);
}
if (!actionPredicate.isEmpty()) {
queryEntityInteraction = queryEntityInteraction.replace(actionPredicate, "1");
}
String baseQuery = ((!includeEntity.isEmpty() || !excludeEntity.isEmpty()) ? queryEntity : queryBlock);
String blockSourceQuery = actionList.size() == 1 && actionList.contains(LookupActions.INTERACTION) ? queryBlock : baseQuery;
if (!summary && limitOffset > -1 && limitCount > -1) {
queryLimit = " LIMIT " + limitOffset + ", " + limitCount + "";
unionLimit = " ORDER BY time DESC, id DESC LIMIT " + (limitOffset + limitCount) + "";
@@ -726,7 +749,7 @@ public class LookupRaw extends Queue {
if (count) {
rows = "COUNT(*) as count";
queryLimit = " LIMIT 0, 4";
queryLimit = " LIMIT 0, 5";
queryOrder = "";
unionLimit = "";
}
@@ -856,6 +879,23 @@ public class LookupRaw extends Queue {
}
}
if (includeEntityInteractions) {
if (query.isEmpty()) {
if (!count) {
rows = "rowid as id,time,user,wid,x,y,z,type,meta as metadata,data,-1 as amount,action,rolled_back,0 as entity_spawn_rowid";
}
query = unionSelect + "SELECT '0' as tbl," + rows + " FROM " + ConfigHandler.prefix + "block " + index + "WHERE" + blockSourceQuery + unionLimit + ")";
}
if (!count) {
rows = "rowid as id,time,user,wid,x,y,z,type,metadata,action as data,-1 as amount," + LookupActions.INTERACTION + " as action,rolled_back,entity_spawn_rowid";
}
query += " UNION ALL " + unionSelect + "SELECT '" + InventorySources.ENTITY_INTERACTION + "' as tbl," + rows + " FROM " + ConfigHandler.prefix + "entity_interaction WHERE" + queryEntityInteraction + unionLimit + ")";
if (!count) {
queryOrder = " ORDER BY time DESC, tbl DESC, id DESC";
}
}
if (query.length() == 0) {
if (actionExclude.length() > 0) {
baseQuery = baseQuery.replace("action NOT IN(-1)", "action NOT IN(" + actionExclude + ")");
@@ -0,0 +1,59 @@
package net.coreprotect.database.logger;
import java.sql.PreparedStatement;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import net.coreprotect.CoreProtect;
import net.coreprotect.config.Config;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.database.statement.EntityInteractionStatement;
import net.coreprotect.database.statement.UserStatement;
import net.coreprotect.event.CoreProtectPreLogEvent;
import net.coreprotect.model.action.LookupActions;
import net.coreprotect.model.entity.EntityInteraction;
import net.coreprotect.model.entity.EntitySpawnIdentity;
public final class EntityInteractionLogger {
private EntityInteractionLogger() {
throw new IllegalStateException("Database class");
}
public static LogContext prepare(String user, EntityInteraction interaction) {
if (interaction == null || ConfigHandler.isBlacklisted(user) || ConfigHandler.isBlacklisted(user, interaction.getEntityType().getKey().toString())) {
return null;
}
Location currentLocation = interaction.getCurrentLocation();
CoreProtectPreLogEvent event = new CoreProtectPreLogEvent(user, currentLocation.clone(), CoreProtectPreLogEvent.Action.ENTITY_INTERACTION, LookupActions.INTERACTION, null, interaction.getEntityType(), interaction.getAction().name());
if (Config.getGlobal().API_ENABLED && !Bukkit.isPrimaryThread()) {
CoreProtect.getInstance().getServer().getPluginManager().callEvent(event);
}
return event.isCancelled() ? null : new LogContext(event);
}
public static void log(PreparedStatement interactionStatement, PreparedStatement checkpointStatement, EntitySpawnIdentity identity, EntityInteraction interaction, LogContext context) throws Exception {
Location currentLocation = interaction.getCurrentLocation();
EntityInteractionStatement.checkpoint(checkpointStatement, identity, currentLocation);
int worldId = identity.getOriginalWorldId();
int x = identity.getOriginalX();
int y = identity.getOriginalY();
int z = identity.getOriginalZ();
int userId = UserStatement.getId(interactionStatement, context.event.getUser(), true);
int time = (int) (System.currentTimeMillis() / 1000L);
EntityInteractionStatement.insert(interactionStatement, time, userId, identity, worldId, x, y, z, interaction);
}
public static final class LogContext {
private final CoreProtectPreLogEvent event;
private LogContext(CoreProtectPreLogEvent event) {
this.event = event;
}
}
}
@@ -0,0 +1,198 @@
package net.coreprotect.database.lookup;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.EntityType;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.database.statement.EntitySpawnStatement;
import net.coreprotect.database.statement.UserStatement;
import net.coreprotect.language.Phrase;
import net.coreprotect.language.Selector;
import net.coreprotect.listener.channel.PluginChannelListener;
import net.coreprotect.model.entity.EntityInteractionAction;
import net.coreprotect.model.entity.EntitySpawnRecord;
import net.coreprotect.utility.ChatUtils;
import net.coreprotect.utility.Color;
import net.coreprotect.utility.EntitySpawnTracking;
import net.coreprotect.utility.EntityUtils;
import net.coreprotect.utility.ErrorReporter;
import net.coreprotect.utility.WorldUtils;
public final class EntityInteractionLookup {
private EntityInteractionLookup() {
throw new IllegalStateException("Lookup class");
}
public static List<String> performLookup(String command, Statement statement, CommandSender commandSender, int page, int limit, Integer entitySpawnRowId) {
return performLookup(command, statement, commandSender, page, limit, entitySpawnRowId, null);
}
public static List<String> performLookup(String command, Statement statement, CommandSender commandSender, int page, int limit, Integer entitySpawnRowId, Location liveLocation) {
List<String> result = new ArrayList<>();
if (entitySpawnRowId == null) {
clearLookup(commandSender);
result.add(noData());
return result;
}
try {
String resolvedCommand = resolveCommand(command, commandSender);
int currentTime = (int) (System.currentTimeMillis() / 1000L);
int rowMax = page * limit;
int pageStart = rowMax - limit;
int count;
String where = "entity_spawn_rowid='" + entitySpawnRowId + "'";
try (ResultSet results = statement.executeQuery("SELECT COUNT(*) AS count FROM " + ConfigHandler.prefix + "entity_interaction WHERE " + where)) {
count = results.next() ? results.getInt("count") : 0;
}
EntitySpawnRecord record = EntitySpawnStatement.loadLocationRecords(statement.getConnection(), Collections.singleton(entitySpawnRowId)).get(entitySpawnRowId);
DisplayLocation displayLocation = resolveDisplayLocation(record, liveLocation);
boolean found = false;
String query = "SELECT time,user,wid,x,y,z,type,action,rolled_back FROM " + ConfigHandler.prefix + "entity_interaction WHERE " + where + " ORDER BY time DESC,rowid DESC LIMIT " + pageStart + ", " + limit;
try (ResultSet results = statement.executeQuery(query)) {
while (results.next()) {
int userId = results.getInt("user");
String resultUser = ConfigHandler.playerIdCacheReversed.get(userId);
if (resultUser == null) {
resultUser = UserStatement.loadName(statement.getConnection(), userId);
}
int originWorldId = results.getInt("wid");
int originX = results.getInt("x");
int originY = results.getInt("y");
int originZ = results.getInt("z");
int displayWorldId = displayLocation == null ? originWorldId : displayLocation.worldId;
int displayX = displayLocation == null ? originX : displayLocation.x;
int displayY = displayLocation == null ? originY : displayLocation.y;
int displayZ = displayLocation == null ? originZ : displayLocation.z;
if (!found) {
result.add(Color.WHITE + "----- " + Color.DARK_AQUA + Phrase.build(Phrase.INTERACTIONS_HEADER) + Color.WHITE + " ----- " + ChatUtils.getCoordinates(resolvedCommand, displayWorldId, displayX, displayY, displayZ, false, false));
}
found = true;
int actionId = results.getInt("action");
int typeId = results.getInt("type");
int rolledBack = results.getInt("rolled_back");
long resultTime = results.getLong("time");
String rollbackFormat = rolledBack == 1 || rolledBack == 3 ? Color.STRIKETHROUGH : "";
String target = entityName(typeId);
String coordinateInfo = displayWorldId == originWorldId && displayX == originX && displayY == originY && displayZ == originZ ? "" : ChatUtils.getCoordinateTooltip(originWorldId, originX, originY, originZ, Phrase.build(Phrase.LOOKUP_ENTITY_INTERACTION_ORIGIN), true);
String selector = actionSelector(actionId);
String timeAgo = ChatUtils.getTimeSince(resultTime, currentTime, true);
result.add(timeAgo + " " + Color.WHITE + "- " + Phrase.build(Phrase.LOOKUP_ENTITY_INTERACTION, Color.DARK_AQUA + rollbackFormat + resultUser + Color.WHITE + rollbackFormat, Color.DARK_AQUA + rollbackFormat + target + Color.WHITE + coordinateInfo, selector));
PluginChannelListener.getInstance().sendData(commandSender, resultTime, Phrase.LOOKUP_ENTITY_INTERACTION, selector, resultUser, target, -1, displayX, displayY, displayZ, displayWorldId, rollbackFormat, false, false);
}
}
if (found && count > limit) {
int totalPages = (int) Math.ceil(count / (limit + 0.0));
result.add(Color.WHITE + "-----");
result.add(ChatUtils.getPageNavigation(resolvedCommand, page, totalPages));
}
else if (!found) {
result.add(rowMax > count && count > 0 ? Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_RESULTS_PAGE, Selector.SECOND) : noData());
}
ConfigHandler.lookupEntityInteraction.put(commandSender.getName(), entitySpawnRowId);
ConfigHandler.lookupType.put(commandSender.getName(), 9);
ConfigHandler.lookupPage.put(commandSender.getName(), page);
ConfigHandler.lookupCommand.put(commandSender.getName(), displayLocation == null ? "0.0.0.0.9." + limit : displayLocation.x + "." + displayLocation.y + "." + displayLocation.z + "." + displayLocation.worldId + ".9." + limit);
}
catch (Exception e) {
ErrorReporter.report(e);
}
return result;
}
public static String actionSelector(int actionId) {
switch (EntityInteractionAction.fromId(actionId)) {
case SHEAR:
return Selector.SECOND;
case LEASH:
return Selector.THIRD;
case UNLEASH:
return Selector.FOURTH;
case GENERIC:
default:
return Selector.FIRST;
}
}
public static String entityName(int typeId) {
EntityType type = EntityUtils.getEntityType(typeId);
return type == null ? "unknown" : type.name().toLowerCase(Locale.ROOT);
}
private static DisplayLocation resolveDisplayLocation(EntitySpawnRecord record, Location liveLocation) throws Exception {
if (liveLocation != null && liveLocation.getWorld() != null) {
return new DisplayLocation(WorldUtils.getWorldId(liveLocation.getWorld().getName()), liveLocation.getBlockX(), liveLocation.getBlockY(), liveLocation.getBlockZ());
}
if (record == null) {
return null;
}
int worldId = record.getWorldId();
int x = (int) Math.floor(record.getX());
int y = (int) Math.floor(record.getY());
int z = (int) Math.floor(record.getZ());
Map<UUID, Location> loadedLocations = EntitySpawnTracking.findLoadedLocations(Collections.singleton(record.getUuid()));
Location loaded = loadedLocations.get(record.getUuid());
if (loaded != null && loaded.getWorld() != null) {
worldId = WorldUtils.getWorldId(loaded.getWorld().getName());
x = loaded.getBlockX();
y = loaded.getBlockY();
z = loaded.getBlockZ();
}
return new DisplayLocation(worldId, x, y, z);
}
private static String resolveCommand(String command, CommandSender commandSender) {
if (command != null) {
return command;
}
if (commandSender.hasPermission("coreprotect.co")) {
return "co";
}
if (commandSender.hasPermission("coreprotect.core")) {
return "core";
}
return commandSender.hasPermission("coreprotect.coreprotect") ? "coreprotect" : "co";
}
private static String noData() {
return Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_DATA_LOCATION, Selector.THIRD);
}
private static void clearLookup(CommandSender commandSender) {
ConfigHandler.lookupEntityInteraction.remove(commandSender.getName());
ConfigHandler.lookupType.remove(commandSender.getName());
ConfigHandler.lookupCommand.remove(commandSender.getName());
}
private static final class DisplayLocation {
private final int worldId;
private final int x;
private final int y;
private final int z;
private DisplayLocation(int worldId, int x, int y, int z) {
this.worldId = worldId;
this.x = x;
this.y = y;
this.z = z;
}
}
}
@@ -0,0 +1,61 @@
package net.coreprotect.database.statement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
import org.bukkit.Location;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.model.entity.EntityInteraction;
import net.coreprotect.model.entity.EntitySpawnIdentity;
import net.coreprotect.utility.EntityUtils;
import net.coreprotect.utility.WorldUtils;
public final class EntityInteractionStatement {
private EntityInteractionStatement() {
throw new IllegalStateException("Database class");
}
public static PreparedStatement prepareCheckpoint(Connection connection) throws SQLException {
return connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET current_wid=?,x=?,y=?,z=?,yaw=?,pitch=? WHERE rowid=? AND removed=0");
}
public static void checkpoint(PreparedStatement statement, EntitySpawnIdentity identity, Location location) throws SQLException {
statement.setInt(1, WorldUtils.getWorldId(location.getWorld().getName()));
statement.setDouble(2, location.getX());
statement.setDouble(3, location.getY());
statement.setDouble(4, location.getZ());
statement.setFloat(5, location.getYaw());
statement.setFloat(6, location.getPitch());
statement.setInt(7, identity.getRowId());
if (statement.executeUpdate() != 1) {
throw new SQLException("Entity interaction could not checkpoint its tracking row");
}
}
public static void insert(PreparedStatement statement, int time, int userId, EntitySpawnIdentity identity, int worldId, int x, int y, int z, EntityInteraction interaction) throws SQLException {
statement.setInt(1, time);
statement.setInt(2, userId);
statement.setInt(3, identity.getRowId());
statement.setInt(4, worldId);
statement.setInt(5, x);
statement.setInt(6, y);
statement.setInt(7, z);
statement.setInt(8, EntityUtils.getEntityId(interaction.getEntityType()));
statement.setInt(9, interaction.getAction().getId());
byte[] metadata = interaction.getMetadata();
if (metadata == null) {
statement.setNull(10, Types.BLOB);
}
else {
statement.setBytes(10, metadata);
}
statement.setInt(11, 0);
if (statement.executeUpdate() != 1) {
throw new SQLException("Entity interaction insert did not insert one row");
}
}
}
@@ -25,6 +25,7 @@ import net.coreprotect.database.Database;
import net.coreprotect.database.rollback.EntitySpawnRollbackHandler;
import net.coreprotect.model.action.LookupActions;
import net.coreprotect.model.entity.EntityContainerRollbackUpdate;
import net.coreprotect.model.entity.EntityInteractionOrigin;
import net.coreprotect.model.entity.EntitySpawnData;
import net.coreprotect.model.entity.EntitySpawnIdentity;
import net.coreprotect.model.entity.EntitySpawnRecord;
@@ -76,6 +77,45 @@ public final class EntitySpawnStatement {
}
}
public static EntitySpawnIdentity insertIdentity(PreparedStatement statement, int time, UUID uuid, EntityInteractionOrigin origin, Location currentLocation) throws Exception {
statement.setInt(1, time);
statement.setString(2, uuid.toString());
statement.setInt(3, origin.getWorldId());
statement.setInt(4, WorldUtils.getWorldId(currentLocation.getWorld().getName()));
statement.setDouble(5, origin.getX());
statement.setDouble(6, origin.getY());
statement.setDouble(7, origin.getZ());
statement.setDouble(8, currentLocation.getX());
statement.setDouble(9, currentLocation.getY());
statement.setDouble(10, currentLocation.getZ());
statement.setFloat(11, currentLocation.getYaw());
statement.setFloat(12, currentLocation.getPitch());
statement.setNull(13, Types.BLOB);
statement.setInt(14, 0);
int rowId;
if (Database.hasReturningKeys()) {
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
throw new SQLException("Entity identity insert did not return a row id");
}
rowId = resultSet.getInt(1);
}
}
else {
if (statement.executeUpdate() != 1) {
throw new SQLException("Entity identity insert did not insert one row");
}
try (ResultSet resultSet = statement.getGeneratedKeys()) {
if (!resultSet.next()) {
throw new SQLException("Entity identity insert did not generate a row id");
}
rowId = resultSet.getInt(1);
}
}
return new EntitySpawnIdentity(rowId, uuid, origin.getWorldId(), origin.getX(), origin.getY(), origin.getZ());
}
public static PreparedStatement prepareBlockLink(Connection connection) throws SQLException {
return connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET block_rowid=? WHERE rowid=? AND block_rowid IS NULL");
}
@@ -306,8 +346,8 @@ public final class EntitySpawnStatement {
location = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET current_wid=?,x=?,y=?,z=?,yaw=?,pitch=? WHERE uuid=? AND removed=0");
removed = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET current_wid=?,x=?,y=?,z=?,yaw=?,pitch=?,data=NULL,removed=1 WHERE uuid=? AND removed=0");
revived = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET uuid=?,current_wid=?,x=?,y=?,z=?,yaw=?,pitch=?,data=NULL,removed=0 WHERE uuid=? AND removed=1");
rollback = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET current_wid=?,x=?,y=?,z=?,yaw=?,pitch=?,data=?,removed=1 WHERE rowid=?");
restore = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET uuid=?,current_wid=?,x=?,y=?,z=?,yaw=?,pitch=?,data=NULL,removed=0 WHERE rowid=?");
rollback = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET current_wid=?,x=?,y=?,z=?,yaw=?,pitch=?,data=?,kill_rowid=NULL,removed=1 WHERE rowid=?");
restore = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET uuid=?,current_wid=?,x=?,y=?,z=?,yaw=?,pitch=?,data=NULL,kill_rowid=NULL,removed=0 WHERE rowid=?");
killRollback = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET uuid=?,current_wid=?,x=?,y=?,z=?,yaw=?,pitch=?,data=NULL,removed=0 WHERE rowid=? AND kill_rowid=?");
killRestore = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET current_wid=?,x=?,y=?,z=?,yaw=?,pitch=?,data=NULL,removed=1 WHERE rowid=? AND kill_rowid=?");
compositeRollback = connection.prepareStatement("UPDATE " + ConfigHandler.prefix + "entity_spawn SET current_wid=?,x=?,y=?,z=?,yaw=?,pitch=?,data=?,removed=1 WHERE rowid=? AND kill_rowid=?");
@@ -20,7 +20,8 @@ public class CoreProtectPreLogEvent extends Event implements Cancellable {
CONTAINER_TRANSACTION,
ITEM_TRANSACTION,
PLAYER_COMMAND,
ENTITY_SPAWN
ENTITY_SPAWN,
ENTITY_INTERACTION
}
private static final HandlerList handlers = new HandlerList();
@@ -154,6 +154,8 @@ public class Language {
phrases.put(Phrase.LOOKUP_CONTAINER, "{0} {added|removed} {1} {2}.");
phrases.put(Phrase.LOOKUP_HEADER, "{0} Lookup Results");
phrases.put(Phrase.LOOKUP_INTERACTION, "{0} {clicked|killed} {1}.");
phrases.put(Phrase.LOOKUP_ENTITY_INTERACTION, "{0} {clicked|sheared|leashed|unleashed} {1}.");
phrases.put(Phrase.LOOKUP_ENTITY_INTERACTION_ORIGIN, "Origin location");
phrases.put(Phrase.LOOKUP_ENTITY_ORIGIN, "Spawn location");
phrases.put(Phrase.LOOKUP_ENTITY_SPAWN, "{0} spawned {1}.");
phrases.put(Phrase.LOOKUP_ITEM, "{0} {picked up|dropped} {1} {2}.");
@@ -234,7 +234,9 @@ public enum Phrase {
VERSION_REQUIRED,
WORLD_NOT_FOUND,
LOOKUP_ENTITY_SPAWN,
LOOKUP_ENTITY_ORIGIN;
LOOKUP_ENTITY_ORIGIN,
LOOKUP_ENTITY_INTERACTION,
LOOKUP_ENTITY_INTERACTION_ORIGIN;
final private static Set<Phrase> HEADERS = new HashSet<>(Arrays.asList(Phrase.CONTAINER_HEADER, Phrase.HELP_HEADER, Phrase.INTERACTIONS_HEADER, Phrase.LOOKUP_HEADER, Phrase.SIGN_HEADER, Phrase.UPDATE_HEADER));
final private static Set<String> COLORS = new HashSet<>(Arrays.asList(Color.WHITE, Color.DARK_AQUA));
@@ -42,6 +42,7 @@ import net.coreprotect.listener.entity.TrackedEntityRemoveListener;
import net.coreprotect.listener.entity.TrackedEntityTeleportListener;
import net.coreprotect.listener.player.ArmorStandManipulateListener;
import net.coreprotect.listener.player.CraftItemListener;
import net.coreprotect.listener.player.EntityInteractionListener;
import net.coreprotect.listener.player.FoodLevelChangeListener;
import net.coreprotect.listener.player.InventoryChangeListener;
import net.coreprotect.listener.player.InventoryClickListener;
@@ -181,6 +182,7 @@ public final class ListenerHandler {
// Player Listeners
pluginManager.registerEvents(new ArmorStandManipulateListener(), plugin);
pluginManager.registerEvents(new CraftItemListener(), plugin);
pluginManager.registerEvents(new EntityInteractionListener(), plugin);
pluginManager.registerEvents(new FoodLevelChangeListener(), plugin);
pluginManager.registerEvents(new InventoryChangeListener(), plugin);
pluginManager.registerEvents(new InventoryClickListener(), plugin);
@@ -43,6 +43,9 @@ public final class CreatureSpawnListener extends Queue implements Listener {
Queue.queueEntitySpawnLog(user, event.getEntity().getUniqueId(), event.getEntityType(), event.getEntity().getLocation());
}
}
if (Config.getConfig(world).PLAYER_INTERACTIONS) {
EntitySpawnTracking.seedOrigin(event.getEntity());
}
if (!event.getEntityType().equals(EntityType.ARMOR_STAND)) {
return;
@@ -0,0 +1,209 @@
package net.coreprotect.listener.player;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerLeashEntityEvent;
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerShearEntityEvent;
import net.coreprotect.CoreProtect;
import net.coreprotect.config.Config;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.consumer.Queue;
import net.coreprotect.model.entity.EntityInteractionAction;
import net.coreprotect.thread.Scheduler;
public final class EntityInteractionListener extends Queue implements Listener {
private static final int MAX_PENDING_INTERACTIONS = 4096;
private static final long STALE_INTERACTION_NANOS = 1_000_000_000L;
private final Map<InteractionKey, PendingInteraction> pendingInteractions = new HashMap<>();
private long nextCleanup;
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (event instanceof PlayerArmorStandManipulateEvent) {
return;
}
Player player = event.getPlayer();
Entity entity = event.getRightClicked();
if (!shouldLog(player, entity)) {
return;
}
capture(player, entity, EntityInteractionAction.GENERIC, isLeashed(entity));
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerShearEntity(PlayerShearEntityEvent event) {
Player player = event.getPlayer();
Entity entity = event.getEntity();
if (shouldLog(player, entity)) {
capture(player, entity, EntityInteractionAction.SHEAR, false);
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerLeashEntity(PlayerLeashEntityEvent event) {
Player player = event.getPlayer();
Entity entity = event.getEntity();
if (shouldLog(player, entity)) {
capture(player, entity, EntityInteractionAction.LEASH, false);
}
}
private void capture(Player player, Entity entity, EntityInteractionAction action, boolean unleashCandidate) {
InteractionKey key = new InteractionKey(player.getUniqueId(), entity.getUniqueId());
PendingInteraction interaction;
boolean scheduleFlush = false;
long now = System.nanoTime();
synchronized (pendingInteractions) {
cleanup(now);
interaction = pendingInteractions.get(key);
if (interaction == null) {
if (pendingInteractions.size() >= MAX_PENDING_INTERACTIONS) {
return;
}
interaction = new PendingInteraction(player.getName(), entity, action, unleashCandidate, now);
pendingInteractions.put(key, interaction);
scheduleFlush = true;
}
else {
interaction.merge(action, unleashCandidate);
}
}
if (scheduleFlush) {
PendingInteraction scheduledInteraction = interaction;
try {
Scheduler.scheduleSyncDelayedTask(CoreProtect.getInstance(),
() -> flush(key, scheduledInteraction),
() -> discard(key, scheduledInteraction), entity, 1);
}
catch (RuntimeException e) {
discard(key, scheduledInteraction);
}
}
}
private void flush(InteractionKey key, PendingInteraction interaction) {
synchronized (pendingInteractions) {
if (!pendingInteractions.remove(key, interaction)) {
return;
}
}
EntityInteractionAction action = interaction.action;
if (action == EntityInteractionAction.GENERIC
&& interaction.unleashCandidate
&& interaction.entity.isValid()
&& !isLeashed(interaction.entity)) {
action = EntityInteractionAction.UNLEASH;
}
Queue.queueEntityInteraction(interaction.user, interaction.entity, action);
}
private void discard(InteractionKey key, PendingInteraction interaction) {
synchronized (pendingInteractions) {
pendingInteractions.remove(key, interaction);
}
}
private void cleanup(long now) {
if (now < nextCleanup) {
return;
}
nextCleanup = now + STALE_INTERACTION_NANOS;
Iterator<PendingInteraction> iterator = pendingInteractions.values().iterator();
while (iterator.hasNext()) {
if (now - iterator.next().createdAt >= STALE_INTERACTION_NANOS) {
iterator.remove();
}
}
}
private static boolean shouldLog(Player player, Entity entity) {
return !Boolean.TRUE.equals(ConfigHandler.inspecting.get(player.getName()))
&& Config.getConfig(player.getWorld()).PLAYER_INTERACTIONS
&& !(entity instanceof Player)
&& !(entity instanceof ArmorStand)
&& !(entity instanceof ItemFrame);
}
private static boolean isLeashed(Entity entity) {
if (!(entity instanceof LivingEntity)) {
return false;
}
return ((LivingEntity) entity).isLeashed();
}
private static final class InteractionKey {
private final UUID playerId;
private final UUID entityId;
private InteractionKey(UUID playerId, UUID entityId) {
this.playerId = playerId;
this.entityId = entityId;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof InteractionKey)) {
return false;
}
InteractionKey other = (InteractionKey) object;
return playerId.equals(other.playerId) && entityId.equals(other.entityId);
}
@Override
public int hashCode() {
return 31 * playerId.hashCode() + entityId.hashCode();
}
}
private static final class PendingInteraction {
private final String user;
private final Entity entity;
private final long createdAt;
private EntityInteractionAction action;
private boolean unleashCandidate;
private PendingInteraction(String user, Entity entity, EntityInteractionAction action, boolean unleashCandidate, long createdAt) {
this.user = user;
this.entity = entity;
this.action = action;
this.unleashCandidate = unleashCandidate;
this.createdAt = createdAt;
}
private void merge(EntityInteractionAction action, boolean unleashCandidate) {
if (action != EntityInteractionAction.GENERIC) {
this.action = action;
}
this.unleashCandidate |= unleashCandidate;
}
}
}
@@ -25,6 +25,7 @@ import net.coreprotect.consumer.Queue;
import net.coreprotect.database.logger.ItemLogger;
import net.coreprotect.language.Phrase;
import net.coreprotect.listener.player.inspector.ContainerInspector;
import net.coreprotect.listener.player.inspector.EntityInteractionInspector;
import net.coreprotect.model.BlockGroup;
import net.coreprotect.utility.Chat;
import net.coreprotect.utility.Color;
@@ -35,6 +36,7 @@ import net.coreprotect.utility.ItemUtils;
public final class PlayerInteractEntityListener extends Queue implements Listener {
private final ContainerInspector containerInspector = new ContainerInspector();
private final EntityInteractionInspector entityInteractionInspector = new EntityInteractionInspector();
@EventHandler(priority = EventPriority.MONITOR)
protected void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
@@ -44,7 +46,7 @@ public final class PlayerInteractEntityListener extends Queue implements Listene
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) {
if (Boolean.TRUE.equals(ConfigHandler.inspecting.get(player.getName())) && EntitySpawnTracking.isPlacedEntity(entity) && entity instanceof InventoryHolder && Config.getConfig(player.getWorld()).ITEM_TRANSACTIONS) {
String playerUuid = player.getUniqueId().toString();
long now = System.currentTimeMillis();
Object[] previousInspection = PlayerInteractListener.lastInspectorEvent.get(playerUuid);
@@ -60,11 +62,9 @@ public final class PlayerInteractEntityListener extends Queue implements Listene
event.setCancelled(true);
return;
}
if (Config.getConfig(player.getWorld()).ITEM_TRANSACTIONS) {
containerInspector.performEntityContainerLookup(player, entity.getUniqueId(), entity.getLocation());
event.setCancelled(true);
return;
}
containerInspector.performEntityContainerLookup(player, entity.getUniqueId(), entity.getLocation());
event.setCancelled(true);
return;
}
if (entity instanceof ItemFrame) {
@@ -115,6 +115,23 @@ public final class PlayerInteractEntityListener extends Queue implements Listene
queueContainerSpecifiedItems(player.getName(), Material.ITEM_FRAME, new Object[] { oldState, newState, frame.getFacing() }, frame.getLocation(), false);
}
}
else if (Boolean.TRUE.equals(ConfigHandler.inspecting.get(player.getName()))) {
String playerUuid = player.getUniqueId().toString();
long now = System.currentTimeMillis();
Object[] previousInspection = PlayerInteractListener.lastInspectorEvent.get(playerUuid);
if (previousInspection == null || now - (long) previousInspection[0] >= 100L) {
PlayerInteractListener.lastInspectorEvent.put(playerUuid, new Object[] { now, event.getHand() });
if (!player.hasPermission("coreprotect.inspect")) {
Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION));
ConfigHandler.inspecting.put(player.getName(), false);
}
else {
entityInteractionInspector.performLookup(player, entity.getUniqueId(), entity.getLocation());
}
}
event.setCancelled(true);
return;
}
else if (!event.isCancelled() && entity instanceof Creature && entity.getType().name().equals("ALLAY")) {
ItemStack handItem = new ItemStack(Material.AIR);
ItemStack mainHand = player.getInventory().getItemInMainHand();
@@ -0,0 +1,60 @@
package net.coreprotect.listener.player.inspector;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.List;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.database.lookup.EntityInteractionLookup;
import net.coreprotect.utility.Chat;
import net.coreprotect.utility.ErrorReporter;
public final class EntityInteractionInspector extends BaseInspector {
public void performLookup(Player player, UUID entityUuid, Location location) {
ConfigHandler.lookupEntityInteraction.remove(player.getName());
ConfigHandler.lookupEntityContainer.remove(player.getName());
ConfigHandler.lookupType.remove(player.getName());
Thread thread = new Thread(() -> {
try {
checkPreconditions(player);
try (Connection connection = getDatabaseConnection(player)) {
Integer entitySpawnRowId = findEntitySpawnRowId(connection, entityUuid);
try (Statement statement = connection.createStatement()) {
List<String> results = EntityInteractionLookup.performLookup(null, statement, player, 1, 7, entitySpawnRowId, location);
for (String result : results) {
Chat.sendComponent(player, result);
}
}
}
}
catch (InspectionException e) {
Chat.sendMessage(player, e.getMessage());
}
catch (Exception e) {
ErrorReporter.report(e);
}
finally {
finishInspection(player);
}
});
thread.start();
}
private Integer findEntitySpawnRowId(Connection connection, UUID entityUuid) throws Exception {
String query = "SELECT rowid AS id FROM " + ConfigHandler.prefix + "entity_spawn WHERE uuid=? LIMIT 1";
try (PreparedStatement statement = connection.prepareStatement(query)) {
statement.setString(1, entityUuid.toString());
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? resultSet.getInt("id") : null;
}
}
}
}
@@ -0,0 +1,71 @@
package net.coreprotect.model.entity;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
public final class EntityInteraction {
private static final int MAX_IDENTITY_RESOLUTION_ATTEMPTS = 3;
private final UUID entityUuid;
private final EntityType entityType;
private final EntityInteractionOrigin origin;
private final Location currentLocation;
private final EntityInteractionAction action;
private final byte[] metadata;
private final int identityResolutionAttempts;
public EntityInteraction(UUID entityUuid, EntityType entityType, EntityInteractionOrigin origin, Location currentLocation, EntityInteractionAction action, byte[] metadata) {
this(entityUuid, entityType, origin, currentLocation, action, metadata, 0);
}
private EntityInteraction(UUID entityUuid, EntityType entityType, EntityInteractionOrigin origin, Location currentLocation, EntityInteractionAction action, byte[] metadata, int identityResolutionAttempts) {
if (entityUuid == null || entityType == null || origin == null || currentLocation == null || currentLocation.getWorld() == null || action == null) {
throw new IllegalArgumentException("Invalid entity interaction");
}
this.entityUuid = entityUuid;
this.entityType = entityType;
this.origin = origin;
this.currentLocation = currentLocation.clone();
this.action = action;
this.metadata = metadata == null ? null : metadata.clone();
this.identityResolutionAttempts = identityResolutionAttempts;
}
public UUID getEntityUuid() {
return entityUuid;
}
public EntityType getEntityType() {
return entityType;
}
public EntityInteractionOrigin getOrigin() {
return origin;
}
public Location getCurrentLocation() {
return currentLocation.clone();
}
public EntityInteractionAction getAction() {
return action;
}
public byte[] getMetadata() {
return metadata == null ? null : metadata.clone();
}
public EntityInteraction retry() {
if (!canRetry()) {
return null;
}
return new EntityInteraction(entityUuid, entityType, origin, currentLocation, action, metadata, identityResolutionAttempts + 1);
}
public boolean canRetry() {
return identityResolutionAttempts < MAX_IDENTITY_RESOLUTION_ATTEMPTS;
}
}
@@ -0,0 +1,34 @@
package net.coreprotect.model.entity;
public enum EntityInteractionAction {
GENERIC(0, "clicked"),
SHEAR(1, "sheared"),
LEASH(2, "leashed"),
UNLEASH(3, "unleashed");
private final int id;
private final String displayVerb;
EntityInteractionAction(int id, String displayVerb) {
this.id = id;
this.displayVerb = displayVerb;
}
public static EntityInteractionAction fromId(int id) {
for (EntityInteractionAction action : values()) {
if (action.id == id) {
return action;
}
}
return GENERIC;
}
public int getId() {
return id;
}
public String getDisplayVerb() {
return displayVerb;
}
}
@@ -0,0 +1,35 @@
package net.coreprotect.model.entity;
public final class EntityInteractionOrigin {
private final int worldId;
private final double x;
private final double y;
private final double z;
public EntityInteractionOrigin(int worldId, double x, double y, double z) {
if (worldId < 0) {
throw new IllegalArgumentException("Invalid entity interaction origin");
}
this.worldId = worldId;
this.x = x;
this.y = y;
this.z = z;
}
public int getWorldId() {
return worldId;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getZ() {
return z;
}
}
@@ -5,6 +5,7 @@ public final class InventorySources {
public static final int CONTAINER = 1;
public static final int ITEM = 2;
public static final int ENTITY_CONTAINER = 3;
public static final int ENTITY_INTERACTION = 4;
private InventorySources() {
throw new IllegalStateException("Model class");
@@ -19,6 +20,8 @@ public final class InventorySources {
return "container";
case ITEM:
return "item";
case ENTITY_INTERACTION:
return "interaction";
default:
return "unknown";
}
@@ -11,15 +11,16 @@ public class __2_24_1 {
try {
Integer[] last_version = Patch.getDatabaseVersion(statement.getConnection(), true);
if (last_version[0] == 2 && last_version[1] == 24 && last_version[2] == 0) {
return __2_24_0.updateItemMetadataColumns(statement) && __2_24_0.updateSkullSkinColumn(statement);
if (!__2_24_0.updateItemMetadataColumns(statement) || !__2_24_0.updateSkullSkinColumn(statement)) {
return false;
}
}
return true;
}
catch (Exception e) {
ErrorReporter.report(e);
return false;
}
return true;
}
}
@@ -16,7 +16,8 @@ public class __2_25_0 {
protected static boolean patch(Statement statement) {
if (Config.getGlobal().MYSQL) {
if (!createMySQLPrefixIndex(statement, ConfigHandler.prefix + "chat", "message_prefix_index", "message(16)")
if (!createEntityInteractionTable(statement)
|| !createMySQLPrefixIndex(statement, ConfigHandler.prefix + "chat", "message_prefix_index", "message(16)")
|| !createMySQLPrefixIndex(statement, ConfigHandler.prefix + "command", "message_prefix_index", "message(16)")) {
return false;
}
@@ -34,6 +35,10 @@ public class __2_25_0 {
connection = statement.getConnection();
autoCommit = connection.getAutoCommit();
connection.setAutoCommit(false);
if (!createEntityInteractionTable(statement)) {
connection.rollback();
return false;
}
String[] indexQueries = new String[10];
indexQueries[0] = "CREATE INDEX IF NOT EXISTS chat_message_prefix_index ON " + ConfigHandler.prefix + "chat(substr(message,1,16) COLLATE NOCASE)";
indexQueries[1] = "CREATE INDEX IF NOT EXISTS command_message_prefix_index ON " + ConfigHandler.prefix + "command(substr(message,1,16) COLLATE NOCASE)";
@@ -60,6 +65,29 @@ public class __2_25_0 {
}
}
private static boolean createEntityInteractionTable(Statement statement) {
String table = ConfigHandler.prefix + "entity_interaction";
try {
if (Config.getGlobal().MYSQL) {
String indexes = ", INDEX(wid,x,z,time), INDEX(entity_spawn_rowid,time), INDEX(user,time), INDEX(type,time), INDEX(action,time)";
statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + table + "(rowid int NOT NULL AUTO_INCREMENT,PRIMARY KEY(rowid), time int, user int, entity_spawn_rowid int NOT NULL, wid int, x int, y int, z int, type int, action tinyint, metadata mediumblob, rolled_back tinyint" + indexes + ") ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4");
}
else {
statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + table + " (time INTEGER, user INTEGER, entity_spawn_rowid INTEGER NOT NULL, wid INTEGER, x INTEGER, y INTEGER, z INTEGER, type INTEGER, action INTEGER, metadata BLOB, rolled_back INTEGER)");
statement.executeUpdate("CREATE INDEX IF NOT EXISTS entity_interaction_index ON " + table + "(wid,x,z,time)");
statement.executeUpdate("CREATE INDEX IF NOT EXISTS entity_interaction_spawn_index ON " + table + "(entity_spawn_rowid,time)");
statement.executeUpdate("CREATE INDEX IF NOT EXISTS entity_interaction_user_index ON " + table + "(user,time)");
statement.executeUpdate("CREATE INDEX IF NOT EXISTS entity_interaction_type_index ON " + table + "(type,time)");
statement.executeUpdate("CREATE INDEX IF NOT EXISTS entity_interaction_action_index ON " + table + "(action,time)");
}
return true;
}
catch (Exception e) {
ErrorReporter.report(e);
return false;
}
}
private static boolean createMySQLPrefixIndex(Statement statement, String table, String index, String column) {
try {
statement.executeUpdate("CREATE INDEX " + index + " ON " + table + "(" + column + ")");
@@ -16,6 +16,10 @@ public class Scheduler {
}
public static void scheduleSyncDelayedTask(CoreProtect plugin, Runnable task, Object regionData, int delay) {
scheduleSyncDelayedTask(plugin, task, null, regionData, delay);
}
public static void scheduleSyncDelayedTask(CoreProtect plugin, Runnable task, Runnable retiredTask, Object regionData, int delay) {
if (ConfigHandler.isFolia) {
if (regionData instanceof Location) {
Location location = (Location) regionData;
@@ -29,10 +33,10 @@ public class Scheduler {
else if (regionData instanceof Entity) {
Entity entity = (Entity) regionData;
if (delay == 0) {
entity.getScheduler().run(plugin, value -> task.run(), null);
entity.getScheduler().run(plugin, value -> task.run(), retiredTask);
}
else {
entity.getScheduler().runDelayed(plugin, value -> task.run(), null, delay);
entity.getScheduler().runDelayed(plugin, value -> task.run(), retiredTask, delay);
}
}
else {
@@ -1,5 +1,6 @@
package net.coreprotect.utility;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
@@ -19,11 +20,13 @@ import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.TreeSpecies;
import org.bukkit.World;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Boat;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Minecart;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
@@ -33,6 +36,7 @@ import org.bukkit.persistence.PersistentDataType;
import net.coreprotect.CoreProtect;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.consumer.Queue;
import net.coreprotect.model.entity.EntityInteractionOrigin;
import net.coreprotect.model.entity.EntitySpawnData;
import net.coreprotect.paper.PaperAdapter;
import net.coreprotect.thread.Scheduler;
@@ -40,6 +44,8 @@ import net.coreprotect.thread.Scheduler;
public final class EntitySpawnTracking {
private static final String TRACKING_KEY = "spawn";
private static final String ORIGIN_SEED_KEY = "entity_origin";
private static final int ORIGIN_SEED_SIZE = Integer.BYTES + Double.BYTES * 3;
private static final int KILL_LOCATION_INDEX = 8;
private static final long PENDING_CLEAR_TTL_MILLIS = 300_000L;
private static final long UNLOADED_CACHE_RETENTION_MILLIS = 300_000L;
@@ -49,10 +55,12 @@ public final class EntitySpawnTracking {
private static final long ENTITY_SCAN_TIMEOUT_SECONDS = 30L;
private static final Map<UUID, TrackedLocation> trackedLocations = new ConcurrentHashMap<>();
private static final Map<UUID, PendingClear> pendingClear = new ConcurrentHashMap<>();
private static final Map<UUID, PendingIdentityConfirmation> pendingIdentityConfirmations = new ConcurrentHashMap<>();
private static final Set<UUID> coreProtectRemovals = ConcurrentHashMap.newKeySet();
private static final AtomicLong verificationEpoch = new AtomicLong();
private static final AtomicLong nextCleanup = new AtomicLong();
private static volatile NamespacedKey trackingKey;
private static volatile NamespacedKey originSeedKey;
private EntitySpawnTracking() {
throw new IllegalStateException("Utility class");
@@ -99,10 +107,98 @@ public final class EntitySpawnTracking {
UUID uuid = entity.getUniqueId();
Location location = entity.getLocation();
pendingClear.remove(uuid);
pendingIdentityConfirmations.remove(uuid);
entity.getPersistentDataContainer().set(getKey(), PersistentDataType.BYTE, (byte) 1);
entity.getPersistentDataContainer().remove(getOriginSeedKey());
trackedLocations.compute(uuid, (key, previous) -> TrackedLocation.from(location, true, verificationEpoch.get()));
}
public static void seedOrigin(Entity entity) {
if (!isEligibleInteractionEntity(entity) || isTracked(entity)) {
return;
}
byte[] existingSeed = entity.getPersistentDataContainer().get(getOriginSeedKey(), PersistentDataType.BYTE_ARRAY);
if (existingSeed != null && existingSeed.length == ORIGIN_SEED_SIZE) {
return;
}
Location location = entity.getLocation();
if (location.getWorld() == null) {
return;
}
int worldId = WorldUtils.getWorldId(location.getWorld().getName());
if (worldId < 0) {
return;
}
ByteBuffer buffer = ByteBuffer.allocate(ORIGIN_SEED_SIZE);
buffer.putInt(worldId);
buffer.putDouble(location.getX());
buffer.putDouble(location.getY());
buffer.putDouble(location.getZ());
entity.getPersistentDataContainer().set(getOriginSeedKey(), PersistentDataType.BYTE_ARRAY, buffer.array());
}
public static EntityInteractionOrigin getOrCreateInteractionOrigin(Entity entity) {
if (entity == null) {
return null;
}
seedOrigin(entity);
byte[] data = entity.getPersistentDataContainer().get(getOriginSeedKey(), PersistentDataType.BYTE_ARRAY);
if (data == null || data.length != ORIGIN_SEED_SIZE) {
Location location = entity.getLocation();
if (location.getWorld() == null) {
return null;
}
int worldId = WorldUtils.getWorldId(location.getWorld().getName());
return worldId < 0 ? null : new EntityInteractionOrigin(worldId, location.getX(), location.getY(), location.getZ());
}
ByteBuffer buffer = ByteBuffer.wrap(data);
int worldId = buffer.getInt();
double x = buffer.getDouble();
double y = buffer.getDouble();
double z = buffer.getDouble();
return new EntityInteractionOrigin(worldId, x, y, z);
}
public static boolean isEligibleInteractionEntity(Entity entity) {
return entity instanceof LivingEntity && !(entity instanceof Player) && !(entity instanceof ArmorStand);
}
public static void confirmDatabaseIdentity(UUID uuid, Location location) {
if (uuid == null || location == null || location.getWorld() == null) {
return;
}
scheduleCleanup();
PendingIdentityConfirmation confirmation = new PendingIdentityConfirmation(location, System.currentTimeMillis() + PENDING_CLEAR_TTL_MILLIS);
pendingIdentityConfirmations.put(uuid, confirmation);
CoreProtect plugin = CoreProtect.getInstance();
if (plugin == null || !plugin.isEnabled()) {
return;
}
try {
Scheduler.runTask(plugin, () -> {
Entity entity = Bukkit.getEntity(uuid);
if (entity == null) {
return;
}
Runnable confirm = () -> applyIdentityConfirmation(entity, confirmation);
if (ConfigHandler.isFolia && !PaperAdapter.ADAPTER.isOwnedByCurrentRegion(entity)) {
PaperAdapter.ADAPTER.executeEntityTask(plugin, entity, confirm, () -> {
});
}
else {
confirm.run();
}
}, location);
}
catch (Exception e) {
ErrorReporter.report(e);
}
}
public static void trackRevivedEntity(Entity entity, UUID previousUuid) {
track(entity);
Queue.queueEntitySpawnRevived(previousUuid, entity.getUniqueId(), entity.getLocation());
@@ -112,6 +208,10 @@ public final class EntitySpawnTracking {
if (clearPendingTracking(entity)) {
return;
}
PendingIdentityConfirmation confirmation = pendingIdentityConfirmations.get(entity.getUniqueId());
if (confirmation != null && !confirmation.isExpired(System.currentTimeMillis())) {
applyIdentityConfirmation(entity, confirmation);
}
if (isTracked(entity)) {
observe(entity, true);
}
@@ -485,6 +585,7 @@ public final class EntitySpawnTracking {
public static void clearTracking(UUID uuid) {
scheduleCleanup();
pendingIdentityConfirmations.remove(uuid);
PendingClear request = new PendingClear(System.currentTimeMillis() + PENDING_CLEAR_TTL_MILLIS);
pendingClear.put(uuid, request);
Location location = getCachedLocation(uuid);
@@ -855,6 +956,40 @@ public final class EntitySpawnTracking {
return key;
}
private static NamespacedKey getOriginSeedKey() {
NamespacedKey key = originSeedKey;
if (key == null) {
synchronized (EntitySpawnTracking.class) {
key = originSeedKey;
if (key == null) {
key = new NamespacedKey(CoreProtect.getInstance(), ORIGIN_SEED_KEY);
originSeedKey = key;
}
}
}
return key;
}
private static void applyIdentityConfirmation(Entity entity, PendingIdentityConfirmation confirmation) {
UUID uuid = entity.getUniqueId();
if (!pendingIdentityConfirmations.remove(uuid, confirmation)) {
return;
}
if (pendingClear.containsKey(uuid)) {
return;
}
entity.getPersistentDataContainer().set(getKey(), PersistentDataType.BYTE, (byte) 1);
entity.getPersistentDataContainer().remove(getOriginSeedKey());
Location location = entity.getLocation();
long epoch = verificationEpoch.get();
boolean locationConfirmed = TrackedLocation.from(confirmation.location, true, epoch).matches(location);
trackedLocations.put(uuid, TrackedLocation.from(location, true, locationConfirmed ? epoch : -1L));
if (!locationConfirmed) {
Queue.queueEntitySpawnLocation(uuid, location, epoch);
}
}
private static boolean clearPendingTracking(Entity entity) {
UUID uuid = entity.getUniqueId();
PendingClear request = pendingClear.get(uuid);
@@ -869,6 +1004,7 @@ public final class EntitySpawnTracking {
return false;
}
pendingIdentityConfirmations.remove(uuid);
entity.getPersistentDataContainer().remove(getKey());
forget(uuid);
return true;
@@ -1000,6 +1136,26 @@ public final class EntitySpawnTracking {
trackedLocations.remove(entry.getKey(), entry.getValue());
}
}
for (Map.Entry<UUID, PendingIdentityConfirmation> entry : pendingIdentityConfirmations.entrySet()) {
if (entry.getValue().isExpired(now)) {
pendingIdentityConfirmations.remove(entry.getKey(), entry.getValue());
}
}
}
private static final class PendingIdentityConfirmation {
private final Location location;
private final long expiry;
private PendingIdentityConfirmation(Location location, long expiry) {
this.location = location.clone();
this.expiry = expiry;
}
private boolean isExpired(long now) {
return expiry < now;
}
}
private static final class TrackedLocation {