Skip to main content

Listen for lifecycle events

Reverse exposes Bukkit events so integrations can observe lifecycle without patching command handlers. Register listeners through Bukkit’s PluginManager using your integration plugin as the owner.
Choose the event for the outcome you need. Use RecordingStopEvent for capture ending, RecordingSavedEvent for persisted data, and PlaybackStopEvent for playback cleanup finishing. Catalogue changes arrive as ReplayDeletedEvent/ReplayMetadataUpdatedEvent, and watch outcomes as DispatchResultEvent.

Recording events

Imports use com.marioded.reverse.api.recorder.event. A saved replay can still be degraded. Check isDegraded()/getTerrainFailure() instead of assuming every RecordingSavedEvent has usable terrain. getTickCount() counts occupied event ticks, not wall-clock seconds or a guarantee that the timeline is dense. These persistence events also cover clip exports: their replay ID is the clip’s fresh ID. Exporting a clip leaves the source recording active and does not emit RecordingStopEvent for it. Save notifications are dispatched directly on the completion thread. Schedule any Bukkit UI, player lookup, or world access onto the main thread. Events may be attempted during shutdown; only listeners still registered receive them. Keep persistent match bookkeeping in the save stage or your own durable state if receiving an event during teardown is not sufficient.

Playback events

Imports use com.marioded.reverse.api.player.event. Stop is a post-cleanup notification. The session is no longer in the service’s active registry. Its viewer snapshot contains those present when stopping began; earlier leavers are not included, and handles can already be offline. session.isFinished() distinguishes reaching the timeline end from an early stop; there is no separate public stop-reason enum.

Catalogue events

Imports use com.marioded.reverse.api.catalog.event. The runtime’s MemoryCachedStorageProvider implements both storage and catalogue access and emits these notifications around its catalogue delegate. Use the facade’s catalogue; calling a raw backend directly, or calling the one-argument StorageProvider.delete(replayId), does not emit these catalogue events.
  • A successful deletion has isDeleted() == true. A missing/forbidden deletion has false and no failure. An exceptionally completed deletion stage has false and its cause in getFailure(); the returned stage remains exceptional.
  • A metadata result of UPDATED confirms the replacement, including when the text is unchanged. MISSING_OR_FORBIDDEN also emits an event, but its text fields are the requested values, not evidence that anything was written. Exceptional metadata completion emits no metadata event; handle the returned stage to observe backend failures.
  • A synchronous throw before the delegate returns a stage does not reach either completion hook. Invalid arguments are not guaranteed to produce a notification.
These are post-operation, noncancellable notifications. The actor describes the request; it is not an authentication token. Events run directly on the completion thread, which can also be the calling thread for an already-completed stage. There is no main-thread dispatch guarantee. Each instance marks isAsynchronous() according to its constructing thread; schedule Bukkit-facing work yourself.

Dispatch events

Imports use com.marioded.reverse.api.lobby.event. getResult() is the DispatchService.Result: SUCCESS, NOT_AUTHORIZED, NO_REPLAYER, or TRANSFER_FAILED. For valid watch(viewer, replayId) calls, this includes immediate offline/overlapping-request refusals. An immediate refusal fires on the calling thread and has no recorded cause. It does not release another request that is already pending. For an accepted request, the runtime releases its pending marker before emitting the event on the request’s completion thread. There is no main-thread notification guarantee. An exceptional request is reported as TRANSFER_FAILED with getFailure() carrying the cause, while its returned stage remains exceptional. Some downstream transfer/availability failures are instead logged and converted to a normal TRANSFER_FAILED result, so a null event failure does not prove there was no backend error. Invalid/null arguments are not guaranteed an event. The viewer handle may already be offline. SUCCESS means the pending selection was stored and a proxy transfer message was sent; it does not confirm arrival or playback start. Observe PlaybackStartEvent on the destination for that outcome.

Listener pattern

Let’s react when a replay finishes saving. In a JavaPlugin that implements Bukkit Listener, register the listener during enable:
Then add the handler:
We copy the result values before scheduling the main-thread application callback. Replace the logging hook with the notification your application needs; do not assume the save event itself was synchronous. The complete ReplayNotifications example also demonstrates a playback stop listener. Keep event handlers short; do not load an entire replay or block on SQL inside a handler. Events expose isAsynchronous() according to the constructing thread. Ordinary player runtime operations should occur on the main thread, but use the event’s actual context instead of assuming every event delivered through Bukkit is synchronous.

Notifications, not cancellation hooks

These events do not implement Bukkit Cancellable. They are not a supported way to veto a recording save, deny a viewer after playback starts, or rewrite seek behavior. Enforce access before starting/dispatching, and use service operations for intentional lifecycle changes. Avoid an unconditional listener that responds to a pause event by immediately setting the opposite pause state: reentrant state changes can create event loops. Likewise, do not retain mutable playback contexts as asynchronous work items.