> ## Documentation Index
> Fetch the complete documentation index at: https://docs.marioded.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Work with recordings

> List recordings, check ownership, edit metadata, and load the data you actually need.

# Work with recordings

Use the **catalog** for user-facing discovery and access checks. Use **storage** when you need the full event data. Keeping those jobs separate makes your integration easier to reason about.

## Three models, three jobs

Use `RecordSession` while capture is active. Use `Replay` when presenting a catalog entry. Use `LoadedReplay` when you actually need the event stream.

| Model | Intended use |
| - | - |
| `RecordSession` | Identity and capture selection of a live session, immutable snapshots of captured data, snapshot/capture bookkeeping. |
| `Replay` | Metadata such as ID, title, description, owners, source world ID, terrain identifier, start time and recorded duration. |
| `LoadedReplay` | Replay metadata plus an immutable, ascending-tick event map with immutable per-tick lists. |

Do not load every replay to draw a page of titles. Catalog paging is designed for that metadata-only use case. Do not modify a `LoadedReplay` event collection in place; running sessions may share that event stream.

## Identity and duration

* Recording session UUID identifies the replay it produces; it is distinct from the Bukkit world UUID.
* Playback session UUID identifies one viewing instance. Several sessions can play the same replay ID independently.
* Terrain identifier locates base chunks and may be a recording UUID string or a template name.
* `Replay.getDuration()` is milliseconds of recorded wall-clock time, not the last playable tick.

Keep these identities distinct in database columns, match records, and UI. Stopping a recording uses its session ID, not the world ID. Seeking uses ticks, not wall-clock milliseconds.

## UUID identity and presentation

Replay and session identity is a full UUID throughout APIs, storage and commands. Native Lamp UUID parameters parse command inputs; generated command payloads use the canonical 36-character representation with hyphens. There is no short-ID utility, prefix-resolution method, alias table or ambiguity path.

User-facing names, dates and durations provide readability without another identity. Names need not be unique and must not be used as lookup keys. GUI Watch acts through its replay context; chat Watch and Prepare command actions contain the full UUID. UUIDs remain available in details and hover text.

Parsing is not authorization: use `catalog.find`, an authorized mutation or lobby dispatch before access. `/reverseplayer list [page]` reads one metadata page of ten entries, not replay payloads or the entire archive. Its completion dispatches to the main thread and rechecks actor/permission before showing entries. Storage/terrain keys and Minecraft identities keep their UUID representations.

## Build an actor from trusted identity

```java theme={null}
ReplayCatalog.Viewer actor = new ReplayCatalog.Viewer(
    player.getUniqueId(), player.hasPermission("myplugin.replays.admin"));
```

Compute this on the main thread from your real access policy. On the supplied player runtime, administrators come from `reverse.admin` or open access. Lobby also recognizes `reverse.lobby.admin`. The actor is data, not an authentication token: constructing it with arbitrary UUID/admin values bypasses the intended application-level policy.

Owner membership means access to that replay. An empty owner list does not mean public: the default permits logic allows administrators or explicitly listed owner UUIDs.

> [!NOTE]
>
> **No owners does not mean public.** An ordinary viewer must be an owner; administrators can access all recordings under the configured policy.

## Page the catalog

```java theme={null}
catalog.page(actor, 0, 20).whenComplete((page, failure) -> {
    if (failure != null) {
        plugin.getLogger().log(Level.WARNING, "Could not read the replay catalog", failure);
        return;
    }
    for (Replay replay : page.entries()) {
        plugin.getLogger().info(replay.getUniqueId() + " — " + replay.getName());
    }
});
```

Here `catalog` is the runtime's catalog, `actor` is the trusted viewer we just created, and `plugin` is our JavaPlugin. This small example logs metadata only. Schedule actual inventory rendering to the main thread.

Offset ranges from 0 to 10000 and limit from 1 to 100. Results are ordered by descending start time, then lexical replay ID. `hasMore` indicates another page. Because the dataset can change between requests, offset pagination is not a frozen database snapshot.

## Find before loading

For a user-selected ID:

1. `catalog.find(replayId, actor)` checks access using fresh metadata.
2. Empty means absent **or forbidden**, intentionally indistinguishable.
3. Load only after the check succeeds.
4. Before acting on a delayed result, revalidate the live connection and any relevant access-policy change.

