Start and control playback
Runtime: player · Entry point:ReversePlayer · Controls: Bukkit main thread
Use this guide when your own command, menu, or game UI needs to open a saved recording. The runtime handles loading, viewer state, and the playback scene.
Start by replay ID
DiscoverReversePlayer and use play(replayId, viewer) or play(replayId, viewers). The runtime loads asynchronously and schedules the actual start on the main thread.
Let’s assume api is the discovered player provider, viewer is the requesting online player, and the replay ID has passed a fresh catalog access check. Inside our JavaPlugin, we can start playback and handle its result:
- Complete with a
PlaybackSession. - Complete normally with null if the replay is missing or no viewer can be accepted.
- Complete exceptionally if loading or starting fails.
play is not itself a catalog actor check. See the PlaybackIntegration example.
Start an already loaded replay
CallstartPlaying(loadedReplay, viewers) on the main thread. Offline players and duplicates are dropped. A player already in another session is deliberately switched where their previous state can be restored; a failed restoration can cause that viewer to be skipped. If nobody is accepted, the result is null.
The supplied /reverseplayer play command applies its own “already watching” guard. Do not infer the more general service behavior only from that command UX.
Shared sessions
Start with a list to give several people one synchronized viewing session. Its cursor, pause state, speed, and folded world state are shared. Every viewer sees the same timeline position. If each player needs independent controls, start separate sessions. The recording’s immutable event stream can be shared, while mutable session state and entity mappings remain independent.One session means one timeline. Pausing, changing speed, or seeking affects everyone in that session. Start separate sessions when viewers need independent controls.
session: neither viewer may have been accepted. To give them independent controls, call startPlaying separately for each viewer instead.
Control a session
pause, not by setting speed to zero.
All timeline controls take the playback session UUID: seekTo(sessionId, tick),
seekBy(sessionId, deltaTicks), pause(sessionId), resume(sessionId),
isPaused(sessionId) and setSpeed(sessionId, speed). Unknown-session controls do nothing;
isPaused returns false for an absent session.
seekTo uses an absolute session-relative tick. seekBy uses a signed relative delta. Tick zero is the start of the event timeline. The service reconstructs scene state and resynchronizes viewers. Setting session.setCurrentTick yourself does not do that work.
The API seek does not imply the command’s extra “resume after seek” behavior. Decide whether your integration should preserve pause or explicitly resume.
Leave one viewer or stop everyone
session.removeViewer: the service also performs restoration, listener-visible lifecycle, scene cleanup, and final-session handling. Handle disconnections through the runtime rather than retaining offline player objects indefinitely.
Read session information
getReplayId()identifies the recording;getSessionId()identifies this viewing instance.getCurrentTick()andgetLastTick()describe the playable timeline.getRecordedDurationMillis()is a separate wall-clock recording duration.getViewers()returns an immutable membership snapshot.getWorldState()exposes the accumulated durable scene; treat it as runtime-owned.getRecordablesAt(tick)gives events for the requested tick, or an empty list.
ReversePlayer.getSession(viewerId) (an Optional) or PlaybackService.getSessionByViewer(viewerId) (nullable) to find a viewer’s current session. ReversePlayer.isWatching(viewerId) and PlaybackService.isViewerPlaying(viewerId) test viewer membership. In contrast, PlaybackService.getSession(sessionId) resolves a playback session UUID and returns null if absent. These lookups require the main thread. A cached session reference is not proof that it is still active after an asynchronous operation.
For example, a main-thread pause button can look up the current session at click time:
Entity IDs are session-specific
Recorded entity IDs cannot be sent directly back to every client. Different playback sessions may contain the same original ID, and real server entities also have IDs. Render throughPlaybackContext and its session entity ID/UUID mappings.
Do not retain a remapped ID indefinitely across resynchronization or use one session’s mapping for another session. Let entity-aware renderers and lifecycle operations maintain those mappings.
Events and custom display
Subscribe to playback events to respond to session lifecycle. Use the layout/menu APIs to add controls without replacing the player’s inventory management.playTick(PlaybackSession) is runtime scheduling machinery and still takes the actual session object; an integration should not advance a session separately while Reverse’s own tick task is already doing so. handleViewerQuit(Player) likewise retains a player handle for disconnect cleanup, and leaveSession(sessionId, Player) needs it for restoration. The UUID controls do not change these helper contracts.
The runtime owns its sidebar lifecycle too. Its implementation-level ScoreboardService.clear(sessionId) uses the playback session UUID, while layout defaults and clearViewerState(viewerId) use viewer UUIDs. Scoreboard internals are not exposed by ReversePlayer; ordinary integrations should stop/leave through the playback service and let it clean up displays.
