# Logging edits

> Log the blocks, items and entities your plugin changes, so staff can look them up and roll them back.

Source: https://oasis-protection.com/docs/api/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.

```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

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

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

`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

| 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

| 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

| 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

| 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

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

`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

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

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

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.
