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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/api/version/v13.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ API version 13 adds entity-spawn lookup support while retaining all API version

## Upgrading from API v12

- `LookupOptions` supports material inclusion/exclusion filters for typed container, item, and inventory lookups, and `users(List<String>)` / `excludeUsers(List<String>)` filters for all typed lookups.
- `LookupOptions` supports material inclusion/exclusion filters for typed block, container, item, and inventory lookups, and `users(List<String>)` / `excludeUsers(List<String>)` filters for all typed lookups.
- Added `blockLookup(LookupOptions)` for block breaks, placements, and interactions at a location, within a radius, in a world, or across all worlds. Entity events are excluded; use `entityLookup` instead. `blockLookup(Block, LookupOptions)` continues to use the supplied block's coordinates and retains entity events for compatibility.
- `LookupOptions.world(World)` selects a whole world for spatial typed lookups. The last call to `world`, `location`, or `radius` determines the search area. Username history has no world, so this option is ignored by `usernameLookup`.
- Added `entityLookup(LookupOptions)` returning `EntityResult` for entity spawns and kills. Use `entityActions(List<EntityAction>)` to select `SPAWN` or `KILL`, and `includeEntities(List<EntityType>)` / `excludeEntities(List<EntityType>)` to filter entity types. Empty filters return both actions and all entity types. These filters only affect `entityLookup`.
- `LookupOptions` supports `blockActions(List<BlockAction>)`, `containerActions(List<ContainerAction>)`, `itemActions(List<ItemAction>)`, `inventoryActions(List<InventoryAction>)`, and `sessionActions(List<SessionAction>)` to select events returned by `blockLookup`, `containerLookup`, `itemLookup`, `inventoryLookup`, and `sessionLookup`, respectively. Each filter only affects its corresponding lookup. Select multiple actions to match any of them, or omit the filter or pass an empty list to keep the default results.
- Added `CoreProtectAction.ENTITY_SPAWN` with action ID `13`.
- Added `CoreProtectPreLogEvent.Action.ENTITY_SPAWN`.
Expand All @@ -23,12 +26,16 @@ Entity-spawn records are available through normal lookup, rollback, and restore

Radius lookups match entity spawns at either their original spawn location or their tracked current/final location. Whole-world lookups likewise match either the original or tracked current/final world, while rollback and restore selection uses the tracked current/final world.

`entityLookup` supports the existing user, time, location, radius, world, and limit options. Spawn events match either their original or persisted current/final location; kill events match their logged location. Results always return the original event coordinates and world. Material filters and other lookup-specific action filters do not affect entity lookups. No blocking live-entity scans are performed.

Command filters present boats and minecarts as block changes: `a:+block` includes placements, `a:-block` includes destruction, and `a:block` includes both. The corresponding `a:spawn` and `a:kill` filters exclude those aliased vehicle records. This is a command and display alias only; API results and action lists continue to identify the rows as `CoreProtectAction.ENTITY_SPAWN` (action ID `13`) and `CoreProtectAction.ENTITY_KILL` (action ID `3`).

Entity-container location filters match either the transaction's immutable original location or the entity's persisted current/final location, and typed results expose the persisted current/final location. The persisted position is checkpointed when a transaction is logged and during relevant entity lifecycle changes; the synchronous typed APIs do not schedule blocking live-entity scans.

Use `blockActions(List.of(BlockAction.BREAK))` to find broken blocks, or select `BlockAction.PLACE` or `BlockAction.INTERACTION` for placements or interactions. Use `sessionActions(List.of(SessionAction.LOGIN))` for player logins or `SessionAction.LOGOUT` for logouts.

Block material filters match the logged block material, not the block currently at that location. Including materials returns only matching block events; excluding materials leaves entity events unchanged in the existing `blockLookup(Block, LookupOptions)` overload.

To find items deposited into containers, use `containerActions(List.of(ContainerAction.ADD))`. For inventory lookups, select `InventoryAction.CONTAINER_ADD` for deposits or `InventoryAction.BLOCK_PLACE` for block placement. Container filters also cover tracked boat and minecart transactions.

