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

# How recordings work

> Understand what a recording contains and how recorder, player, and lobby work together.

# How recordings work

Reverse separates capturing gameplay, storing a recording, and watching it. That lets you start on one server and later move to a network without changing how players choose a replay.

## From match to replay

```mermaid theme={null}
flowchart LR
  Game[Gameplay] --> Recorder[Recorder]
  Recorder --> Storage[(Recordings and terrain)]
  Storage --> Player[Player]
  Player --> Viewers[Replay viewers]
```

The recorder saves the starting scene and supported actions. The player loads that data and shows the scene to viewers. The original gameplay world does not need to remain loaded on the viewing server.

## What is saved?

| Data | Why it matters |
| - | - |
| Replay details | Name, description, owners, and recording time are shown in the catalog. |
| Recorded actions | Movement, block changes, and other supported activity form the playback timeline. |
| Starting terrain | Provides the world before those changes happen. It is stored separately from replay events. |

Keep the complete dataset when moving or backing up recordings. A `.rpl` event file alone is not the entire starting world.

The recorded world UUID, recording session UUID, replay UUID, and playback session UUID have different roles. A normal recording stop saves under the recording session UUID; a clip receives a fresh replay UUID. Playback controls use the playback session UUID, never the replay UUID. Commands use native UUID arguments and canonical full UUID payloads. Readability comes from names, dates, durations and clickable actions, not a second identifier. No prefix resolution or ambiguity machinery remains.

Capture selection is separate from ownership. Owners determine catalog access. Optional API bounds determine spatial scope, while `participantIds` selects players (`null`: all in scope; empty: none). Nonplayer entities are spatially selected. Bounds and participant sets are immutable session-only state, not persisted selection rules in the replay.

## Capture and saving are separate steps

Stopping a recording ends capture immediately. Saving can take longer, so wait for the completion result before offering the replay to viewers.

A degraded result means the actions were saved but terrain was unavailable. A failed save is a different outcome. Integrations should handle both rather than treating every stop as a ready-to-watch replay.

### Clips retain a standalone starting scene

Every live recording retains its complete event history from tick zero. There are no recording modes or rolling-buffer windows, and stopping always saves the full stream. `/reverserecorder clip` can export a recent duration or an explicit interval without stopping the source.

An export freezes its cutoff and snapshots the interval under the capture lock, rebases its ticks to zero, and prefixes the durable entity/block state that existed before the interval. It copies source terrain under the clip's fresh replay UUID, including terrain read from a selected template. The copy is independent of the source identifier, while content-addressed chunk payloads can remain deduplicated. A terrain-copy failure produces a degraded result; event persistence failure completes the export exceptionally. Exporting does not trim the source history.

