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
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?
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. 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 underdisplay.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 theBungeeCord 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:
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 →
