> For the complete documentation index, see [llms.txt](https://docs.marioded.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.marioded.com/advanced-integrations/recordables.md).

# Custom recordables

Store a game-specific event and play it back with a matching codec and renderer.

**Advanced integration** · Shared payload on recording and viewing machines

Use a custom recordable when a game-specific event needs to be stored and replayed, not merely shown live in the integration's UI. A complete feature spans capture, serialization, and rendering; implementing only one side does not make a recording portable.

## The data path

```mermaid
flowchart LR
  Game[Gameplay event] --> Event[Recordable]
  Event --> Capture[Recorder session]
  Capture --> Codec[Codec and event ID]
  Codec --> Storage[(Replay storage)]
  Storage --> Decode[Matching decoder]
  Decode --> Render[Player renderer]
```

The same custom data contract must be available on the recording and viewing machines. Keep shared payload/codec code independent of `api-player` and `api-recorder` when both sides need it; put renderer/capture bindings in the relevant integration modules.

## Transient versus durable

`Recordable` is the marker for an event. `StatefulRecordable` adds `apply(WorldState)` for a mutation that participates in timeline folding.

* A transient announcement, particle burst, or one-off sound is typically emitted during normal playback.
* A durable entity/block change must update the accumulated scene so pause/seek/resync reproduce the target state.

Implementing a live renderer alone does not make a custom durable event seek-safe. The folded state and resynchronization path must know how to reconstruct it. Do not add an arbitrary new field to runtime internals from an integration and assume the SPI will persist/resync it automatically.

{% hint style="info" %}
**Start with a transient event.** A milestone announcement is a small end-to-end example. Persistent scene changes also need correct folding and resynchronization when viewers seek.
{% endhint %}

## Create a small payload

Let's record an announcement when the game enters a new round. Start with an immutable payload shared by recording and playback integrations:

```java
public record Milestone(String text) implements Recordable {}
```

The data is deliberately small: one string. It contains no Bukkit player handles, live world objects, or protocol packet types.

## Teach Reverse how to save it

Create a symmetric codec in your shared integration code:

```java
RecordableCodec<Milestone> codec = RecordableCodec.of(
    input -> new Milestone(input.readString()),
    (output, event) -> output.writeString(event.text()));
```

The reader consumes exactly the string written by the writer. Next, register the same ID, payload class, and layout with the recorder and player runtime's `RecordableService`:

```java
codecs.register(1000, Milestone.class, codec);
```

Here `codecs` is the borrowed runtime service. Register once before emitting or loading this event. The complete [Milestone example](/api-how-to-guides/examples/milestone.md) shows separate recorder/player binding helpers.

Its codec ID `1000` is an **example allocation**, not a reserved extension range. Check the actual registry and coordinate a collision-free ID across all installed extensions. Register before your recording can emit the event and before replay files containing it can be loaded.

Unknown codec IDs are a hard load failure. Never ignore an unknown record: without its payload layout, the decoder cannot know where the next event begins.

## Insert it into the match

When the round starts, append the event to the recording session your match owns:

```java
recorder.getRecorderService().insertRecordable(session, new Milestone("Round 2"));
```

The recorder assigns the session-relative tick. Do not mutate the immutable event snapshot or substitute a Bukkit world UUID for the target session.

## Render it for viewers

On the player runtime, register the renderer:

```java
player.getRendererService().register(Milestone.class, (event, context) -> {
    for (Player viewer : context.getViewers()) {
        viewer.sendMessage(event.text());
    }
});
```

The callback receives the decoded event and the active session context. It sends a plain-text announcement to that session's viewers during ordinary playback. This is a transient effect, not a persistent field restored on every seek.

## Codec design

`RecordableCodec.of(reader, writer)` is suitable for small explicit schemas. Use the corresponding `ReplayDataInput`/`ReplayDataOutput` primitives in the same order and with the same signedness/units.

Avoid Java object serialization, Bukkit entity handles, and protocol-specific types in persisted data. Validate counts/ordinals before allocating collections or indexing enum arrays. An invalid payload must fail, not return a plausible but misaligned partial replay.

Payload/ID changes require coordinated extension releases on both recording and playback machines. Do not assume an earlier recording can be decoded after changing your custom event layout. Follow the release's recording-compatibility notes as well as your own extension's versioning policy.

## Use the session context for entity effects

The renderer receives only the recordable and the active `PlaybackContext`; use its viewer collection and mappings for the current session.

For entity effects, map recorded IDs through the context. Do not send the original captured entity ID directly to viewers, or reuse one session's remapped ID in another session.

The public player adapter describes the protocol-neutral playback contract. Keep saved data separate from packet-library types, and use the renderer/context contracts for ordinary custom-event playback.

## Correct coordinate and state conventions

* Relative entity movement deltas use shorts in 1/4096-block units; they are not absolute coordinates.
* Rotation values carried alongside relative movement are absolute orientations.
* Packed block positions have signed Y semantics. Use `Vector3i.pack/unpack`, not an unsigned `& 0xFFF` Y extraction.
* Block values are state strings, not saved global protocol state IDs. Resolve IDs only at the protocol encode/decode boundary.
* Metadata and equipment packets can contain partial updates; durable folding merges entries rather than replacing the complete set.
* A baseline is pre-history for touched block positions. It seeds an untouched value and is not itself a block-change effect to draw.

## Built-in vocabulary

Reverse records supported block changes, entity activity, chat, sounds, particles, and world effects automatically. Add a custom event when your integration needs additional game-specific data, such as the milestone announcement above.

Use the public recordable and codec contracts documented here, and register your own types with the service. Keep custom IDs coordinated across extensions and insert into the intended session(s) rather than editing a captured event collection.

## Validation strategy

Run an actual scene containing the custom event, save/load it through the configured backend, and verify forward playback, pause, seek backward/forward, and multiple sessions. A transient chat example need not appear while scrubbing history; a durable block/entity extension must reconstruct correctly at the target tick.

{% content-ref url="/pages/YUds2Z1FhUzTxzC5CLBM" %}
[Recording a milestone](/api-how-to-guides/examples/milestone.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.marioded.com/advanced-integrations/recordables.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