Integrations use [`saveRecentClip` for a trailing duration and `saveClip` for an explicit interval](https://docs.marioded.com/api/recording). Both operate on a live recording session UUID, not a previously saved replay UUID.

### Player identity and item appearance

Incoming player movement, rotation, and animations use the Bukkit player's entity ID, matching the initial entity snapshot. A packet-library connection may have missed the join packet during late injection, so its cached entity ID is not authoritative for recording admission. This does not bypass bounds, participant selection, or the check that an entity is already recorded.

Items retain a version-neutral appearance containing RGB color, custom model data, and custom name. Playback translates that appearance to the viewing server's item format. On NBT-based versions, dyed leather color is an integer under `display.color`, including on 1.8.8; component-based versions use the dyed-color component.

## Watching a recording

A viewing session has a current position, pause state, and speed. Several viewers can share the same session, in which case they share its controls. Separate sessions allow independent watching of the same recording.

Seeking jumps to the scene at a different point. It does not modify the original gameplay world or replay every sound from the skipped time.

The player delivers terrain and recorded changes as client packets in one shared void world; it never reconstructs server-side blocks. Backward seek folds the stream from tick zero into fresh absolute state and resynchronizes the scene. There are no checkpoints. Partial entity metadata/equipment changes merge into the accumulated state. Block baselines carry the pre-change value, so seeking never guesses it from the live Bukkit world.

## Startup selection and storage paths

`storage.backend` resolves the built-in `spatial`, `sql` or `mongodb` factory. Recorder/player `adapter` resolves the built-in `packetevents` factory. These are case-sensitive, one-shot `FactoryRegistry` lookups over factories explicitly listed by the composition root. An unknown id fails startup with the known ids. Neither path discovers plugins, scans Bukkit services for factories, or uses `ServiceLoader`. Bukkit service registration of the public role facades is a separate mechanism.

Each role resolves `storage.recordings-folder` before constructing storage. Blank selects a `recordings` sibling of the role's data folder, normally `plugins/recordings`; a relative path resolves against that data folder's parent and an absolute path is used verbatim. This folder holds spatial recordings and is also the base for a relative `database.sqlite-file`. Changing paths or backends requires restart and performs no automated migration.

The `sql` factory loads common SQL libraries plus the selected driver group, then constructs pooled SQL storage. MySQL/MariaDB use the MariaDB driver, PostgreSQL and SQLite use their respective drivers. Only configured SQL-startup fallback constructs spatial storage instead; a successful SQL startup does not also create a spatial terrain provider. Fallback is a startup decision, not replication or live failover.

### Spatial visibility across independent roles

Spatial readers and writers do not share an implementation-class static map. Each manager owns its channels and positive caches. A permanent `.spatial.lock` in the canonical storage directory coordinates publications through a filesystem lock; an interned path-based monitor also coordinates lock-channel lifetimes across independently loaded plugins in the same JVM.

A new chunk write refreshes the committed CAS index, selects its append offset from disk, writes and forces the pack entry, then forces and atomically publishes the updated index before completing. Before returning a missing hash, a reader refreshes that index. Flush and close never publish a manager's stale index. Immutable terrain-index publication uses the same directory lock, so concurrent differing writes under one identifier cannot replace one another.

Recovery of a missing or lagging index verifies complete pack payloads before publishing a replacement; corrupt or truncated data fails explicitly. These operations require filesystem locking and atomic moves, and do not imply a portable power-loss guarantee for directory entries. Use shared database storage for multi-machine deployments.

### Catalog and details rendering

Standalone player and lobby consume the same catalog/detail view implementation but own separate menu lifecycles and Watch actions. Details perform an asynchronous authorized metadata lookup and publish its result back on the main thread. Release packaging preserves the framework's view callback names and parameter signatures, including compiler-generated bridge methods: losing a details-render bridge can otherwise open an empty inventory without throwing an exception.

## Adding a lobby

The lobby shows accessible recordings, finds an available replay server, and asks the proxy to move the viewer there. The player server then opens the selected recording.

The lobby facade exposes dispatch, menu actions, availability/service access, catalog, replay storage, and recordable services. It has no chunk-storage accessor and does not render or load terrain for Watch; scene delivery belongs to the player. Dispatch uses the `BungeeCord` plugin-message channel's `Connect` subchannel, and success means the handoff was written and the transfer message sent, not that arrival or playback completed.

Shared recording storage and online-server coordination do different jobs: SQL can provide the common archive, while SQL or Redis can coordinate transfers.

Availability uses the built-in backends opened once by player/lobby startup. The existing `getAvailability()` and `getAvailabilityService()` facade methods remain available to integrations, but external factory injection is not a deployment mechanism. Consumers borrow the plugin-owned services and do not reopen or close them.

## Source map

The application sources used for these decisions are:

| Concern | Source in the application repository |
| - | - |
| UUID command input and named clickable presentation | Role command classes, message configs and `gui/.../MenuPlaceholders.java` |
| Commands and clip flags | `plugin-recorder/.../command/RecorderCommand.java` |
| Capture lifetime, interval snapshots and persistence | `plugin-recorder/.../service/DefaultRecorderService.java` and `ClipExporter.java` |
| Folder/default configuration | `common/.../config/StorageConfigRecord.java`, `DatabaseConfigRecord.java`, and each role's config record |
| Fixed backend selection and injected whole-backend fallback | `common/.../storage/StorageFactory.java`; each plugin root supplies its built-in factories |
| SQL storage and driver provisioning | `storage-sql/build.gradle.kts`, `backend/SqlStorageProviderFactory.java`, `connection/SqlDialect.java` / `SqlConnectionFactory.java` |
| MongoDB/GridFS storage and driver provisioning | `storage-mongo/build.gradle.kts`, `backend/MongoStorageProviderFactory.java`, `MongoBackendFactory.java` |
| Adapter and availability selection | `ReverseRecorderPlugin`, `ReversePlayerPlugin`, and `ReverseLobbyPlugin` composition roots |
| Lobby boundary and proxy dispatch | `api-lobby/.../ReverseLobby.java` and `plugin-lobby/.../service/DefaultDispatchService.java` |

Paths with `...` abbreviate the Java source and package directories. The root `ARCHITECTURE.md` documents design intent; check current source when a historical description differs from the running implementation.

[Lobby and proxy setup →](https://docs.marioded.com/owners/network.md)


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