API: The Java API for plugins: logging edits, reading history, running rollbacks, purging history, and loading the worlds a plugin keeps
# Developer API
> What your plugin can do with the Oasis API, with one example from start to finish.
Oasis has a Java API for other Paper plugins. With it, your plugin can: * **Log its own edits.** When your plugin changes blocks, moves items or spawns entities, log them. Staff can then look them up and roll them back like anything a player did. * **Read history.** Ask for the edits a player made, the edits in an area, the whole history of one block, or the positions an operation kept, a page at a time. * **Run rollbacks, restores and undos.** Start the same operations staff run with `/rollback`, `/rs` and `/undo`, follow their progress, and read what they did. * **Purge history.** Delete edits for good, the same way /oasis purge does, even in a world that is not loaded. * **Read and clear incidents.** List the problems the store recorded, and clear them, as `/oasis incidents` does. * **Load the worlds it keeps.** Give Oasis a loader for worlds in your plugin’s own storage, such as slime worlds, so a rollback can write into them while they are not loaded. * **React to operations and purges.** Listen for the end of every operation and every purge, and for each chunk an operation rewrites, whoever started it. The API lives in one package, `com.oasis.api`, and its events live in `com.oasis.api.event`. Everything else in the Oasis jar is internal and can change at any time. ## A first example [Section titled “A first example”](#a-first-example) This plugin asks for the API when it starts. Its `revert` method shows a staff member the newest edit a player made in the last hour, then rolls back that hour and reports the result.
```java
package com.example.quickstart;
import com.oasis.api.Edit;
import com.oasis.api.Oasis;
import com.oasis.api.OasisApi;
import com.oasis.api.Selection;
import java.time.Duration;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
public final class QuickStart extends JavaPlugin
{
private OasisApi oasis;
@Override
public void onEnable()
{
// Oasis is in `depend` of plugin.yml, so it is enabled before this runs.
oasis = Oasis.api(this);
}
public void revert(Player staff, OfflinePlayer griefer)
{
Selection lastHour = Selection.all().by(griefer).within(Duration.ofHours(1));
oasis.lookups().find(lastHour, 1).thenAccept(page ->
{
if (page.items().isEmpty())
{
staff.sendMessage(griefer.getName() + " made no edits in the last hour.");
return;
}
Edit newest = page.items().getFirst();
staff.sendMessage("Newest edit: " + newest.cause().word() + " " + newest.after() + " at "
+ newest.position().x() + " " + newest.position().y() + " " + newest.position().z());
oasis.operations().rollback(lastHour)
.requestedBy(staff)
.audience(staff)
.start()
.result().thenAccept(result ->
staff.sendMessage("Rolled back " + result.applied() + " edits. Kept " + result.kept() + "."));
});
}
}
```
Every future in the API completes on the main thread, so both callbacks can send messages and touch the world. Because the request names `staff` as its audience, the staff member also sees the start line, the progress bar and the summary, just as for a command. ## The pages [Section titled “The pages”](#the-pages) [Getting started](/docs/api/getting-started/)Add the dependency, declare Oasis in your plugin file, and get the API. [Core concepts](/docs/api/concepts/)The person and the actor, the thread rule, pages without totals, and nullness. [Selecting edits](/docs/api/selections/)Every Selection method: who, when, where and what. [Reading history](/docs/api/reading-history/)Pages of edits, the history of one block, and every field of an Edit. [Logging edits](/docs/api/logging-edits/)Log the blocks, items and entities your plugin changes. [Running operations](/docs/api/operations/)Rollbacks, restores and undos: estimate, start, follow, cancel and read the result. [Purging history](/docs/api/purges/)Delete edits for good: count, start, follow and read the result. Purge a world that is not loaded. [Incidents](/docs/api/incidents/)List the problems the store recorded, and clear them. [Loading your worlds](/docs/api/worlds/)Give Oasis a loader for the worlds your plugin keeps, so a rollback can write into them. [Events](/docs/api/events/)Redraw a map after a rollback, and report every operation and purge that ends. [Vocabulary](/docs/api/reference/)Every kind, cause, actor, phase, outcome and failure reason. ## For AI agents [Section titled “For AI agents”](#for-ai-agents) If you build with an AI coding agent, give it the API pages as plain text, or install the `oasis-api` skill. See [For AI agents](/docs/api/ai-agents/).
# Getting started
> Add the Oasis API to your build, declare Oasis in your plugin file, and get the API in onEnable.
The API is one small jar, `com.oasis:oasis-api`. It holds interfaces and a few records and enums. The Oasis plugin on the server holds the code behind them, so your plugin compiles against the jar and Oasis provides it at runtime. The API needs Java 21 or newer and Paper 1.21.11 or newer. ## Set up your plugin [Section titled “Set up your plugin”](#set-up-your-plugin) 1. **Add the repository and the dependency.** The API is on the Oasis Maven repository, `https://telesphorepo.com/maven/oasis/`. The current version is `3.0-SNAPSHOT`. * Gradle (Kotlin) build.gradle.kts
```kotlin
repositories {
maven("https://repo.papermc.io/repository/maven-public/")
maven("https://telesphorepo.com/maven/oasis/")
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
compileOnly("com.oasis:oasis-api:3.0-SNAPSHOT")
}
```
* Gradle (Groovy) build.gradle
```groovy
repositories {
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://telesphorepo.com/maven/oasis/' }
}
dependencies {
compileOnly 'io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT'
compileOnly 'com.oasis:oasis-api:3.0-SNAPSHOT'
}
```
* Maven pom.xml
```xml
papermchttps://repo.papermc.io/repository/maven-public/oasishttps://telesphorepo.com/maven/oasis/io.papermc.paperpaper-api1.21.11-R0.1-SNAPSHOTprovidedcom.oasisoasis-api3.0-SNAPSHOTprovided
```
2. **Keep the API out of your jar.** Use `compileOnly` in Gradle and `provided` in Maven, as above. Never shade or relocate the API into your plugin. Oasis provides those classes on the server, and a second copy in your jar is a different class to Java. Oasis would not recognize it, and `Oasis.api(this)` would fail. 3. **Declare Oasis in your plugin file.** Oasis must enable before your plugin, so the API is ready in your `onEnable`. Use `depend` when your plugin can’t run without Oasis, and `softdepend` when Oasis is optional. * plugin.yml plugin.yml
```yaml
name: GriefRevert
version: 1.0.0
main: com.example.griefrevert.GriefRevert
api-version: '1.21.11'
# Oasis is required:
depend: [Oasis]
# Or, when Oasis is optional:
# softdepend: [Oasis]
```
* paper-plugin.yml paper-plugin.yml
```yaml
name: GriefRevert
version: 1.0.0
main: com.example.griefrevert.GriefRevert
api-version: '1.21.11'
dependencies:
server:
Oasis:
load: BEFORE
# false when Oasis is optional
required: true
# Lets your plugin see the com.oasis.api classes.
join-classpath: true
```
A Paper plugin needs `join-classpath: true`. Without it, your plugin can’t load the `com.oasis.api` classes even when Oasis runs. 4. **Get the API in `onEnable`.** Ask for the API once, with your own plugin, and keep it in a field.
```java
package com.example.griefrevert;
import com.oasis.api.Oasis;
import com.oasis.api.OasisApi;
import org.bukkit.plugin.java.JavaPlugin;
public final class GriefRevert extends JavaPlugin
{
private OasisApi oasis;
@Override
public void onEnable()
{
oasis = Oasis.api(this);
}
}
```
`Oasis.api(this)` throws `IllegalStateException` when Oasis is not enabled. With `depend`, Paper doesn’t enable your plugin without Oasis, so that only happens when Oasis itself failed to start. The API you get acts for your plugin. See [What the API acts for](/docs/api/concepts/#what-the-api-acts-for). ## When Oasis is optional [Section titled “When Oasis is optional”](#when-oasis-is-optional) With `softdepend`, your plugin also runs on servers without Oasis. On those servers the `com.oasis.api` classes are not on the classpath at all. Java loads a class the first time your code needs it, so any code that names an Oasis type throws `NoClassDefFoundError` when it runs, or when Java loads the class that holds it. The safe pattern is to keep every Oasis reference in one class of your own, and to load that class only after you check that Oasis is enabled. Your main class names only that class, never an Oasis type. OasisHook.java
```java
package com.example.softdepend;
import com.oasis.api.Cause;
import com.oasis.api.Oasis;
import com.oasis.api.OasisApi;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
/** Every Oasis call of the plugin. Nothing else in the plugin imports com.oasis.api. */
final class OasisHook
{
private final OasisApi api;
OasisHook(Plugin plugin)
{
api = Oasis.api(plugin);
}
void placed(Player player, BlockState before, BlockState after)
{
api.logger().by(player).block(Cause.PLACING, before, after);
}
}
```
SoftDepend.java
```java
package com.example.softdepend;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
public final class SoftDepend extends JavaPlugin
{
/** Null when Oasis is not on the server. */
private OasisHook oasis;
@Override
public void onEnable()
{
if (getServer().getPluginManager().isPluginEnabled("Oasis"))
{
oasis = new OasisHook(this);
getLogger().info("Oasis found. Edits of this plugin go into its history.");
}
}
/** Called by the rest of the plugin, with or without Oasis. */
public void logPlacement(Player player, BlockState before, BlockState after)
{
if (oasis != null)
{
oasis.placed(player, before, after);
}
}
}
```
A field or a variable of type `OasisHook` is safe, because Java loads `OasisHook` only when the code first creates one or calls it. Keep these rules: * No `com.oasis.api` import in the main class, or in any class that loads when Oasis is missing. * No Oasis type in a field, a parameter, a return type, a lambda or a method reference of those classes. * Register an Oasis event listener from inside the hook class, after the check. A listener class names the event type, so it must not load on a server without Oasis. ## The Javadoc in your IDE [Section titled “The Javadoc in your IDE”](#the-javadoc-in-your-ide) Oasis publishes a sources jar and a Javadoc jar beside the API jar. Your IDE shows the Javadoc of every method as you type, and the Javadoc states the same rules as these pages: the thread of each callback, what throws, and what each value means. IntelliJ IDEA downloads the sources for a Gradle project on its own. In a Maven project, click **Download Sources** in the editor banner, or run `mvn dependency:sources`.
# Core concepts
> The person and the actor, immutable selections, the thread rule, pages without totals, nullness, and who the API acts for.
A few ideas run through the whole API. Read this page once, and the rest of the API reads the way you expect. ## The person and the actor [Section titled “The person and the actor”](#the-person-and-the-actor) Every edit names two things: * The **person** is who is behind the edit: the player who acted, or the player who started a chain that the world carried on. * The **actor** is the world actor that carried the edit, such as `Actor.LAVA`. It is empty for an edit a person made directly. Take a player who pours lava. The lava flows down a hill and sets a forest on fire. Oasis logs each block the lava and the fire change with the player as the person, and with `Actor.LAVA` or `Actor.FIRE` as the actor. So a rollback of that player puts the whole forest back, and a lookup still tells you that lava did it. When nobody is behind an edit, such as lava that flows out of a natural lava lake, the person is the id of the [`Actor`](/docs/api/reference/#actor) that acted. A world actor, such as `Actor.LAVA`, is then the actor of the edit too: `edit.person()` is `Actor.LAVA.id()`, and `edit.actor()` holds `Actor.LAVA`. `Actor.of(edit.person())` turns a person id back into an actor, and is empty for a player. The logger and the reader use the same two words. You log the lava above as:
```java
oasis.logger().by(player).via(Actor.LAVA).block(Cause.FLOW, at, air, lava);
```
And you read it back with `edit.person()`, which is the player’s id, and `edit.actor()`, which holds `Actor.LAVA`. A `Selection.all().by(player)` chooses the edits of that person, the lava and the fire included. A `Selection.all().by(Actor.LAVA)` chooses all lava: the lava with nobody behind it, and the lava from any player’s bucket. ## Selections and requests are immutable [Section titled “Selections and requests are immutable”](#selections-and-requests-are-immutable) A `Selection` is a filter you build from `Selection.all()`. Every method returns a new selection and changes one part of it. It never changes the selection you call it on.
```java
Selection base = Selection.all().within(Duration.ofHours(2));
Selection ofSteve = base.by(steve); // base still selects every player
Selection ofAlex = base.by(alex); // Alex alone: ofSteve did not change base
```
Each method **replaces** its part. `by(steve).by(alex)` selects Alex only. To select both, name them in one call: `by(steve, alex)`. A part you never set does not narrow the selection. `not()` leaves edits out with the same verbs, and replaces its part the same way. An `OperationRequest` works the same way. You can keep one in a field, share it between threads, and start it as often as you like. See [Selecting edits](/docs/api/selections/) and [Running operations](/docs/api/operations/). ## The thread rule [Section titled “The thread rule”](#the-thread-rule) Three rules cover every call in the API: 1. **Every call returns at once.** No method waits for the disk. A method that needs the store returns a `CompletableFuture`. 2. **Every future and every callback completes on the main thread.** That covers pages, estimates, progress callbacks and results. You may touch the world, send messages and open inventories in any of them, with no scheduler call of your own. 3. **The logger takes edits from any thread.** Some of its methods read a Paper object on the thread that calls. [Logging edits](/docs/api/logging-edits/#threads) says which. Never join() on the main thread Never call `join()` or `get()` on an Oasis future from the main thread. The future completes on the main thread, so the main thread would wait for itself and the server would freeze. Use `thenAccept`, `thenCompose` and `whenComplete` instead. A call fails in one of three ways: * **A call you get wrong throws `IllegalArgumentException`** at once, such as a selection that cannot match anything. See [Selecting edits](/docs/api/selections/#a-selection-that-matches-nothing). * **A read completes exceptionally with an `OasisException`.** A lookup, a `Page.next()`, a history, a kept list, an estimate or a purge count can fail, for example when the store is still opening, holds too many requests, or when a selection names a world that does not exist. The exception carries a `Failure`. Handle it with `exceptionally` or `whenComplete`. See [When a read fails](/docs/api/reading-history/#when-a-read-fails). * **An operation or a purge always completes its result,** with the failure inside it: a failed operation ends with `Outcome.FAILED` and a [`Failure`](/docs/api/operations/#when-an-operation-fails), and a failed purge ends the same way with a [`PurgeResult`](/docs/api/purges/#the-result). When Oasis disables, for example while the server stops, it runs no more ticks. A future that has not completed by then never completes, and the logger drops the edits it gets. ## Pages carry no totals [Section titled “Pages carry no totals”](#pages-carry-no-totals) A `Page` holds its `items()`, `hasNext()`, and `next()`. It has no total, no page count and no page number. That is on purpose. To count a selection, the store must read every edit in it, and on a busy server that can be hundreds of millions of rows. A page reads its own edits and one more, so the first page costs the same however much history the server holds. `next()` reads on from the last edit of the page, so the tenth page costs what the first page costs. Progress works the same way. An operation counts the chunks it writes, not the edits it will write. When you need a size before an operation starts, ask for it by name with [`estimate()`](/docs/api/operations/#estimate-and-confirm), which reads the selection once and changes nothing. ## Nullness [Section titled “Nullness”](#nullness) Both packages, `com.oasis.api` and `com.oasis.api.event`, are `@NullMarked` with [JSpecify](https://jspecify.dev). No method takes or returns null. Your IDE and a null checker read the annotation and warn you. A value that can be absent is an `Optional` or an `OptionalLong`: `Edit.actor()`, `Edit.item()`, `Edit.operation()`, `Operation.id()`, `OperationEndEvent.operation()`, `Result.failure()`, `Result.incomplete()`, `Result.forceKept()` and the getters of a `Selection`. No id or count uses 0 or -1 to mean “none”. The text fields of an edit, `before()`, `after()` and `text()`, are an empty string when the edit has none. ## What the API acts for [Section titled “What the API acts for”](#what-the-api-acts-for) `Oasis.api(this)` returns an API that acts for your plugin. Oasis gives your plugin a person of its own, with the key `#plugin:` followed by your plugin’s name, such as `#plugin:GriefRevert`. * An operation you start names your plugin as the one who asked for it, until you name someone else with `requestedBy`. Staff see `#plugin:GriefRevert` in a lookup, and `/undo` of a staff member never reaches your plugin’s operations. * `oasis.id()` is the id of that person. Oasis makes it from the key, so it is the same on every server. * `oasis.key(person)` is the key staff type after `user` for a person that is not a player. It is an actor key such as `#lava`, or `#plugin:Name` for a plugin that asked for the API since the server started. It is empty for a player, and for an id Oasis does not know. Ask Bukkit for a player’s name. * `oasis.operations().active()` lists the operations your plugin started that still run. It does not list another plugin’s operations, or the ones staff typed.
```java
String who = oasis.key(event.requester())
.orElseGet(() -> String.valueOf(Bukkit.getOfflinePlayer(event.requester()).getName()));
```
This turns any requester into a name: a plugin, an actor such as `#console`, or a player.
# Selecting edits
> Every Selection method, by who, when, where and what, with the rules each one follows.
A `Selection` chooses edits. Lookups read the edits it chooses, rollbacks and restores act on them, and purges delete them. It is the API’s version of the filters staff type after `/lookup` and `/rollback`. Start from `Selection.all()`, which chooses every edit, and narrow it:
```java
Selection grief = Selection.all()
.by(griefer)
.within(Duration.ofHours(2))
.near(staff.getLocation(), 30)
.kinds(Kind.BLOCK, Kind.CONTAINER)
.not().causes(Cause.WORLDEDIT);
```
A selection is split into parts: who, when, where, the kinds, the causes, the materials, and what it leaves out. Each method sets one part and **replaces** what that part held before. A part you never set does not narrow the selection, so `Selection.all()` holds every edit of every kind, chat, commands and logins included. Every method returns a new selection and leaves the old one as it was. ## Who [Section titled “Who”](#who) | Method | Chooses | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `by(OfflinePlayer... players)` | Edits by these players, whether the player acted or started a chain the world carried on. | | `by(UUID... ids)` | Edits by these people: player ids, or `Actor.id()` ids. | | `by(Actor... actors)` | Edits these actors made or carried, such as `Actor.LAVA`: the lava with nobody behind it, and the lava that flowed from any player’s bucket. | People and actors share one part. An edit matches when its [person, or the actor that carried it](/docs/api/concepts/#the-person-and-the-actor), is one of the ids you name. So: * `by(player)` chooses what the player did, and what the world carried on for them: the lava they poured and the fire that lava started. * `by(Actor.LAVA)` chooses all lava, whoever is behind it. It is what staff get with `user #lava` in a command. The three `by` methods replace one another, so `by(player).by(Actor.LAVA)` selects all lava, not the player’s lava. To mix players and actors in one selection, pass their ids to `by(UUID...)`. An actor’s id there works as it does in `by(Actor...)`:
```java
Selection griefOrCreepers = Selection.all().by(griefer.getUniqueId(), Actor.CREEPER.id());
```
To find only the lava one player poured, select the player and check `edit.actor()` on each edit. A lookup of `by(player)` also reads the player’s logins and logouts. Add `kinds(Kind.BLOCK)` to read blocks alone. ## When [Section titled “When”](#when) | Method | Chooses | | --------------------------- | ---------------------------------------------------------------------------- | | `within(Duration duration)` | Edits in the last `duration`, counted from the moment the selection is read. | | `since(Instant time)` | Edits at or after this time. | | `until(Instant time)` | Edits at or before this time. | Times are inclusive at both ends. The three methods share the time part, and each one replaces what it finds there: * `within` replaces the whole time part. It removes a `since` and an `until`. * `since` replaces a `within` window, and keeps an `until`. * `until` replaces a `within` window, and keeps a `since`. A window always runs up to the moment of the read, so a selection never holds a window and an `until`. For a span that ends in the past, use `since` and `until` with an `Instant`:
```java
// Edits from the last day, up to one hour ago.
Instant now = Instant.now();
Selection dayBefore = Selection.all()
.since(now.minus(Duration.ofDays(1)))
.until(now.minus(Duration.ofHours(1)));
// Edits older than thirty days.
Selection old = Selection.all().until(Instant.now().minus(Duration.ofDays(30)));
```
`since` after `until` throws `IllegalArgumentException`. ### A window counts back from when it runs [Section titled “A window counts back from when it runs”](#a-window-counts-back-from-when-it-runs) `within` keeps the duration, not a fixed time. A lookup, an estimate or a start counts it back from the moment it reads the selection. So a request you build at noon and start at one o’clock, with `within(Duration.ofHours(2))`, rolls back the edits from eleven to one.
```java
// Built once, in onEnable.
OperationRequest lastTwoHours = oasis.operations()
.rollback(Selection.all().by(griefer).within(Duration.ofHours(2)));
// Started later. Each start reads the two hours before it.
lastTwoHours.start();
```
A start fixes the window once, when it begins. Its sweeps and its [`forceKept()`](/docs/api/operations/#kept-left-and-force) request then read the same edits as its first pass. `since` and `until` take an `Instant`, so they never move. ## Where [Section titled “Where”](#where) | Method | Chooses | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `in(World... worlds)` | Edits anywhere in these worlds. | | `in(String... worlds)` | Edits anywhere in these worlds by their stored names, for a world that is not loaded. | | `near(Location center, int radius)` | Edits in the cube of this radius, in blocks, around the location. | | `box(Location corner, Location opposite)` | Edits in the box between two corners of one world. | | `at(Block block)` | Edits at one block, of every kind. | | `in(String world, Selection.Box box)` | Edits in a box of a world by its stored name, for a world that is not loaded. The box is never null. | All six share one part, the world and the box, so the last one you call wins. Boxes are inclusive. `near` is a cube, not a sphere, so `near(location, 0)` is the one block at that location. `box` takes its corners in any order. `Selection.Box` holds `minX`, `minY`, `minZ`, `maxX`, `maxY` and `maxZ`, all inclusive. It throws `IllegalArgumentException` for a lowest bound above its highest:
```java
// The spawn area of a world that is not loaded right now.
Selection spawn = Selection.all().in("world_old", new Selection.Box(-64, -64, -64, 64, 320, 64));
```
### A world that is not loaded [Section titled “A world that is not loaded”](#a-world-that-is-not-loaded) `in(String... worlds)` and `in(String world, Box box)` name a world that no server world has now. A world is not loaded when a staff member unloaded it, or when its folder is gone. The stored name is the one the world had when Oasis logged its edits. A lookup and a purge read and write the store alone. They take any world that is loaded, that Oasis saw loaded, or that holds edits in the store, with no folder needed. [A purge of whole worlds](/docs/api/purges/#a-purge-of-whole-worlds) deletes the history of such a world. A rollback, a restore and an undo write the world, so they need it. Oasis loads a world that is not loaded for the pass: through the [world loader](/docs/api/worlds/) of the plugin that keeps it, or from its folder. When nobody can load it, the operation fails with `WORLD_GONE` before it writes anything. Names are exact, as `World.getName()` reads them. Oasis checks every world name of a selection, chosen or left out, when a request reads it: a lookup, a history, an estimate, an operation, or a purge with its count. A name that is not loaded under that exact name, that Oasis never saw, and that the store holds no edits of fails the request with `Failure.Reason.NO_SUCH_WORLD`, so a typo never widens a rollback. ## What [Section titled “What”](#what) | Method | Chooses | | -------------------------------- | ---------------------------------------------------------------- | | `kinds(Kind... values)` | Edits of these kinds only, such as `Kind.BLOCK`. | | `causes(Cause... values)` | Edits with these causes only, such as `Cause.BREAKING`. | | `materials(Material... values)` | Edits of these blocks or items, before or after the edit. | | `entities(EntityType... values)` | Edits of these entities. | | `matching(String... patterns)` | Edits of blocks, items or entities by id or by a pattern of ids. | `kinds` and `causes` each have a part of their own. `materials`, `entities` and `matching` share one part, so the last of them you call wins. To choose blocks and entities in one selection, use `matching` with both ids:
```java
Selection signsAndFrames = Selection.all().matching("*_sign", "item_frame");
```
[Vocabulary](/docs/api/reference/) lists every `Kind` and `Cause`. ### Patterns [Section titled “Patterns”](#patterns) `matching` and `not().matching` take ids and patterns of ids: * An id is a namespaced id, such as `minecraft:oak_log` or `minecraft:zombie`. * An id with no namespace is a vanilla one: `oak_log` means `minecraft:oak_log`. * A `*` matches any run of characters: `*_sign` matches every sign, and `*_ore` every ore. * Oasis reads a pattern in lower case, so `OAK_LOG` works too. A value may hold only letters, digits, `_`, `-`, `.`, `/` and `*`, with at most one `:` between the namespace and the path. Anything else, such as a space or a `#`, throws `IllegalArgumentException`. ## Leaving edits out [Section titled “Leaving edits out”](#leaving-edits-out) `not()` returns the parts a selection leaves out. Each method takes the same values as the method of the same name above, leaves them out, and returns the selection. | Method | Leaves out | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | `not().by(OfflinePlayer...)`, `not().by(UUID...)`, `not().by(Actor...)` | The edits of these people or actors, and every chain they started or carried. | | `not().in(World...)`, `not().in(String...)` | Edits in these worlds. | | `not().kinds(Kind...)` | Edits of these kinds. | | `not().causes(Cause...)` | Edits with these causes. | | `not().materials(Material...)` | Edits of these blocks or items. | | `not().entities(EntityType...)` | Edits of these entities. | | `not().matching(String...)` | Edits whose id matches one of these patterns. | `not().by(x)` is the exact inverse of `by(x)`: it leaves out what `x` did and the fire and water `x` set going. Each call replaces its part, as the rest of the selection does. `not().by(steve).not().by(alex)` leaves out Alex only. `not().materials`, `not().entities` and `not().matching` share one part.
```java
// Everything a player broke or placed, but not their signs.
Selection handWork = Selection.all()
.by(builder)
.causes(Cause.BREAKING, Cause.PLACING)
.not().matching("*_sign");
// Everything near a location, except what one builder did.
Selection others = Selection.all().near(center, 20).not().by(builder);
```
## An empty list throws [Section titled “An empty list throws”](#an-empty-list-throws) Every method that takes a list of values throws `IllegalArgumentException` when the list is empty. That covers every `by`, `in`, `kinds`, `causes`, `materials`, `entities` and `matching`, and every one of them after `not()`. This protects you from a costly mistake. A part you never set chooses everything, so if an empty list meant “no filter”, it would choose every edit on the server. Picture a plugin that collects the griefers in a region, finds none, and rolls back `by()` of that empty list. It would roll back the whole server. So check your list before you pass it:
```java
public void rollBackGriefers(List griefers, Location center)
{
if (griefers.isEmpty())
{
return;
}
oasis.operations()
.rollback(Selection.all().by(griefers.toArray(UUID[]::new)).near(center, 50))
.start();
}
```
To choose every player, leave the part unset. ## A selection that matches nothing [Section titled “A selection that matches nothing”](#a-selection-that-matches-nothing) A selection that cannot match any edit by how it is built throws `IllegalArgumentException` at the call that makes it, in either order of the calls: * A person, world, kind, cause or material is both chosen and left out. * `near`, `box`, `at` or `in(name, box)` names a world that is left out. * `since` is after `until`. ## Every rule that throws [Section titled “Every rule that throws”](#every-rule-that-throws) Each of these throws `IllegalArgumentException` when you build the selection, before anything reads the store: | Method | Throws when | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `by(...)`, all three, and `not().by(...)` | No player, id or actor is named, or a person is both chosen and left out. | | `in(...)`, and `not().in(...)` | No world is named, or a world is both chosen and left out. | | `kinds(...)`, `not().kinds(...)` | No kind is named, or a kind is both chosen and left out. | | `causes(...)`, `not().causes(...)` | No cause is named, or a cause is both chosen and left out. | | `materials(...)`, `not().materials(...)` | No material is named, or a material is both chosen and left out. | | `entities(...)`, `not().entities(...)` | No entity type is named, or a type is both chosen and left out. | | `matching(...)`, `not().matching(...)` | No pattern is named, a value is not an id or a pattern of one, or an id is both chosen and left out. | | `near(center, radius)` | The radius is negative. | | `box(corner, opposite)` | The corners are in two worlds. | | `near`, `box`, `at`, `in(name, box)` | The world is left out. | | `since(time)`, `until(time)` | `since` is after `until`. | | `new Selection.Box(...)` | A lowest bound is above its highest. | The message of each exception says what was wrong and how to fix it, so you can show it to the person who typed the value. ## Reading a selection back [Section titled “Reading a selection back”](#reading-a-selection-back) A selection tells you what it chooses. Each getter returns an empty set or an empty `Optional` for a part you never set. | Getter | Returns | | --------------------- | -------------------------------------------------------------------------------------- | | `people()` | The ids chosen, or empty for everyone. | | `since()` | The time set with `since`. Empty when unset, and empty while a `within` window is set. | | `within()` | The window set with `within`, or empty. | | `until()` | The time set with `until`, or empty. | | `worlds()` | The stored world names, or empty for every world. | | `box()` | The box inside the one world of `worlds()`, or empty for whole worlds. | | `kinds()`, `causes()` | The kinds or causes chosen, or empty for all. | | `materials()` | The namespaced ids and patterns chosen, or empty for all. | | `left()` | What the selection leaves out, as a `Selection.Left`. | `left()` returns `Selection.Left(people, worlds, kinds, causes, materials)`. Each is a set, empty when nothing of that part is left out. `people` holds player ids and `Actor.id()` ids. `materials()` and `left().materials()` hold namespaced ids. A `Material.STONE` reads back as `minecraft:stone`, and a pattern `*_sign` as `minecraft:*_sign`.
# Reading history
> Read pages of edits with find, read the history of one block, and learn every field of an Edit.
`oasis.lookups()` reads history, newest first, a page at a time. It has three ways in: `find` reads the edits a [selection](/docs/api/selections/) chooses, `history` reads the history of one block, as the inspector shows it, and `kept` lists the positions an operation kept. ## Find edits [Section titled “Find edits”](#find-edits)
```java
oasis.lookups().find(Selection.all().by(player).within(Duration.ofHours(1)))
.thenAccept(page ->
{
for (Edit edit : page.items())
{
viewer.sendMessage(edit.cause().word() + " " + edit.after());
}
});
```
`find(selection)` returns the first page, with `Lookups.PAGE_SIZE` edits to a page. That is 8, the size staff read in chat. `find(selection, size)` takes a page size of your own, such as 45 for a chest inventory. A size below 1 throws `IllegalArgumentException`. The future completes on the main thread, so you can send messages and open inventories in the callback. ## Turn pages [Section titled “Turn pages”](#turn-pages) A `Page` has three methods. `find` and `history` return a `Page`, and `kept` returns a `Page`. | Method | Returns | | ----------- | ---------------------------------------------------------------------- | | `items()` | The items on this page, newest first for edits. The list may be empty. | | `hasNext()` | True when another page follows this one. | | `next()` | A future of the page after this one. It completes on the main thread. | `next()` throws `java.util.NoSuchElementException` when `hasNext()` is false, so check first:
```java
private void showAll(Player viewer, Page page)
{
for (Edit edit : page.items())
{
viewer.sendMessage(edit.cause().word() + " " + edit.after());
}
if (page.hasNext())
{
page.next().thenAccept(next -> showAll(viewer, next));
}
}
```
Caution This loop reads every page, one after the other, which is fine for a small selection. For a selection that could hold millions of edits, read a page when the person asks for it, as in the [example below](#example-a-paged-history-command). `next()` reads on from the last edit of the page. Turning a page costs one page, however deep you are. A page keeps its place, so an edit that someone makes while you read does not push edits onto the next page. To see new edits, start again with `find`. A page has no total and no page number. See [Pages carry no totals](/docs/api/concepts/#pages-carry-no-totals) for the reason. When you want to show “more” without a count, `hasNext()` is all you need. ## The history of one block [Section titled “The history of one block”](#the-history-of-one-block)
```java
oasis.lookups().history(block).thenAccept(page ->
{
for (Edit edit : page.items())
{
String mark = edit.current() ? " (stands now)" : "";
viewer.sendMessage(edit.cause().word() + " " + edit.after() + mark);
}
});
```
`history(block)` reads the history of one block the way the inspector shows it. It holds only the [kinds](/docs/api/reference/#kind) that describe the world at a place: blocks, containers, entities and items. It leaves out books, chat, commands, logins and logouts. `history(block, size)` takes a page size of your own, and a size below 1 throws. `history(BlockPosition)` reads the same history for a position whose world need not be loaded, such as the `position()` of an edit you read. `history(position, size)` takes a page size of your own. `history` marks the edit that stands at the block now with `current()`. At most one edit in all its pages has the mark. `Selection.all().at(block)` with `find` is different: it reads every kind at the block, chat included, and marks no edit as current. ## The positions an operation kept [Section titled “The positions an operation kept”](#the-positions-an-operation-kept) An operation leaves a position standing when a later edit by another person holds it. `kept(operation)` lists those positions, as `/oasis kept` does, a page at a time:
```java
oasis.lookups().kept(operation.id().getAsLong()).thenAccept(page ->
{
for (Kept kept : page.items())
{
BlockPosition at = kept.position();
viewer.sendMessage(at.x() + " " + at.y() + " " + at.z() + " is held by " + name(kept.keeper()));
}
});
```
A `Kept` holds the `position()`, the `keeper()` (the person whose later edit holds the position: a player id or an `Actor.id()`), and `holding()`, the newest edit at the position. `holding()` is empty when a purge deleted the edits there. Any pass id of the operation’s group names it: the id of the operation, of a sweep, or of a force. After a force, the list holds what the force kept. An id that the history does not hold fails with `NO_SUCH_OPERATION`. `kept(operation, size)` takes a page size of your own, and a size below 1 throws. [Kept, left and force](/docs/api/operations/#kept-left-and-force) explains how an operation keeps a position. ## When a read fails [Section titled “When a read fails”](#when-a-read-fails) A read can fail when the store can’t answer. Then the future of `find`, `history`, `kept`, `Page.next()` or [`estimate()`](/docs/api/operations/#estimate-and-confirm) completes exceptionally with an `OasisException`. It still completes on the main thread. `OasisException` is a `RuntimeException` that carries a [`Failure`](/docs/api/reference/#failurereason), the same type a failed operation carries. Test `failure().reason()` in code, and show `failure().message()` to people. Its cause, when it has one, is the error Oasis met, for your server log. A read fails with one of these reasons: | Reason | What happened | What to do | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `STARTING` | Oasis still opens its store, just after the server started. | Ask again shortly. | | `OVERLOADED` | The store holds as many requests as it takes. | Ask again shortly. | | `MOVING` | The history moves to another store with [`/oasis move`](/docs/packs-server/setup/#4-bring-your-history-along). | Ask again when the move ends. | | `STORE` | The store failed. | Tell staff. The server log names the error. | | `NOTHING_TO_UNDO` | Only for an estimate of `undoLast`: the person has run no operation. | Tell the person there is nothing to undo. | | `NO_SUCH_OPERATION` | For `kept`, and for an estimate of `undo`: the history does not hold that operation. | Check the id you passed. | | `NO_SUCH_WORLD` | The selection names a world, chosen or left out, that is not loaded under that exact name, that Oasis never saw, and that the store holds no edits of. Most often a typo. | Check the name. Names are exact, as `World.getName()` reads them. | A callback of `whenComplete` or `exceptionally` on the future that Oasis returns gets the `OasisException` itself. A callback on a stage further down the chain, such as after a `thenApply`, gets it wrapped in a `CompletionException`. This helper handles both:
```java
/** The OasisException behind an error of an Oasis future, or null for any other error. */
private static OasisException oasisFailure(Throwable error)
{
Throwable cause = error instanceof CompletionException && error.getCause() != null ? error.getCause() : error;
return cause instanceof OasisException failed ? failed : null;
}
```
```java
oasis.lookups().find(selection).whenComplete((page, error) ->
{
if (error == null)
{
showAll(viewer, page);
return;
}
OasisException failed = oasisFailure(error);
if (failed == null)
{
getLogger().log(Level.WARNING, "The lookup failed", error);
return;
}
switch (failed.failure().reason())
{
case STARTING, OVERLOADED -> viewer.sendMessage(failed.failure().message() + " Try again in a moment.");
case MOVING -> viewer.sendMessage(failed.failure().message() + " Try again when the move ends.");
default ->
{
viewer.sendMessage("The lookup failed: " + failed.failure().message());
getLogger().log(Level.WARNING, "The lookup failed", failed);
}
}
});
```
A size below 1 and a `next()` with no next page throw at once, from the call itself. They never reach the future. ## The fields of an edit [Section titled “The fields of an edit”](#the-fields-of-an-edit) An `Edit` is a record. Each field is one of these: | Field | Type | What it holds | | ------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `time()` | `Instant` | When the edit happened. | | `person()` | `UUID` | Who is behind the edit: the player who acted or started the chain, or the id of the `Actor` that acted when no player is. | | `actor()` | `Optional` | The world actor that carried the edit, such as `Actor.LAVA`. Empty for an edit the person made directly. A world actor with nobody behind it is both the person and the actor. | | `position()` | `BlockPosition` | Where the edit happened: the stored world name, `x`, `y` and `z`. | | `kind()` | `Kind` | What changed: a block, a container, an entity, and so on. | | `cause()` | `Cause` | Why it changed, such as `Cause.BREAKING` or `Cause.FLOW`. | | `before()` | `String` | The block or entity before the edit, or an empty string. | | `after()` | `String` | The block or entity after the edit, or an empty string. | | `item()` | `Optional` | The items that moved, or empty for an edit that moved none. | | `text()` | `String` | The chat message, the command, or the book text, or an empty string. | | `reversed()` | `boolean` | True when a rollback, a restore or an undo has put the position back. | | `current()` | `boolean` | True on the edit that stands at the block now. Only `history` sets it. | | `operation()` | `OptionalLong` | The pass of a rollback, a restore or an undo that wrote this edit, or empty. | The person and the actor are explained in [Core concepts](/docs/api/concepts/#the-person-and-the-actor). To turn a person into a name, use `oasis.key(edit.person())` for an actor or a plugin, and Bukkit for a player:
```java
private String name(UUID person)
{
return oasis.key(person).orElseGet(() ->
{
String player = Bukkit.getOfflinePlayer(person).getName();
return player == null ? person.toString() : player;
});
}
```
### before and after [Section titled “before and after”](#before-and-after) What `before()` and `after()` hold depends on the kind: | Kind | `before()` and `after()` | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BLOCK` | The block data as a string, such as `minecraft:stone` or `minecraft:oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=false]`. A broken block has `minecraft:air` after it. | | `ENTITY` | The namespaced entity id, such as `minecraft:zombie`. An entity that appeared has an empty `before()`. An entity that went away has an empty `after()`. | | `CONTAINER` and `ITEM` | Empty strings. `item()` says what moved. | | `CHAT`, `COMMAND` and `BOOK` | Empty strings. `text()` holds the message, the command or the book text. | `Bukkit.createBlockData(edit.after())` turns a block string back into `BlockData`, if you want to show or compare it. ### Items and their direction [Section titled “Items and their direction”](#items-and-their-direction) `item()` holds an `ItemChange` with two fields: `type()`, the namespaced item id such as `minecraft:diamond`, and `amount()`, how many moved. The amount is always positive. The cause says which way the items went: | Cause | Direction | | ---------- | ----------------------------------------------- | | `TAKING` | Out of a container. | | `PUTTING` | Into a container. | | `DROPPING` | Out of the player’s inventory, onto the ground. | | `PICKUP` | From the ground, into the player’s inventory. | ### reversed and current [Section titled “reversed and current”](#reversed-and-current) `reversed()` is true when a rollback, a restore or an undo has put the position back. A rolled-back grief reads as reversed, and the inspector shows it crossed out. After a rollback, the history of the block also holds the edit Oasis wrote to put the block back. That edit has `operation()` set. `current()` marks the one edit whose result stands at the block now. On a page of `find`, it is always false. ### operation [Section titled “operation”](#operation) `operation()` holds an id when Oasis wrote the edit itself, for a rollback, a restore or an undo. It is the id of the pass that wrote the edit: the first pass, a [sweep](/docs/api/operations/#sweeps), or a [force](/docs/api/operations/#kept-left-and-force). It is empty for an edit that Oasis did not write for an operation. Pass any of those ids to [`undo(id)`](/docs/api/operations/#undo), and Oasis reverses the whole operation: its first pass, its sweeps and its force. ## Example: a paged history command [Section titled “Example: a paged history command”](#example-a-paged-history-command) This plugin adds `/history `, which shows what a player did in the last seven days, a page at a time. `/history more` shows the next page. It keeps the last page each viewer read, and reads the next one only when the viewer asks.
```java
package com.example.history;
import com.oasis.api.Edit;
import com.oasis.api.Failure;
import com.oasis.api.Oasis;
import com.oasis.api.OasisApi;
import com.oasis.api.OasisException;
import com.oasis.api.Page;
import com.oasis.api.Selection;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
public final class HistoryPlugin extends JavaPlugin
{
private static final int PAGE_SIZE = 10;
private final Map> lastPage = new HashMap<>();
private OasisApi oasis;
@Override
public void onEnable()
{
oasis = Oasis.api(this);
registerCommand("history", "Show what a player did, a page at a time", new HistoryCommand());
}
private final class HistoryCommand implements BasicCommand
{
@Override
public void execute(CommandSourceStack source, String[] args)
{
if (!(source.getSender() instanceof Player viewer))
{
source.getSender().sendMessage("Only players can read history pages.");
return;
}
if (args.length != 1)
{
viewer.sendMessage(Component.text("Usage: /history or /history more", NamedTextColor.RED));
return;
}
if (args[0].equalsIgnoreCase("more"))
{
Page page = lastPage.get(viewer.getUniqueId());
if (page == null || !page.hasNext())
{
viewer.sendMessage(Component.text("There is no next page.", NamedTextColor.GRAY));
return;
}
page.next().whenComplete((next, error) -> show(viewer, next, error));
return;
}
OfflinePlayer target = Bukkit.getOfflinePlayerIfCached(args[0]);
if (target == null)
{
viewer.sendMessage(Component.text("No player called " + args[0] + " has played here.",
NamedTextColor.RED));
return;
}
Selection selection = Selection.all().by(target).within(Duration.ofDays(7));
oasis.lookups().find(selection, PAGE_SIZE).whenComplete((page, error) -> show(viewer, page, error));
}
@Override
public String permission()
{
return "history.use";
}
}
/** Runs on the main thread, because every Oasis future completes there. */
private void show(Player viewer, Page page, Throwable error)
{
if (!viewer.isOnline())
{
return;
}
if (error != null)
{
// The future comes straight from Oasis, so the error is the OasisException itself.
String why = error instanceof OasisException failed ? failed.failure().message() : "an error, see the log";
viewer.sendMessage(Component.text("The lookup failed: " + why, NamedTextColor.RED));
if (!(error instanceof OasisException failed) || failed.failure().reason() == Failure.Reason.STORE)
{
getLogger().log(Level.WARNING, "A history lookup failed", error);
}
return;
}
lastPage.put(viewer.getUniqueId(), page);
if (page.items().isEmpty())
{
viewer.sendMessage(Component.text("No edits found.", NamedTextColor.GRAY));
return;
}
for (Edit edit : page.items())
{
viewer.sendMessage(line(edit));
}
if (page.hasNext())
{
viewer.sendMessage(Component.text("[Next page]", NamedTextColor.AQUA)
.clickEvent(ClickEvent.runCommand("/history more")));
}
}
private Component line(Edit edit)
{
Duration age = Duration.between(edit.time(), Instant.now());
String who = name(edit.person()) + edit.actor().map(actor -> " via " + actor.key()).orElse("");
String where = edit.position().x() + " " + edit.position().y() + " " + edit.position().z();
Component text = Component.text(ago(age) + " " + who + " " + edit.cause().word() + " " + what(edit)
+ " at " + where);
// Oasis crosses out what a rollback put back. Do the same.
return edit.reversed() ? text.decorate(TextDecoration.STRIKETHROUGH) : text;
}
private String what(Edit edit)
{
return switch (edit.kind())
{
case BLOCK, ENTITY -> shortId(edit.after().isEmpty() ? edit.before() : edit.after());
case CONTAINER, ITEM -> edit.item().map(item -> item.amount() + " " + shortId(item.type())).orElse("");
default -> edit.text();
};
}
/** minecraft:oak_stairs[facing=north] reads as oak_stairs. */
private static String shortId(String id)
{
int bracket = id.indexOf('[');
String plain = bracket < 0 ? id : id.substring(0, bracket);
return plain.startsWith("minecraft:") ? plain.substring("minecraft:".length()) : plain;
}
private static String ago(Duration age)
{
if (age.toDays() > 0)
{
return age.toDays() + "d";
}
if (age.toHours() > 0)
{
return age.toHours() + "h";
}
return Math.max(1, age.toMinutes()) + "m";
}
private String name(UUID person)
{
return oasis.key(person).orElseGet(() ->
{
String player = Bukkit.getOfflinePlayer(person).getName();
return player == null ? person.toString() : player;
});
}
}
```
A few things to note: * The command reads one page per request. Nothing reads ahead, and nothing counts. * `show` checks `isOnline()`, because the viewer can leave before the page arrives. * `lastPage` keeps one `Page` per viewer. A page holds its edits and its place, so it is cheap to keep. Remove a viewer’s entry in a `PlayerQuitEvent` listener if your server has many viewers.
# Logging edits
> Log the blocks, items and entities your plugin changes, so staff can look them up and roll them back.
Oasis logs what players and the world do by listening to Bukkit events. When your plugin changes the world itself, with `setBlockData`, `setType`, `spawn` or an inventory call, no event fires and Oasis doesn’t see it. Log those edits through the API, and staff can look them up, inspect them and roll them back like any other.
```java
EditLog log = oasis.logger().by(player);
BlockState before = block.getState();
block.setType(Material.GOLD_BLOCK);
log.block(Cause.PLACING, before, block.getState());
```
## Who made the edit [Section titled “Who made the edit”](#who-made-the-edit) Start with the person behind the edit. `oasis.logger()` returns an `EditLogger` with three ways in: | Method | Logs the edits of | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `by(OfflinePlayer player)` | This player. | | `by(UUID person)` | This person: a player id, or an `Actor.id()` when no player is behind the edit. An actor’s id works as `by(Actor)` does. | | `by(Actor actor)` | The world, with no person behind the edit. | Each returns an `EditLog`. An `EditLog` is immutable, so keep it and reuse it for as many edits as you like. With `by(Actor.LAVA)`, the actor is both the person and the actor of the edit, as for the lava Oasis logs itself. You read it back with `edit.person()` as `Actor.LAVA.id()`, and `edit.actor()` holding `Actor.LAVA`. The same holds for every actor where `isWorld()` is true. `Actor.CONSOLE`, `Actor.WORLDEDIT` and `Actor.ROLLBACK` act as a person and carry nothing, so their edits read back with an empty `actor()`. ### Edits the world carried [Section titled “Edits the world carried”](#edits-the-world-carried) When the world carried the edit on for the person, such as lava that flowed from a bucket they poured, add the actor with `via`:
```java
oasis.logger().by(player).via(Actor.LAVA).block(Cause.FLOW, at, air, lava);
```
`via(actor)` returns the same person with that actor beside them. A lookup names the person, with the actor beside them, and a rollback of the person takes the edit too. See [The person and the actor](/docs/api/concepts/#the-person-and-the-actor). `Edit.actor()` reads the actor back. `via` takes only an actor where `isWorld()` is true. It throws `IllegalArgumentException` for `Actor.WORLDEDIT`, `Actor.CONSOLE` and `Actor.ROLLBACK`, because those act as a person and carry nothing. To log an edit of the console, use `by(Actor.CONSOLE)`. ## Blocks [Section titled “Blocks”](#blocks) `EditLog` has four `block` methods. They differ in which side of the edit carries block entity data, such as a sign’s text or a chest’s items. | Method | Use it when | | -------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `block(cause, BlockState before, BlockState after)` | Both sides may carry block entity data, or you don’t know. The safe default. | | `block(cause, BlockState before, BlockData after)` | A block with data became one without, such as a chest that was broken. | | `block(cause, BlockData before, BlockState after)` | A block with data took the place of one without, such as a chest that was placed. | | `block(cause, Location at, BlockData before, BlockData after)` | Neither side carries block entity data. Safe from any thread. | Take a `BlockState` with `block.getState()`: before the change for `before`, and after it for `after`. Oasis reads the block entity data of the state into the row, so a rollback brings a broken chest back full, and a restore puts the text back on a sign.
```java
// A chest that your plugin breaks. The row keeps its items.
BlockState chest = block.getState();
block.setType(Material.AIR);
log.block(Cause.BREAKING, chest, Material.AIR.createBlockData());
// A chest that your plugin places and fills.
BlockData air = block.getBlockData();
block.setType(Material.CHEST);
log.block(Cause.PLACING, air, block.getState());
```
`block(cause, BlockState, BlockState)` throws `IllegalArgumentException` when the two states are not of one block. ## Containers [Section titled “Containers”](#containers) | Method | Logs | | --------------------------------------------------- | ---------------------------------------------------------------------- | | `taken(Block container, ItemStack items)` | Items taken out of a container. A rollback puts them back. | | `taken(Block container, int slot, ItemStack items)` | Items taken out of one slot. A rollback puts them back into that slot. | | `put(Block container, ItemStack items)` | Items put into a container. A rollback takes them out again. | | `put(Block container, int slot, ItemStack items)` | Items put into one slot. A rollback takes them out of that slot. | The amount is the size of the `ItemStack`. Oasis reads the whole item, its name, enchantments and other data included, so a rollback puts back the same item.
```java
// A shop plugin takes the payment out of the first slot of the shop chest.
Inventory inventory = ((Container) block.getState(false)).getInventory();
ItemStack payment = inventory.getItem(0);
if (payment != null)
{
inventory.setItem(0, null);
oasis.logger().by(buyer).taken(block, 0, payment);
}
```
Pass a slot when you know it. A rollback then puts the items back where they were. The cause of the row is `Cause.TAKING` or `Cause.PUTTING`. ## Items dropped and picked up [Section titled “Items dropped and picked up”](#items-dropped-and-picked-up) | Method | Logs | | ---------------------------------------- | --------------------------- | | `dropped(Location at, ItemStack items)` | Items the person dropped. | | `pickedUp(Location at, ItemStack items)` | Items the person picked up. | Staff can look these up, and a rollback does not change them. Their causes are `Cause.DROPPING` and `Cause.PICKUP`. ## Entities [Section titled “Entities”](#entities) | Method | Logs | | ------------------------------------- | ------------------------------------------------------------------------------------------- | | `spawned(Cause cause, Entity entity)` | An entity appeared. A rollback removes it. Call it once the entity is in the world. | | `removed(Cause cause, Entity entity)` | An entity went away. A rollback puts it back as it was. Call it before it leaves the world. | Oasis reads the whole entity, so a rollback brings back a removed entity with its data, such as an armor stand with its armor.
```java
ArmorStand stand = world.spawn(at, ArmorStand.class);
log.spawned(Cause.SPAWNING, stand);
// Later: log it before it goes, while Oasis can still read it.
log.removed(Cause.BREAKING, stand);
stand.remove();
```
## Chat and commands [Section titled “Chat and commands”](#chat-and-commands) | Method | Logs | | -------------------------------------- | ---------------------------------- | | `chat(Location at, String message)` | A chat message. | | `command(Location at, String command)` | A command, with its leading slash. | Staff can look these up. They are not part of the history of a block, and a rollback does not change them. Log them when your plugin takes messages or commands that never reach Bukkit’s own events, such as messages from a Discord bridge or a custom chat channel. ## Threads [Section titled “Threads”](#threads) Every logger method returns at once and never waits for the disk. You may call the logger from any thread. Some methods read a Paper object on the thread that calls, so call those where the object may be read: | Method | Reads | Call it from | | ---------------------------------------------- | ----------------------------------- | ----------------------------------------------- | | `block(cause, Location, BlockData, BlockData)` | Nothing from the world. | Any thread. | | `block` with a `BlockState` | The block entity data of the state. | The main thread, for a block of a loaded chunk. | | `taken`, `put`, `dropped`, `pickedUp` | The `ItemStack`. | A thread that may read the item. | | `spawned`, `removed` | The whole entity. | The main thread, for a live entity. | | `chat`, `command` | Nothing from the world. | Any thread. | Oasis takes an edit you log on the main thread at once. It takes an edit you log on another thread on the next tick, in the order you logged it. ## Causes only Oasis writes [Section titled “Causes only Oasis writes”](#causes-only-oasis-writes) `Cause.ROLLBACK`, `Cause.UNDO` and `Cause.REDO` belong to Oasis. It writes them for its own operations and for a FAWE undo or redo. Every method that takes a cause throws `IllegalArgumentException` for them, before it reads anything. A row with one of those causes reads as an edit Oasis made, and a rollback treats it as one. If your plugin reverts its own work, log what it put back with an ordinary cause, such as `PLACING` for a block it put back. ## Choosing a cause [Section titled “Choosing a cause”](#choosing-a-cause) The API has no “plugin” cause. Pick the cause that says what happened in the world, as staff would type it after `cause`: * A block your plugin puts in the world: `PLACING`. * A block your plugin takes away: `BREAKING`. * A block that grows, melts or spreads through your plugin: `GROWTH`, `MELTING`, `SPREADING`. * An entity your plugin spawns: `SPAWNING`. A mob it kills: `KILLING`. An armor stand or a vehicle it breaks: `BREAKING`. [Vocabulary](/docs/api/reference/#cause) lists every cause. ## Edits that Oasis drops [Section titled “Edits that Oasis drops”](#edits-that-oasis-drops) Oasis drops an edit quietly, with no error and no message, when: * the server’s `config.yml` turns its cause off under [`logging.events`](/docs/reference/config/#logging-events), * its world is in [`logging.ignored-worlds`](/docs/reference/config/#logging-ignored-worlds), * or Oasis is not enabled. That is the same rule Oasis applies to the edits it logs itself, so server owners control your plugin’s edits with the settings they already know. There is no way to read whether an edit was dropped. Caution Don’t change the world yourself to undo an edit that Oasis logged. Start a [rollback](/docs/api/operations/) instead, so it keeps to the tick, logs what it puts back, and staff can undo it. ## Example: a builder’s wand [Section titled “Example: a builder’s wand”](#example-a-builders-wand) This plugin adds a builder’s wand. A player right-clicks a block face with a blaze rod, and the wand copies the clicked block onto every empty spot in front of the 3 by 3 area of that face, wherever the same block already is. Each block the wand places goes into the history under the player’s name, so a rollback of that player takes it away.
```java
package com.example.wand;
import com.oasis.api.Cause;
import com.oasis.api.EditLog;
import com.oasis.api.Oasis;
import com.oasis.api.OasisApi;
import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
public final class BuilderWand extends JavaPlugin implements Listener
{
private OasisApi oasis;
@Override
public void onEnable()
{
oasis = Oasis.api(this);
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler
public void onUse(PlayerInteractEvent event)
{
ItemStack held = event.getItem();
Block clicked = event.getClickedBlock();
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getHand() != EquipmentSlot.HAND
|| held == null || held.getType() != Material.BLAZE_ROD || clicked == null)
{
return;
}
event.setCancelled(true);
Player player = event.getPlayer();
int placed = extend(player, clicked, event.getBlockFace());
player.sendActionBar(Component.text("Placed " + placed + " blocks"));
}
/** Copies the clicked block onto the face of the 3 by 3 area around it, and logs each block. */
private int extend(Player player, Block clicked, BlockFace face)
{
EditLog log = oasis.logger().by(player);
BlockData copy = clicked.getBlockData();
int placed = 0;
for (int a = -1; a <= 1; a++)
{
for (int b = -1; b <= 1; b++)
{
Block source = inPlane(clicked, face, a, b);
Block target = source.getRelative(face);
if (source.getType() != copy.getMaterial() || !target.isEmpty())
{
continue;
}
// Take the state before and after the change, so the row holds both sides.
BlockState before = target.getState();
target.setBlockData(copy);
log.block(Cause.PLACING, before, target.getState());
placed++;
}
}
return placed;
}
/** The block at (a, b) in the plane of the face, around the clicked block. */
private static Block inPlane(Block center, BlockFace face, int a, int b)
{
if (face.getModY() != 0)
{
return center.getRelative(a, 0, b);
}
if (face.getModX() != 0)
{
return center.getRelative(0, a, b);
}
return center.getRelative(a, b, 0);
}
}
```
The wand runs in an event handler, on the main thread, so it can use the `BlockState` form. A wand that copied plain blocks only could log with `block(Cause.PLACING, target.getLocation(), before, after)` and two `BlockData` values instead. Staff then find the wand’s work with `/lookup user Steve cause placing`, and `/rollback user Steve last 10m` takes it away.
# Running operations
> Run rollbacks, restores and undos. Build a request, estimate it, start it, follow its progress, cancel it, and read the result.
An operation changes the world back. It is the same thing staff run with `/rollback`, `/rs` and `/undo`, with the same speed, the same care for later edits, and the same history afterwards. `oasis.operations()` has one method for each: | Method | Does | | ----------------------------------- | ---------------------------------------------------------------------------------- | | `rollback(Selection selection)` | Puts back what the chosen edits changed, as it stood before them. | | `restore(Selection selection)` | Puts the chosen edits back after a rollback took them away. | | `undo(long operation)` | Reverses this operation, and the sweeps and the force that followed it, as one. | | `undoLast(UUID requester)` | Reverses the last operation this person asked for, as `/oasis undo` does for them. | | `undoLast(CommandSender requester)` | The same for a sender: a player, or the console for any other sender. | Each returns an `OperationRequest`: an operation you have not started yet. The `Operation` it starts, and the events it fires, name the verb as an [`OperationType`](/docs/api/reference/#operationtype): `ROLLBACK`, `RESTORE` or `UNDO`. A sweep and a force have the type of the operation they follow.
```java
oasis.operations().rollback(Selection.all().by(griefer).within(Duration.ofHours(2)))
.requestedBy(staff)
.audience(staff)
.start()
.result().thenAccept(result -> staff.sendMessage("Rolled back " + result.applied() + " edits."));
```
## The request [Section titled “The request”](#the-request) A request is immutable. Each method returns a new request, so you can keep one in a field and start it as often as you like. | Method | Sets | Default | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `requestedBy(UUID person)` | Who asked. Staff read this name in lookups, and `undoLast` and `/oasis undo` reverse the last operation of this person. | Your plugin, `#plugin:`. | | `requestedBy(CommandSender sender)` | Who asked: the player, or `Actor.CONSOLE` for any other sender. | | | `audience(Audience audience)` | Who sees it run: the start line, the progress bar and the summary, as a command shows them. | Nobody. | | `targetTick(Duration tick)` | The longest tick the operation may leave the server with. A longer tick finishes sooner, and the players feel it. A zero or negative duration throws `IllegalArgumentException`. | The config’s [`rollback.target-tick-ms`](/docs/reference/config/#rollback-target-tick-ms). | | `estimate()` | Nothing. It reads how large the operation is. See [Estimate and confirm](#estimate-and-confirm). | | | `start()` | Nothing. It starts the operation now and returns an `Operation`. | | Name the staff member who asked with `requestedBy`, so the history says who asked for the rollback, and that staff member can `/oasis undo` it. Any `Audience` works: a player, a `CommandSender`, the console, or `Audience.audience(staff, Bukkit.getConsoleSender())` for several at once. The audience also gets the force link of the summary, as a staff member does after a command. ## Estimate and confirm [Section titled “Estimate and confirm”](#estimate-and-confirm) Oasis never asks for a confirmation when you start an operation through the API. When your plugin wants to ask one, call `estimate()` first:
```java
OperationRequest request = oasis.operations()
.rollback(Selection.all().by(griefer).within(Duration.ofHours(2)))
.requestedBy(staff);
request.estimate().thenAccept(estimate ->
staff.sendMessage("This rolls back " + estimate.edits() + " edits in " + estimate.chunks()
+ " chunks, in about " + estimate.duration().toSeconds() + " seconds."));
```
An `Estimate` has three fields: | Field | Type | Holds | | ------------ | ---------- | -------------------------------------------------------------------------------- | | `edits()` | `long` | The edits the operation chooses. | | `chunks()` | `int` | The chunks those edits are in. | | `duration()` | `Duration` | How long it takes, at the rate this server has measured, within its target tick. | The estimate reads history and changes nothing. It is the one place in the API that reads a whole selection, so ask for it only when you show it to someone. The operation decides as it runs which of the chosen edits it writes and which a later edit holds, so `edits()` counts what it chooses, not what it writes. The [result](#the-result) splits those edits into `applied()`, `kept()` and `left()`, plus what is `incomplete()`. A sweep can add edits that the estimate could not see. The estimate of a [`forceKept()`](#kept-left-and-force) request is different. It names the kept positions and the chunks they lie in, as the operation that kept them counted them, and reads no history. When the store can’t answer, a world of the selection does not exist, or an undo names nothing to undo, the estimate future completes exceptionally with an `OasisException`. [When a read fails](/docs/api/reading-history/#when-a-read-fails) lists its reasons and shows how to handle it. See [Example: /griefrevert](#example-griefrevert) for a full confirmation flow. ## Start and follow an operation [Section titled “Start and follow an operation”](#start-and-follow-an-operation) `start()` returns an `Operation` at once. The pass begins on a later tick. | Method | Returns | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `id()` | The id of the operation, once history has recorded it: the id of its first pass. Empty before that. Pass it to `undo`. | | `type()` | What the operation does: `OperationType.ROLLBACK`, `RESTORE` or `UNDO`. | | `progress()` | Where the operation is now, as a `Progress`. | | `onProgress(Consumer