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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions docs/api/version/v13.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ All API v12 methods remain available. API v13 adds the following:
| Entity-spawn action | Adds `CoreProtectAction.ENTITY_SPAWN` with ID `13`. Legacy lookup, rollback, and restore methods accept this action explicitly. |
| Entity result parsing | `BlockResult` and `ParseResult` expose `getEntityType()` for entity kills and spawns. Their material and block-data getters return `null` for entity rows. |
| Tracked containers | Container and inventory lookups include transactions in tracked boats and minecarts. |
| Pre-log events | Adds `CoreProtectPreLogEvent.Action.ENTITY_SPAWN` and `ENTITY_INTERACTION`. |
| Virtual containers | Adds `logVirtualContainerTransaction` for plugin-owned inventories that have no block or entity, and `registerVirtualContainer` with `VirtualContainerHandler` and `VirtualContainerChange` to roll them back and restore them. |
| Pre-log events | Adds `CoreProtectPreLogEvent.Action.ENTITY_SPAWN`, `ENTITY_INTERACTION`, and `VIRTUAL_CONTAINER_TRANSACTION`. |

The existing `blockLookup(Block, LookupOptions)` overload retains entity events for compatibility. The new `blockLookup(LookupOptions)` overload returns only block events; use `entityLookup` for entities.

