Logging edits
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.
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”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”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:
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.
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”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.
// 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”| 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.
// 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”| 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”| 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.
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”| 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”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”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”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 lists every cause.
Edits that Oasis drops
Section titled “Edits that Oasis drops”Oasis drops an edit quietly, with no error and no message, when:
- the server’s
config.ymlturns its cause off underlogging.events, - its world is in
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.
Example: a builder’s wand
Section titled “Example: a builder’s 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.
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.