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

# Record a match automatically

> Connect recording to your match lifecycle and publish the replay only after saving succeeds.

# Record a match automatically

**Runtime:** recorder · **Entry point:** `ReverseRecorder` · **Lifecycle calls:** Bukkit main thread

Your integration starts a session when the arena is ready and stops that session when the match ends. Keep the returned session ID in your match object so every teardown path stops the correct recording.

If you are new to the API, follow [Creating your first recording](https://docs.marioded.com/api/first-recording.md) and [Improving your recording](https://docs.marioded.com/api/improving-recording.md) first. This page collects the remaining lifecycle, selection, and capture details.

## Connect capture to your match lifecycle

Discover `ReverseRecorder`, obtain its recorder service, and start a session when your arena is ready. Store the returned session ID in your match object. Stop that exact session when the match ends or is cancelled.

The complete [RecordingIntegration example](https://docs.marioded.com/examples/recording-integration.md) contains the start/stop flow and persistence error handling. It does not depend on the player runtime.

## Starting a session

The simplest call captures a world with no owners or text:

```java theme={null}
RecordSession session = recorder.getRecorderService().startRecording(world);
```

For player-facing recordings, specify owners and useful text instead:

```java theme={null}
RecordSession session = recorder.getRecorderService().startRecording(
    world,
    List.of(ownerId),
    new ReplayText("Arena final", "Best of three"));
```

Start on the Bukkit main thread. Validate user-facing metadata before calling. The start path seeds the initial terrain/entity scene before publishing the session and its start event. Setup failures throw synchronously and do not publish a half-created session.

Owners are deduplicated to an immutable snapshot. Empty is allowed, but such a replay is not accessible to normal nonadmin catalog actors unless your application deliberately uses another policy.

## Spatial and player selection

Use the canonical overload when a world contains several arenas or spectators:

```java theme={null}
RecordSession session = recorder.getRecorderService().startRecording(
    world,
    null,
    List.of(ownerId),
    new ReplayText("Arena A", "Round 1"),
    new RecordingBounds(-64, 0, -64, 64, 255, 64),
    Set.copyOf(participantIds));
```

There are three independent decisions:

1. **Bounds** limit spatial capture. Null means the whole world selection.
2. **Participant UUIDs** select players. Null means all in-scope players; an empty set selects no players. Nonplayer entities remain spatially selected.
3. **Owners** control the finished replay's catalog access, not capture admission.

Bounds use inclusive block coordinates. Continuous positions use `min <= p < max+1`. Inverted bounds throw. Terrain capture includes full-height payloads of chunk columns intersecting the horizontal box. Bounds and participant selection are session-only, not additional persisted replay fields.

Movement updates only admitted entities. Do not rely on an arbitrary outgoing move of an unknown/outside entity to create membership, or on bounds to synthesize exit/re-entry tracking. Participant selection applies to initial player snapshots, chat, equipment, and player-spawn packet producers. A non-participant's spawn is dropped before it can create entity membership, so membership-gated follow-up packets do not admit that player later.

Selection belongs to the **player described by the observation**, not the player whose connection carries it. The packet listener visits all sessions for the channel holder's current world; it does not discard the channel just because its holder is not a participant. A spectator can receive packets about selected players, blocks, or particles that still belong in a recording. `RecordSession.getParticipantIds()` exposes the immutable session selection (nullable for all players); it is not a persisted owner list or a packet-recipient filter.

## Templates

Supply a template name when the base scene already exists in a terrain provider:

```java theme={null}
RecordSession session = recorder.getRecorderService().startRecording(
    world, "arena-template", List.of(ownerId), ReplayText.EMPTY);
```

Null/trimmed-empty means no template. Nonempty templates skip runtime terrain capture. The player machine must resolve that template too. Template mode and nonnull bounds are mutually exclusive and throw before capture starts.

## Several sessions in one world

Multiple recordings may observe the same Bukkit world. Each owns independent selection, ticks, terrain identity, and stop lifecycle. Use `getSessionsForWorld(worldId)` to inspect them; a single “session by world” variable is not sufficient to identify a particular match's recording.

`ReverseRecorder.getSessionsForWorld(worldId)` returns an immutable snapshot of every active session in that world. Use `RecorderService.getSession(sessionId)` and your stored session ID for deterministic lifecycle control. `isRecording(worldId)` tests whether any session records that world; these world queries take UUIDs, not `World` objects.

`RecorderService.getSessions()` returns the coordinated immutable snapshot across all worlds. `getSession(sessionId)` is nullable when inactive. There is no recorder `getSession(Player)` lookup or `getSessions(World)` overload; world membership and ownership do not identify one recording to stop.

## Stopping and publishing

```java theme={null}
CompletionStage<StopResult> saving =
    recorder.getRecorderService().stopSession(session.getUniqueId());
```

`stopSession` accepts only a session UUID. Call on the main thread. Capture stops immediately,
while persistence proceeds asynchronously. Other sessions recording the same world remain active.

> [!NOTE]
>
> **Stopped is not yet saved.** Publish a Watch link only after the save stage succeeds, and decide how your product should handle a degraded terrain result.

| Completion | What your match system should do |
| - | - |
| Exceptional | Log the replay/session ID; report save failure. |
| `replayId() == null` | Session was not recording; do not publish a new replay link. |
| `degraded() == true` | Events were saved, terrain failed; surface a degraded result or withhold normal Watch links according to your product policy. |
| Normal nondegraded ID | Publish the replay ID. |

Callbacks have no main-thread guarantee. Schedule Bukkit notifications back to the server thread. `awaitPendingSaves()` is useful for controlled shutdown orchestration, but do not block normal ticks with `join()`/`get()` waiting for backend I/O.

## Clips

A recording keeps every tick: there is no mode and no window, so **any** live session can export a clip. Export a trailing duration, or an explicit interval, of a live session as a standalone replay:

| Method | Time arguments | Example |
| - | - | - |
| `saveRecentClip(sessionId, duration, text)` | A positive length measured back from the current tick. If the recording is shorter, includes all available history. | Last 10 seconds. |
| `saveClip(sessionId, from, to, text)` | Offsets from the recording's start, not lengths. A null end means the current tick. | Seconds 30 through 45. |

```java theme={null}
RecorderService service = recorder.getRecorderService();
UUID sessionId = session.getUniqueId();
ReplayText text = session.getText();

CompletionStage<StopResult> recent =
    service.saveRecentClip(sessionId, Duration.ofSeconds(10), text);
CompletionStage<StopResult> firstTen =
    service.saveClip(sessionId, Duration.ZERO, Duration.ofSeconds(10), text);
CompletionStage<StopResult> interval =
    service.saveClip(sessionId, Duration.ofSeconds(30), Duration.ofSeconds(45), text);
CompletionStage<StopResult> fromThirtyToNow =
    service.saveClip(sessionId, Duration.ofSeconds(30), null, text);
```

Both methods address the source by session UUID, so they are not limited to the caller's own recording. `saveClip` reads two offsets from the session's first tick: a `ZERO` start selects the recording's first ticks, a `null` end selects the current tick, and an end past it is clamped. A start beyond the current tick selects the final tick alone. Both bounds are inclusive. Durations are converted with `Math.round(offset.toMillis() / 50.0)`; negative offsets and an end tick before the start tick are rejected. A recent duration must round to at least one tick. An explicit 0–10 second interval therefore includes both endpoint ticks, whereas a ten-second trailing clip retains up to 200 ticks.

Invoke either method on the Bukkit main thread. The current-tick cutoff is frozen when the request is accepted; the source recording keeps running. For a session that is no longer active, valid arguments return a stage containing `StopResult.notRecording()` with a null replay ID. Handle completion as described under saving above: event persistence failures complete exceptionally, while terrain failures produce a degraded result. Completion callbacks have no main-thread guarantee.

The sliced events are rebased so the clip starts at tick `0`, and the state the source had folded up to the first retained tick is re-emitted at tick `0`, so the clip opens with the players, entities and blocks that existed before its window rather than a view of a world it no longer sees. The source terrain is **copied** under the clip's own identifier, so the clip survives the source session's later stop or deletion. A non-positive recent duration is rejected before anything is written.

Clips get fresh replay UUIDs and emit the same `RecordingSavedEvent`/`RecordingSaveFailedEvent` persistence notifications as full recordings. They do not stop the source session or emit its stop event. Pending exports are included in `awaitPendingSaves()`.

Stopping a session always saves the full replay. There is no mode that discards the buffer.

## Adding your own timeline events

Register a matching codec on each runtime that must decode the custom event, and a renderer on the player. Insert through the recorder service for the intended session(s), not by mutating its immutable recordable snapshot.

```java theme={null}
recorder.getRecorderService().insertIntoSession(session.getUniqueId(), customEvent);
```

`insertIntoSession(sessionId, eventOrSupplier)` targets one session. Use `insertIntoSessions(sessionIds, supplier)` with an `Iterable<UUID>` for a chosen set, or `insertRecordable(worldId, eventOrSupplier)` deliberately to broadcast to recordings in that world. Events are rebased to each session's relative tick. A supplier runs inline at most once on the calling thread, outside `captureLock`, after target IDs are frozen; insertion rechecks active targets under that lock before tick sampling, duplicate detection, folding and append. Do not perform Bukkit reads in a supplier called by a packet thread. Stop unregisters the target and snapshots its events under the same lock, so a late supplier cannot append after stop.

Spatial admission belongs to the producer, not generic insertion. The world overload broadcasts
its supplied event unchanged to all active sessions in that world, **without spatial clipping**
or participant selection. Custom integrations must select sessions explicitly from their bounds,
participants and entity context before insertion. For a multi-position event, construct the correct
subset separately per session, preserving metadata, order and duplicates without mutating the
source; skip empty subsets. Do not pass a separate position to `insertRecordable`: that argument
no longer exists.

Built-in packet producers check raw origins before conversion/profile extraction and each raw
multi-block absolute position before state-name conversion. Bukkit chat/equipment producers own
their contextual selection. Baseline producers select active sessions and bounds, resolve state
successfully, construct the event, then atomically mark first capture per session before insertion;
they never mutate the supplied session snapshot. Custom pre-mutation baseline producers must
follow the same admission order. Relative movement and flagged teleports retain their existing
membership and relative-state semantics rather than being treated as absolute point events.

Equal events at the same tick are deduplicated before folding and append; sound declarations are
inserted directly, so repeated sound emissions stay distinct.

See [custom recordables](https://docs.marioded.com/api/recordables.md) for the full end-to-end contract.

## Metadata/profile and packet integrations

The public `ReverseRecorder` facade exposes the metadata/profile provider services through
`getMetadataProviderService()` and `getProfileProviderService()`; the recorder adapter itself is
selected by configuration rather than by an API setter (see [protocol adapters](https://docs.marioded.com/api/adapters.md)).
Ordinary minigame integrations should use `RecorderService`; they do not need to replace the
capture layer or access private plugin internals.

Entity-admission queries support network-thread capture.
`isEntityRecorded(sessionId, entityId)` queries one active session's tracked entity membership.

**Sounds are recorded explicitly, never from packets.** Reverse does not inspect clientbound sound
packets: such a packet carries no information about whether the emission was a world broadcast or
a sound sent to a single player, so a packet capture could only guess and would record personal
sounds. Instead the integration declares the emission at its source:

* `recordWorldSound(worldId, soundName, position, volume, pitch)` records one occurrence into
  every active session recording the world.
* `recordSessionSound(sessionId, soundName, position, volume, pitch)` records one occurrence into
  one session.

Both sample the tick under the capture lock and rebase it against the session start, skip sounds
outside the session's recording bounds, and ignore unknown worlds, stopped sessions and negative
ticks. Each call records one occurrence: two calls in the same tick record two occurrences.
Nothing is deduplicated, so a sound repeated within a tick is preserved.

Because the declaration is the only source, sounds the server emits on its own (mob ambience, block
sounds, vanilla effects) are not captured unless the integration declares them, and a sound sent
to a single player is never captured by the plugin.

Existing packet membership and relationship checks remain unchanged; this policy does not add
full entity entry/exit observation or synthesized re-entry. Absolute spawn and world-event
filtering belongs to packet/Bukkit producers before insertion, and player-spawn producers admit
only a session-selected participant. Relative movement and flagged teleports are not generic point events.

During playback, sounds reach only the session's viewers and the client applies attenuation; there
is no fixed 16-block server-side cutoff.


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