The core systems that ReplayMod uses to record, playback, handle UI and files
have some public interfaces that external mods can use to integrate into the ReplayMod.
The active ReplayMod instance is what controls the UI using its ReplayRecording and
ReplayPlayback instances.
ReplayRecording represents an active recording session. It captures live scene state
over time and stores it as frame data. A recording can be started stopped, buffered,
and saved to disk.
The class maintains its own internal state and must be advanced each frame
via HandleRecording() if it is created manually.
ReplayPlayback represents a replay playback context. It loads recorded frame data
and reconstructs state over time using its own time cursor.
Playback is advanced through HandlePlayback(). It contains independent controls
for playback.
Multiple playback instances can exist at the same time as long as each is advanced
independently.
ReplayFiles controls replay storage and browsing. It exposes the active
ReplayExplorer, which represents the current replay directory and selection state.
It is responsible for selecting and loading available replays and updating the UI
accordingly.
Replay Explorer represents the current replay directory and selection state.
It exposes the list of discovered replay paths in the active folder and provides
access to the currently selected replay and its metadata.
Replay Extensions allow external mods to inject custom data into replay files.
An extension can:
- Store custom archive-level data inside the replay file
- Write custom per-frame data
- Read that data back during playback
- Add timeline markers
- React to replay lifecycle events
Each extension is isolated under its own namespace inside the replay archive.
Register your extension once during initialization:
private ReplayAPI.ReplayExtension extension;
public override void OnlateInitializeMelon()
{
extension = ReplayAPI.RegisterExtension(
id: "MyModId",
onBuild: OnArchiveBuild,
onRead: OnArchiveRead,
onWriteFrame: OnWriteFrame,
onReadFrame: OnReadFrame
);
}The id must remain stable forever once released.
Changing it will prevent old replays from associating with your extension.
ID's must be unique across all mods.
Archive data is written once per replay file.
Use this for:
- Static metadata
- Config snapshots
- Asset references
- Large serialized blobs
private void OnArchiveBuild(ReplayAPI.ArchiveBuilder builder)
{
var bytes = Encoding.UTF8.GetBytes("example");
builder.AddFile("data.txt", bytes)
}This writes to
extensions/MyModId/data.txt
private void OnArchiveRead(ReplayAPI.ArchiveReader reader)
{
if (reader.TryGetFile("data.txt", out var bytes))
{
var text = Encoding.UTF8.GetString(bytes);
}
}Frame data is written per replay frame and is intended for time-dependent state.
Use this for:
- Transform state
- Animation state
- Runtime object values
- Anything that changes over time and frequently
Frame data is written using a FrameExtensionWriter.
Each call to WriteChunk writes one extension chunk for that frame.
private enum MyField : byte
{
Position,
Health
}
private void OnWriteFrame(ReplayAPI.FrameExtensionWriter writer, Frame frame)
{
writer.WriteChunk(subIndex: 0, w =>
{
w.Write(MyField.Position, position);
w.Write(MyField.Health, health);
});
}Each WriteChunk call writes:
- Extension ID
- SubIndex (int)
- Payload length (int)
- Tagged field data
You may call WriteChunk multiple times per frame.
The subIndex identifies which entity the chunk belongs to.
- Only write fields that changed from the previous frame (delta encoding recommended).
- Always use the provided
BinaryWriter.Write(field, value)overloads. - Field payload size is limited to 255 bytes.
- Do not reorder enum field values after release. Keep the numbering of the enum the same if removing/adding fields.
OnReadFrame is invoked once per extension chunk.
If your extension wrote 5 chunks in a frame, OnReadFrame will be called 5 times for each chunk.
private struct MyState
{
public Vector3 Position;
public int Health;
public MyState Clone()
{
return new MyState
{
Position = Position,
Health = Health
}
}
}
private MyState lastState;
private Dictionary<(Frame, int), MyState> reconstructedFrames = new();
private void OnReadFrame(BinaryReader br, Frame frame, int subIndex)
{
var state = ReplaySerializer.ReadChunk<MyState, MyField>(
br,
() => lastState?.Clone() ?? new MyState(),
(state, field, fieldSize, reader) =>
{
switch (field)
{
case MyField.Position:
state.Position = reader.ReadVector3();
break;
case MyField.Health:
state.Health = reader.ReadInt32();
break;
}
});
reconstructedFrames[(frame, subIndex)] = state;
}The BinaryReader provided is already scoped to this specific extension chunk.
Unknown fields are automatically skipped.
Extensions can add markers during recording:
var marker = extension.AddMarker(
name: "SpecialMoment",
time: Time.time,
color: Color.magenta
)Time.time is the current frame in recording.
Markers are automatically namespaced as:
MyModId.SpecialMoment
Markers are only added while recording or buffering is active.
ReplayAPI exposes the following events:
ReplaySelectedReplayStartedReplayEndedReplayTimeChangedReplayPauseChangedOnRecordFrameOnPlaybackFrameReplaySavedReplayDeletedReplayRenamed
Example:
ReplayAPI.ReplayStarted += info =>
{
LoggerInstance.Msg("Playback started")
}A replay archive contains:
manifest.json
replay (binary stream)
extensions/
MyModId/
custom files...
Each extension operates only within its own folder.
- Separate archive data (static) from frame data (dynamic).
- Use delta encoding to reduce file size.
- Keep field payloads small.
- Clear cached state when
ReplayEndedfires. - Treat serialized formats as permanent once released.
- Run Ticked classes (ReplayPlayback and ReplayRecording) in OnLateUpdate
See ExampleMod.cs in this folder for a complete example of recording and replaying a scene object using an extension.