> For the complete documentation index, see [llms.txt](https://docs.marioded.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.marioded.com/api-how-to-guides/catalog-and-storage.md).

# Catalog and storage

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

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.

## Build an actor from trusted identity

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

{% hint style="info" %}
**No owners does not mean public.** An ordinary viewer must be an owner; administrators can access all recordings under the configured policy.
{% endhint %}

## Page the catalog

```java
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](/api-how-to-guides/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
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.

## 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.

## 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](/advanced-integrations/recordables.md) before serializing any custom payload or deriving coordinates from packet data.

## Try it in an integration

{% content-ref url="/pages/sVo2hzYBpCUo8kAi2nkz" %}
[Starting local playback](/api-how-to-guides/examples/playback-integration.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.marioded.com/api-how-to-guides/catalog-and-storage.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
