Work with recordings
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
UseRecordSession while capture is active. Use Replay when presenting a catalog entry. Use LoadedReplay when you actually need the event stream.
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.
UUID identity and presentation
Replay and session identity is a full UUID throughout APIs, storage and commands. Native Lamp UUID parameters parse command inputs; generated command payloads use the canonical 36-character representation with hyphens. There is no short-ID utility, prefix-resolution method, alias table or ambiguity path. User-facing names, dates and durations provide readability without another identity. Names need not be unique and must not be used as lookup keys. GUI Watch acts through its replay context; chat Watch and Prepare command actions contain the full UUID. UUIDs remain available in details and hover text. Parsing is not authorization: usecatalog.find, an authorized mutation or lobby dispatch before access. /reverseplayer list [page] reads one metadata page of ten entries, not replay payloads or the entire archive. Its completion dispatches to the main thread and rechecks actor/permission before showing entries. Storage/terrain keys and Minecraft identities keep their UUID representations.
Build an actor from trusted identity
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.
No owners does not mean public. An ordinary viewer must be an owner; administrators can access all recordings under the configured policy.
Page the catalog
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:catalog.find(replayId, actor)checks access using fresh metadata.- Empty means absent or forbidden, intentionally indistinguishable.
- Load only after the check succeeds.
- Before acting on a delayed result, revalidate the live connection and any relevant access-policy change.
Editing metadata
Usecatalog.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:
Replay/LoadedReplay is a stable value, not a live view that mutates when the catalog updates.
Deleting safely
Usecatalog.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.
The facade catalogue emits catalogue events. Deletion notifications include backend-stage failures; metadata notifications include normal success/refusal results but are absent on exceptional completion. Always handle the operation’s returned stage too.
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.
Recording folder configuration
All three roles resolve the storage root fromstorage.recordings-folder in their own config.yml before opening the backend:
recordings beside the plugin’s data folder: with a normal plugins/reverse-recorder data folder, the default is plugins/recordings. A relative value such as shared/replays resolves against the parent of the plugin data folder (plugins/shared/replays in that installation). An absolute path is used as-is. It is not resolved inside plugins/reverse-recorder or relative to the server working directory.
This root is supplied to storage construction, including local spatial storage and configured database fallback. Configure each role consistently when they share files; on separate machines, provide shared replay and terrain storage explicitly. Changing the backend or folder requires a restart: reload does not rebuild open stores or move existing data. Integrations should borrow the configured provider instead of reconstructing this path themselves.
Adding another backend
storage.backend is resolved once, at startup, by id: spatial, sql and mongodb ship with this build. The resolution is a FactoryRegistry over those built-in StorageProviderFactorys, not a plugin hook — an unknown id fails startup with the list of known ids, and no plugin can inject a storage id from outside.
Adding a backend is an in-tree change: implement StorageProviderFactory, give it an id, and register it in the built-in StorageFactory used by the role composition roots. Your factory returns a StorageBackend: the replay provider, its catalogue, the chunk provider, and (optionally) the availability factory that shares the same store. The host disposes exactly one backend through StorageBackend.close(), so your implementation owns whatever pool or cache it created. There is no ServiceLoader or third-party Bukkit-service discovery for backend factories; Bukkit services expose the already-selected runtime facade.
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 before serializing any custom payload or deriving coordinates from packet data.