For example, `itemActions(List.of(ItemAction.DROP, ItemAction.PICKUP))` selects dropped and picked-up items. To query `ItemAction.BREAK`, `DESTROY`, `CREATE`, `SELL`, or `BUY`, select them explicitly with `itemActions`; these events are not included by default.
Expand Down
32 changes: 31 additions & 1 deletion src/main/java/net/coreprotect/CoreProtectAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.bukkit.entity.Player;

import net.coreprotect.api.BlockAPI;
import net.coreprotect.api.EntityAPI;
import net.coreprotect.api.InventoryAPI;
import net.coreprotect.api.ItemAPI;
import net.coreprotect.api.LookupOptions;
Expand All @@ -29,6 +30,7 @@
import net.coreprotect.api.UsernameAPI;
import net.coreprotect.api.result.BlockResult;
import net.coreprotect.api.result.ContainerResult;
import net.coreprotect.api.result.EntityResult;
import net.coreprotect.api.result.InventoryResult;
import net.coreprotect.api.result.ItemResult;
import net.coreprotect.api.result.MessageResult;
Expand Down Expand Up @@ -136,7 +138,7 @@ public List<String[]> blockLookup(Block block, int time) {
* @param block
* The block to look up
* @param options
* Lookup options. User, time, and limit are applied; location and radius are ignored because the block supplies the exact location.
* Lookup options. World, location, and radius are ignored because the block supplies the exact location.
* @return List of results or null if API is disabled
*/
public List<BlockResult> blockLookup(Block block, LookupOptions options) {
Expand All @@ -146,6 +148,34 @@ public List<BlockResult> blockLookup(Block block, LookupOptions options) {
return null;
}

/**
* Performs a typed lookup of block breaks, placements, and interactions using shared lookup options. Entity events are excluded.
*
* @param options
* Lookup options
* @return List of results or null if API is disabled
*/
public List<BlockResult> blockLookup(LookupOptions options) {
if (isEnabled()) {
return BlockAPI.performLookup(options);
}
return null;
}

/**
* Performs a typed lookup of entity spawn and kill events.
*
* @param options
* Lookup options
* @return List of results in an EntityResult format
*/
public List<EntityResult> entityLookup(LookupOptions options) {
if (isEnabled()) {
return EntityAPI.performLookup(options);
}
return null;
}

/**
* Performs a lookup on the queue data for the specified block.
*
Expand Down
82 changes: 41 additions & 41 deletions src/main/java/net/coreprotect/api/BlockAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,40 @@ public static List<String[]> performLookup(Block block, int offset) {
* @param block
* The block to look up
* @param options
* Lookup options. User, time, and limit are applied; location and radius are ignored because the block supplies the exact location.
* Lookup options. World, location, and radius are ignored because the block supplies the exact location.
* @return List of results in a BlockResult format
*/
public static List<BlockResult> performLookup(Block block, LookupOptions options) {
List<BlockResult> result = new ArrayList<>();
if (!Config.getGlobal().API_ENABLED || block == null || block.getWorld() == null) {
return new ArrayList<>();
}

if (!Config.getGlobal().API_ENABLED) {
return result;
if (options == null) {
options = LookupOptions.builder().build();
}

if (block == null || block.getWorld() == null) {
return performLookup(LookupOptions.builder().location(block.getLocation())
.user(options.getUser()).users(options.getUsers()).excludeUsers(options.getExcludeUsers())
.time(options.getTime()).limit(options.getLimitOffset(), options.getLimitCount())
.includeMaterials(options.getIncludeMaterials()).excludeMaterials(options.getExcludeMaterials())
.blockActions(options.getBlockActions()).build(), false);
}

/**
* Performs a typed lookup of block-related actions using shared lookup options.
*
* @param options
* Lookup options
* @return List of results in a BlockResult format
*/
public static List<BlockResult> performLookup(LookupOptions options) {
return performLookup(options, true);
}

private static List<BlockResult> performLookup(LookupOptions options, boolean blocksOnly) {
List<BlockResult> result = new ArrayList<>();

if (!Config.getGlobal().API_ENABLED) {
return result;
}

Expand All @@ -134,54 +157,31 @@ public static List<BlockResult> performLookup(Block block, LookupOptions options
return result;
}

Integer userId = MessageAPI.getUserId(connection, options.getUser());
if (userId != null && userId == -1) {
LookupFilter filter = LookupFilter.fromOptions(connection, options);
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
return result;
}

int checkTime = 0;
if (options.getTime() > 0) {
checkTime = (int) (System.currentTimeMillis() / 1000L) - options.getTime();
}

int x = block.getX();
int y = block.getY();
int z = block.getZ();
String worldName = block.getWorld().getName();
int worldId = WorldUtils.getWorldId(worldName);

StringBuilder query = new StringBuilder("SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",action,type,data,blockdata,rolled_back,wid,x,y,z FROM ");
query.append(DuckDBLookupQuery.spatialTable(connection, "block", worldId, x, x, z, z, "spatial_rows")).append(' ');
if (!ConfigHandler.databaseType.isDuckDB()) {
query.append(filter.table(connection, "block", "")).append(' ');
if (filter.hasLocation() && !ConfigHandler.databaseType.isDuckDB()) {
query.append(WorldUtils.getWidIndex("block"));
}
query.append("WHERE ").append(LocationQuery.predicate("wid", " = ?"))
.append(" AND ").append(LocationQuery.predicate("x", " = ?"))
.append(" AND ").append(LocationQuery.predicate("z", " = ?"))
.append(" AND y = ? AND time > ?");
if (userId != null) {
query.append(" AND ").append(ConfigHandler.databaseType.getUserColumn()).append(" = ?");
}
LookupFilter.appendUserWhere(query, "", LookupFilter.userIds(connection, options.getUsers()), LookupFilter.userIds(connection, options.getExcludeUsers()));
LookupFilter.appendActionWhere(query, "", options.getBlockActions().stream().mapToInt(BlockAction::id).toArray());
filter.appendWhere(query);
Comment thread
strobil marked this conversation as resolved.
filter.appendBlockMaterialWhere(query);
int[] actions = blocksOnly && options.getBlockActions().isEmpty()
? new int[] { BlockAction.BREAK.id(), BlockAction.PLACE.id(), BlockAction.INTERACTION.id() }
: options.getBlockActions().stream().mapToInt(BlockAction::id).toArray();
LookupFilter.appendActionWhere(query, "", actions);
query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder());
if (options.hasLimit()) {
query.append(" LIMIT ").append(options.getLimitCount()).append(" OFFSET ").append(options.getLimitOffset());
}
filter.appendLimit(query);

try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
statement.setInt(1, worldId);
statement.setInt(2, x);
statement.setInt(3, z);
statement.setInt(4, y);
statement.setInt(5, checkTime);
if (userId != null) {
statement.setInt(6, userId);
}
filter.bind(statement);

try (ResultSet results = statement.executeQuery()) {
while (results.next()) {
result.add(parseBlockResult(connection, results, worldName));
result.add(parseBlockResult(connection, results, WorldUtils.getWorldName(results.getInt("wid"))));
}
}
}
Expand Down
110 changes: 110 additions & 0 deletions src/main/java/net/coreprotect/api/EntityAPI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package net.coreprotect.api;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.StringJoiner;

import org.bukkit.entity.EntityType;

import net.coreprotect.api.result.EntityResult;
import net.coreprotect.config.Config;
import net.coreprotect.config.ConfigHandler;
import net.coreprotect.database.Database;
import net.coreprotect.database.statement.UserStatement;
import net.coreprotect.model.action.LookupActions;
import net.coreprotect.utility.ErrorReporter;
import net.coreprotect.utility.WorldUtils;

/**
* Provides API methods for looking up entity spawn and kill events.
*/
public class EntityAPI {

private EntityAPI() {
throw new IllegalStateException("API class");
}

/**
* Performs a typed entity lookup, matching original or persisted current/final spawn locations.
*
* @param options
* Lookup options
* @return List of entity results at their original event locations
*/
public static List<EntityResult> performLookup(LookupOptions options) {
List<EntityResult> result = new ArrayList<>();
if (!Config.getGlobal().API_ENABLED) {
return result;
}
if (options == null) {
options = LookupOptions.builder().build();
}

try (Connection connection = Database.getConnection(false, 1000)) {
if (connection == null) {
return result;
}
LookupFilter filter = LookupFilter.fromOptions(connection, options);
if (filter.hasInvalidUser() || filter.hasInvalidLocation()) {
return result;
}

boolean snapshot = filter.beginDuckDBSnapshot(connection);
try {
StringBuilder query = new StringBuilder("SELECT entity_rows.time,entity_rows." + ConfigHandler.databaseType.getUserColumn() + ",entity_rows.wid,entity_rows.x,entity_rows.y,entity_rows.z,entity_rows.type,entity_rows.action,entity_rows.rolled_back FROM ");
query.append(filter.entityTable(connection, "entity_rows"));
query.append(' ');
filter.appendEntityWhere(connection, query, "entity_rows");
int[] actions = options.getEntityActions().isEmpty()
? new int[] { LookupActions.ENTITY_KILL, LookupActions.ENTITY_SPAWN }
: options.getEntityActions().stream().mapToInt(EntityAction::id).toArray();
LookupFilter.appendActionWhere(query, "entity_rows", actions);
if (!options.getIncludeEntities().isEmpty()) {
query.append(" AND ").append(entityPredicate(options.getIncludeEntities()));
}
if (!options.getExcludeEntities().isEmpty()) {
query.append(" AND NOT ").append(entityPredicate(options.getExcludeEntities()));
}
query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder().replace("time", "entity_rows.time").replace("rowid", "entity_rows.rowid"));
filter.appendLimit(query);

try (PreparedStatement statement = connection.prepareStatement(query.toString())) {
filter.bindTrackedEntity(statement, 1);
try (ResultSet results = statement.executeQuery()) {
while (results.next()) {
result.add(new EntityResult(
results.getLong("time"), UserStatement.getName(connection, results.getInt("user")), WorldUtils.getWorldName(results.getInt("wid")),
results.getInt("x"), results.getInt("y"), results.getInt("z"), results.getInt("type"), results.getInt("action"), results.getInt("rolled_back")
));
}
}
}
}
finally {
filter.endDuckDBSnapshot(connection, snapshot);
}
}
catch (Exception e) {
ErrorReporter.report(e);
}
return result;
}

private static String entityPredicate(List<EntityType> entities) {
StringJoiner names = new StringJoiner(",");
for (EntityType entity : entities) {
String name = entity.name().toLowerCase(Locale.ROOT);
names.add("'" + name + "'");
names.add("'minecraft:" + name + "'");
}
String predicate = "entity_rows.type IN (SELECT id FROM " + ConfigHandler.prefix + "entity_map WHERE LOWER(entity) IN (" + names + "))";
if (entities.contains(EntityType.PLAYER)) {
predicate += " OR (entity_rows.action=" + LookupActions.ENTITY_KILL + " AND entity_rows.type=0)";
}
return "(" + predicate + ")";
}
}
21 changes: 21 additions & 0 deletions src/main/java/net/coreprotect/api/EntityAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.coreprotect.api;

import net.coreprotect.model.action.LookupActions;

/**
* Entity actions used by typed entity lookup filters.
*/
public enum EntityAction {
KILL(LookupActions.ENTITY_KILL),
SPAWN(LookupActions.ENTITY_SPAWN);

private final int id;

EntityAction(int id) {
this.id = id;
}

public int id() {
return id;
}
}
Loading
Loading