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 and Improving your recording first. This page collects the remaining lifecycle, selection, and capture details.
Connect capture to your match lifecycle
DiscoverReverseRecorder, 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 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:Spatial and player selection
Use the canonical overload when a world contains several arenas or spectators:- Bounds limit spatial capture. Null means the whole world selection.
- Participant UUIDs select players. Null means all in-scope players; an empty set selects no players. Nonplayer entities remain spatially selected.
- Owners control the finished replay’s catalog access, not capture admission.
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:Several sessions in one world
Multiple recordings may observe the same Bukkit world. Each owns independent selection, ticks, terrain identity, and stop lifecycle. UsegetSessionsForWorld(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
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.
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.
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: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.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 for the full end-to-end contract.
Metadata/profile and packet integrations
The publicReverseRecorder 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).
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.

