Skip to main content

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

Discover ReversePlayer 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:
This callback only logs. If we want to send messages, seek, or change the session speed after completion, we schedule that work back to the main thread and recheck the current viewer/request. The returned future can:
  • 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.
Handle all three. Completion callbacks have no thread guarantee. For a user-selected recording, authorize with the catalog first: play is not itself a catalog actor check. See the PlaybackIntegration example.

Start an already loaded replay

Call startPlaying(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.
Membership is chosen at session start. There is no public “join this running session” operation. Viewers leave through the service; do not modify the returned viewer list. For an already loaded replay, start one shared session on the main thread:
Check for null before using session: neither viewer may have been accepted. To give them independent controls, call startPlaying separately for each viewer instead.

Control a session

Use the Bukkit main thread. Speed must be finite and strictly positive; pause with 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

Use service operations instead of calling 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() and getLastTick() 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.
Use 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 through PlaybackContext 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.