Expand Down Expand Up @@ -144,6 +145,8 @@ The table below lists the supported methods on `CoreProtectAPI`. Click a method
| [`logChat(Player, String)`](#chat-and-commands) / [`logCommand(Player, String)`](#chat-and-commands) | `boolean` | Queues player messages or commands. |
| [`logPlacement(...)`](#block-placement-and-removal) / [`logRemoval(...)`](#block-placement-and-removal) | `boolean` | Queues block changes. |
| [`logContainerTransaction(String, Location)`](#container-transactions) | `boolean` | Captures a block inventory before your changes. |
| [`logVirtualContainerTransaction(...)`](#virtual-containers) | `boolean` | Queues a change to a plugin-owned inventory. |
| [`registerVirtualContainer(...)`](#rolling-back-virtual-containers) | `boolean` | Lets rollbacks and restores change a type of plugin-owned inventory. |
| [`logInteraction(String, Location)`](#block-interactions) | `boolean` | Queues a block interaction. |
| [`performPurge(int)`](#purging-history) | `void` | Dispatches a database purge command. |

Expand Down Expand Up @@ -610,6 +613,39 @@ Both return `boolean`, require the API to be enabled, and honor the world's corr

This method is for a block container. Tracked entity-container lookup support does not add an entity overload to this logging method.

### Virtual containers

`boolean logVirtualContainerTransaction(String user, NamespacedKey type, String id, ItemStack[] before, ItemStack[] after)` queues a change to a virtual container: an inventory that belongs to your plugin and has no block or entity, such as a backpack, a team storage, or an ender chest opened by command.

| Parameter | Meaning |
| --- | --- |
| `user` | Username or plugin actor credited with the change. |
| `type` | Container type. Its key must not contain `/`. |
| `id` | Container within the type: 1 to 64 characters of `a-z`, `0-9`, `.`, `_`, or `-`. |
| `before`, `after` | Contents before and after the change. `null` means empty. |

CoreProtect records the difference between `before` and `after` as items removed and added, so you can pass full snapshots or only the stacks that changed. When the arrays are index-aligned snapshots (the same length, or one of them `null`), each record also keeps the slot where the item changed, and an item that changed in two slots is recorded once per slot. Because you supply one actor's change, the record is credited correctly even when several players view the same inventory. Copy the contents before changing them, and call the method on the thread that owns the inventory; CoreProtect copies both arrays before it returns.

The type and id form the key `namespace:type/id`, which can be at most 191 characters. The method returns `false` without logging when the API is disabled, `item-transactions` is disabled in the global configuration, `user` is empty, or the type or id is invalid.

Search the records with `/co lookup container:<type>[/<id>]`, and roll them back or restore them with `/co rollback` or `/co restore` and the same parameter. Other lookups, rollbacks, and restores do not include them, and the legacy `performRollback` and `performRestore` methods cannot select them.

#### Rolling back virtual containers

CoreProtect cannot change an inventory that only your plugin knows, so a rollback or restore calls the handler you register for the type:

`boolean registerVirtualContainer(Plugin plugin, NamespacedKey type, VirtualContainerHandler handler)`

Registering a type again replaces its handler. It returns `false` when the API is disabled, an argument is `null`, or the type is invalid. Register the handler when your plugin enables; while your plugin is disabled, containers of the type are skipped.

`VirtualContainerHandler` has one method, `int apply(String id, List<VirtualContainerChange> changes)`. CoreProtect calls it once per container on the server thread (on Folia, the global region thread), with the changes in the order they must be applied: newest first for a rollback, oldest first for a restore. Apply them in order, stop at the first change that can't be applied, and return how many leading changes you applied. Only those are marked as rolled back or restored; the rest are skipped and reported, and running the command again retries them. When the handler throws or returns a number outside `0` to `changes.size()`, CoreProtect logs it under your plugin's name and marks none of that container's changes, so do not leave a container partly changed in that case. Do not log the changes the handler applies with `logVirtualContainerTransaction`; CoreProtect marks the logged rows instead.

| `VirtualContainerChange` method | Meaning |
| --- | --- |
| `getItem()` | A copy of the items to add or remove. Logged changes are merged per item, so the amount can exceed the maximum stack size. |
| `isAddition()` | `true` to add the items, `false` to remove them. The direction is already resolved: rolling back a removal adds the items back. |
| `getSlot()` | The slot in the container where the logged change happened, or `-1` when it is unknown: the change was logged before CoreProtect recorded slots, or `before` and `after` were not index-aligned. It is a hint; your handler decides where the items go. |

### Block interactions

`boolean logInteraction(String user, Location location)` records an interaction with the block currently at the location. It reads the block's material and block data.
Expand Down Expand Up @@ -664,8 +700,9 @@ The action enum contains:
| `CONTAINER_TRANSACTION` | Container item transaction. |
| `ITEM_TRANSACTION` | World item transaction. |
| `PLAYER_COMMAND` | Player command. |
| `VIRTUAL_CONTAINER_TRANSACTION` | Virtual container item transaction. |

Use `getAction()` to identify the category before interpreting the action ID. Container, item, and sign events use their own stored action values; player-command events use `-1`. Command events expose their command as the message. Entity-interaction events expose the interaction action name. Sign-text events do not provide their lines through `getMessage()`.
Use `getAction()` to identify the category before interpreting the action ID. Container, item, and sign events use their own stored action values; player-command events use `-1`. Command events expose their command as the message. Entity-interaction events expose the interaction action name. Virtual-container events have a `null` location, ignore `setLocation`, and expose the container key as the message. Sign-text events do not provide their lines through `getMessage()`.

The event is not emitted for every logging category; for example, chat and session logging do not emit it.

Expand Down Expand Up @@ -934,6 +971,36 @@ api.logContainerTransaction("Notch", inventory.getLocation());
inventory.addItem(itemStack);
```

For an inventory that only your plugin knows, such as a backpack, copy its contents before the change and pass both snapshots afterwards:

```java
ItemStack[] before = Arrays.stream(backpack.getContents()).map(item -> item == null ? null : item.clone()).toArray(ItemStack[]::new);
backpack.addItem(itemStack);
api.logVirtualContainerTransaction("Notch", new NamespacedKey(plugin, "backpack"), backpackId, before, backpack.getContents());
```

To let rollbacks and restores change those backpacks, register a handler when your plugin enables. This one stops at the first change that does not fit or whose items are no longer there, and undoes the part of that change it applied:

```java
api.registerVirtualContainer(plugin, new NamespacedKey(plugin, "backpack"), (id, changes) -> {
Inventory backpack = backpacks.get(id);
if (backpack == null) {
return 0;
}
int applied = 0;
for (VirtualContainerChange change : changes) {
ItemStack[] contents = Arrays.stream(backpack.getContents()).map(item -> item == null ? null : item.clone()).toArray(ItemStack[]::new);
boolean complete = change.isAddition() ? backpack.addItem(change.getItem()).isEmpty() : backpack.removeItem(change.getItem()).isEmpty();
if (!complete) {
backpack.setContents(contents);
break;
}
applied++;
}
return applied;
});
```

### Listen for pre-log events

This listener cancels block-placement records attributed to a plugin's preview actor:
Expand Down
19 changes: 19 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Perform a lookup. Nearly all of the parameters are optional.
| [`i:<include>`](#iinclude) | Include specific blocks/entities in the lookup. |
| [`e:<exclude>`](#eexclude) | Exclude blocks/entities from the lookup. |
| [`f:<filter>`](#ffilter) | Include or exclude chat, command, or sign text by prefix. |
| [`container:<type>`](#containertype) | Restrict the command to a plugin's virtual containers. |
| [`#<hashtag>`](#hashtag) | Add a hashtag to perform additional actions. |

#### Pagination
Expand Down Expand Up @@ -270,6 +271,20 @@ ___

---

### `container:<type>`

*Look up, roll back, or restore items added to or removed from virtual containers: inventories that a plugin keeps without a block or entity, such as backpacks or team storage, and logs through the API.*

* Example: `container:myplugin:backpack t:1d` *(every backpack of that plugin)*
* Example: `container:myplugin:backpack/1f0c t:1d` *(a single backpack)*
* Example: `container:myplugin:backpack/1f0c u:Notch t:7d a:-container` *(items Notch removed from it)*

> Virtual containers have no location, so these commands are always global and can't be combined with `r:` or `#container`. They accept `u:`, `t:`, `i:`, `e:`, `a:container`, `a:+container`, and `a:-container`, and lookups also accept `#count` and `#summary`. Other lookups, rollbacks, and restores don't include virtual containers.

> A rollback or restore of a single container, `container:<type>/<id>`, works without `u:`, but one of every container of a type needs `u:`. Neither can be previewed. The plugin that owns the containers applies the changes, newest first for a rollback and oldest first for a restore. Changes that it can't apply, or that belong to a type no enabled plugin handles, are skipped and reported, and running the command again retries them.

---

### `#<hashtag>`

Add a hashtag to the end of your command to perform additional actions.
Expand Down Expand Up @@ -311,6 +326,10 @@ By default, if no radius is specified, a radius of 10 will be applied, restricti
*(rollback griefing Notch did in the last hour ONLY in the Nether)*
* `/co rollback u:Notch t:5m a:inventory`
*(rollback inventory transactions by Notch in the last 5 minutes)*
* `/co rollback u:Notch t:1h container:myplugin:backpack/1f0c`
*(rollback the items Notch added to or removed from a plugin's backpack in the last hour)*
* `/co rollback t:1h container:myplugin:backpack/1f0c`
*(rollback everything anyone added to or removed from that backpack in the last hour)*
* `/co rollback t:15m r:30`
*(rollback everything done in the last 15 minutes by anyone within 30 blocks of you)*
* `/co rollback t:15m r:#worldedit`
Expand Down
1 change: 1 addition & 0 deletions lang/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ ROLLBACK_LENGTH: "Time taken: {0} {second|seconds}."
ROLLBACK_MODIFIED: "{Modified|Modifying} {0}."
ROLLBACK_RADIUS: "Radius: {0} {block|blocks}."
ROLLBACK_SELECTION: "Radius set to \"{0}\"."
ROLLBACK_SKIPPED: "Skipped {0} virtual container {change|changes}. See the console for details."
ROLLBACK_STARTED: "{Rollback|Restore|Preview} started on \"{0}\"."
ROLLBACK_TIME: "Time range: {0}."
ROLLBACK_WORLD_ACTION: "Restricted to {world|action} \"{0}\"."
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/net/coreprotect/CoreProtectAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Server;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;

import net.coreprotect.api.BlockAPI;
import net.coreprotect.api.EntityAPI;
Expand All @@ -28,6 +31,7 @@
import net.coreprotect.api.SessionLookup;
import net.coreprotect.api.SignAPI;
import net.coreprotect.api.UsernameAPI;
import net.coreprotect.api.VirtualContainerHandler;
import net.coreprotect.api.result.BlockResult;
import net.coreprotect.api.result.ContainerResult;
import net.coreprotect.api.result.EntityResult;
Expand All @@ -44,12 +48,15 @@
import net.coreprotect.database.Database;
import net.coreprotect.database.Lookup;
import net.coreprotect.database.rollback.Rollback;
import net.coreprotect.database.rollback.VirtualContainerRollbackHandler;
import net.coreprotect.language.Phrase;
import net.coreprotect.listener.player.InventoryChangeListener;
import net.coreprotect.model.action.LookupActions;
import net.coreprotect.model.item.VirtualContainerTransaction;
import net.coreprotect.utility.Chat;
import net.coreprotect.utility.MaterialUtils;
import net.coreprotect.utility.ErrorReporter;
import net.coreprotect.utility.VirtualContainerUtils;

/**
* The main API class for CoreProtect.
Expand Down Expand Up @@ -556,6 +563,61 @@ public boolean logContainerTransaction(String user, Location location) {
return InventoryChangeListener.inventoryTransaction(user, location, null);
}

/**
* Logs a transaction in a virtual container: an inventory owned by a plugin that has no block or entity.
* CoreProtect records the difference between the two snapshots.
*
* @param user
* The username or plugin actor credited with the change
* @param type
* The container type, a namespaced key whose key part contains no "/"
* @param id
* The container within its type: 1 to 64 characters of a-z, 0-9, ".", "_" or "-"
* @param before
* The contents before the change, or null when empty
* @param after
* The contents after the change, or null when empty
* @return True if the transaction was queued
*/
public boolean logVirtualContainerTransaction(String user, NamespacedKey type, String id, ItemStack[] before, ItemStack[] after) {
if (!isEnabled() || !Config.getGlobal().ITEM_TRANSACTIONS || user == null || user.isEmpty() || type == null) {
return false;
}

String key = VirtualContainerUtils.getKey(type.getNamespace() + ":" + type.getKey(), id);
if (key == null) {
return false;
}

return Queue.queueVirtualContainerTransaction(user, new VirtualContainerTransaction(key, before, after));
}

/**
* Registers the handler that applies rollbacks and restores to a type of virtual container. Registering the type
* again replaces its handler. While the plugin is disabled, rollbacks and restores skip containers of this type.
*
* @param plugin
* The plugin that owns the containers
* @param type
* The container type, as passed to logVirtualContainerTransaction
* @param handler
* The handler that applies the changes
* @return True if the handler was registered
*/
public boolean registerVirtualContainer(Plugin plugin, NamespacedKey type, VirtualContainerHandler handler) {
if (!isEnabled() || plugin == null || type == null || handler == null) {
return false;
}

String typeKey = type.getNamespace() + ":" + type.getKey();
if (!VirtualContainerUtils.isValidType(typeKey)) {
return false;
}

VirtualContainerRollbackHandler.register(plugin, typeKey, handler);
return true;
}

/**
* Logs a block placement by a user.
*
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/net/coreprotect/api/VirtualContainerChange.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.coreprotect.api;

import org.bukkit.inventory.ItemStack;

/**
* One item change that a rollback or restore applies to a virtual container.
*/
public final class VirtualContainerChange {

private final ItemStack item;
private final boolean addition;
private final int slot;

public VirtualContainerChange(ItemStack item, boolean addition, int slot) {
this.item = item.clone();
this.addition = addition;
this.slot = slot;
}

/**
* @return A copy of the items to add or remove. Logged changes are merged per item, so the amount can exceed the
* maximum stack size.
*/
public ItemStack getItem() {
return item.clone();
}

/**
* @return True to add the items to the container, false to remove them. Rollbacks and restores are already
* resolved, so a rollback of a removal is an addition.
*/
public boolean isAddition() {
return addition;
}

/**
* @return The slot in the container where the logged change happened, or -1 when it is unknown. It is a hint: the
* handler decides where the items go.
*/
public int getSlot() {
return slot;
}
}
24 changes: 24 additions & 0 deletions src/main/java/net/coreprotect/api/VirtualContainerHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.coreprotect.api;

import java.util.List;

/**
* Applies rollbacks and restores to the virtual containers of one type, registered with
* CoreProtectAPI.registerVirtualContainer.
*/
public interface VirtualContainerHandler {

/**
* Applies changes to a virtual container. CoreProtect calls this on the server thread (on Folia, the global region
* thread) with the changes of one container in the order they must be applied. Apply them in order and stop at
* the first change that can't be applied, for example an item that is no longer in the container.
*
* @param id
* The container within its type, as passed to logVirtualContainerTransaction
* @param changes
* The changes to apply
* @return How many leading changes were applied. Only those are marked as rolled back or restored, so running the
* command again retries the rest.
*/
int apply(String id, List<VirtualContainerChange> changes);
}
4 changes: 4 additions & 0 deletions src/main/java/net/coreprotect/command/CommandParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ protected static RollbackStateParser.ParseResult parseRollbackState(String[] inp
return RollbackStateParser.parse(inputArguments);
}

protected static VirtualContainerParser.ParseResult parseVirtualContainer(String[] inputArguments) {
return VirtualContainerParser.parse(inputArguments);
}

/**
* Parse coordinates from command arguments
*
Expand Down
Loading