The [playback example](https://docs.marioded.com/examples/playback-integration.md) illustrates this pattern without blocking the main thread. For network watching, prefer the lobby dispatch service: it already applies the lobby access policy and handoff flow.

## Editing metadata

Use `catalog.updateMetadata(replayId, actor, name, description)`. It returns `UPDATED` or `MISSING_OR_FORBIDDEN`; backend errors complete exceptionally. Empty text clears a field. Supply both values, retaining the old description if you only rename.

For text supplied by players, call `ReplayCatalog.validateMetadata` first. Its bounds use UTF-16 units: name 128, description 2048. It rejects malformed Unicode, format/control characters (apart from description newlines), and `&` in the current validator. The catalog stores text verbatim, without trimming or MiniMessage evaluation.

For example, when replacing both fields:

```java theme={null}
ReplayCatalog.validateMetadata(name, description);
catalog.updateMetadata(replayId, actor, name, description)
    .whenComplete((result, failure) -> {
        if (failure != null) {
            plugin.getLogger().log(Level.WARNING, "Could not edit replay " + replayId, failure);
        } else if (result == ReplayCatalog.UpdateResult.MISSING_OR_FORBIDDEN) {
            plugin.getLogger().info("Replay was missing or the actor could not edit it");
        } else {
            plugin.getLogger().info("Replay text updated");
        }
    });
```

Catch synchronous text-validation failures in the player-input layer. The stage handles backend completion; the callback above does not perform Bukkit UI operations.

Re-fetch metadata when presenting an edited replay. An old `Replay`/`LoadedReplay` is a stable value, not a live view that mutates when the catalog updates.

## Deleting safely

Use `catalog.delete(replayId, actor)` for a user-driven deletion. The backend performs the authorization and deletion together. `StorageProvider.delete` is a lower-level storage operation and does not represent the owner-policy boundary.

Deletion does not promise immediate removal of data already loaded into an active playback session, nor does it promise automatic terrain pack compaction.

The facade catalogue emits [catalogue events](https://docs.marioded.com/api/events.md#catalogue-events). Deletion notifications include backend-stage failures; metadata notifications include normal success/refusal results but are absent on exceptional completion. Always handle the operation's returned stage too.

## Storage futures and errors

`StorageProvider` saves/loads full replay values, lists raw IDs, checks existence, and deletes. Loading may return null for missing data in current implementations. Failures are exceptional completions; handle them rather than treating every failure as “not found.”

`StorageProvider.exists` has a default list-based implementation. For large user-facing catalogs, use catalog queries rather than repeated raw ID scans.

You borrow runtime storage. Never close it from your integration, and do not expose unrestricted raw storage access to untrusted callers.

## Recording folder configuration

All three roles resolve the storage root from `storage.recordings-folder` in their own `config.yml` before opening the backend:

```yaml theme={null}
storage:
  backend: spatial
  recordings-folder: ''
```

Empty/blank selects `recordings` beside the plugin's data folder: with a normal `plugins/reverse-recorder` data folder, the default is **`plugins/recordings`**. A relative value such as `shared/replays` resolves against the **parent of the plugin data folder** (`plugins/shared/replays` in that installation). An absolute path is used as-is. It is not resolved inside `plugins/reverse-recorder` or relative to the server working directory.

This root is supplied to storage construction, including local spatial storage and configured database fallback. Configure each role consistently when they share files; on separate machines, provide shared replay and terrain storage explicitly. Changing the backend or folder requires a **restart**: reload does not rebuild open stores or move existing data. Integrations should borrow the configured provider instead of reconstructing this path themselves.

## Adding another backend

`storage.backend` is resolved once, at startup, by id: `spatial`, `sql` and `mongodb` ship with this build. The resolution is a `FactoryRegistry` over those built-in `StorageProviderFactory`s, not a plugin hook — an unknown id fails startup with the list of known ids, and no plugin can inject a storage id from outside.

Adding a backend is an in-tree change: implement `StorageProviderFactory`, give it an id, and register it in the built-in `StorageFactory` used by the role composition roots. Your factory returns a `StorageBackend`: the replay provider, its catalogue, the chunk provider, and (optionally) the availability factory that shares the same store. The host disposes exactly one backend through `StorageBackend.close()`, so your implementation owns whatever pool or cache it created. There is no `ServiceLoader` or third-party Bukkit-service discovery for backend factories; Bukkit services expose the already-selected runtime facade.

## Plain data helpers

`ReplayText` represents title and description, with `ReplayText.EMPTY` for unspecified text. `RecordingBounds` selects spatial capture. Vectors represent integer/float/double coordinates. `GameProfile`, metadata entries, and item payloads belong to the event vocabulary; they are not Bukkit entity handles.

See [recordables](https://docs.marioded.com/api/recordables.md) before serializing any custom payload or deriving coordinates from packet data.

## Try it in an integration


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.