# Reading history

> Read pages of edits with find, read the history of one block, and learn every field of an Edit.

Source: https://oasis-protection.com/docs/api/reading-history/

`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

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

A `Page<T>` has three methods. `find` and `history` return a `Page<Edit>`, and `kept` returns a `Page<Kept>`.

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

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

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

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

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

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

`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

`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

`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

This plugin adds `/history <player>`, 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<UUID, Page<Edit>> 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 <player> or /history more", NamedTextColor.RED));
                return;
            }
            if (args[0].equalsIgnoreCase("more"))
            {
                Page<Edit> 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<Edit> 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.
