Skip to main content

Flashback Replay Format (RPLY v1)

Version 3. Binary, little-endian, written with Luau buffer. Everything is op-stream based so the recorder can append incrementally and the player can seek by scanning a keyframe index.

Layout​

Header
StringTable
KeyframeIndex
OpStream
fieldtypenotes
magicu32"RPLY"
versionu83
tickRateu8snapshots per second (e.g. 15)
placeVersionu32game.PlaceVersion — refuse mismatches
startUnixf64os.time() at record start
tickCountu32total ticks
stringCountu16
keyframeCountu16
opBytesu32length of OpStream

StringTable​

stringCount × (u16 len, bytes). Asset descriptors, property names, event names, animation ids, custom channel names all go through this so the op stream only carries u16 ids.

KeyframeIndex​

keyframeCount × (u32 tick, u32 byteOffset). A keyframe is a tick where every live entity emitted a full XFORM (no delta). Seeking = jump to nearest keyframe ≤ target, replay forward.

OpStream​

Sequence of ops. Each op starts with u8 opcode.

opnamepayload
0TICKu32 tick
1SPAWNu16 entityId, u16 descriptorStr, u16 parentEntityId (0=none)
2DESPAWNu16 entityId
3XFORMu16 entityId, u8 mask, [pos: 3×f32] [rot: 3×i16 quantized]
4PROPu16 entityId, u16 nameStr, u8 typeTag, value
5ANIM_PLAYu16 entityId, u16 animIdStr, u8 handle, u8 fade/100, i16 speed×256
6ANIM_STOPu16 entityId, u8 handle, u8 fade/100
7ANIM_SPEEDu16 entityId, u8 handle, i16 speed×256
8EVENTu16 nameStr, u16 payloadStr (JSON)
9CUSTOMu16 channelStr, u16 byteLen, bytes
10TICK_NEXT(none) advances tick by one
11XFORM_Did entityId, u8 mask, deltas (see below)
14SNAPSHOTu16 entityId, u32 byteLen, bytes (geometry snapshot, see below)

XFORM mask bits: 1 = position present, 2 = full rotation present, 4 = yaw-only rotation (i16, ±π). Rotation is quantized as axis-angle scaled to i16 (±π → ±32767). Upright entities (up vector within 0.005% of world up) use yaw-only samples and i8 yaw deltas afterwards. id is one byte below 255, otherwise 0xFF then u16. XFORM_D mask bits: 1 = 3×i8 position delta (1/32 stud), 2 = 3×i16 position delta (same unit, for fast movers), 4 = i8 yaw delta (1/64 rad, yaw-only entities), 8 = 3×i8 axis-angle rotation delta (1/128 rad, tilted entities). Deltas accumulate from the last XFORM; the recorder tracks the quantized value so error does not drift. Deltas that overflow, or a switch between yaw-only and tilted, force a new XFORM. Ticks with no changes emit nothing; TICK_NEXT precedes the first op of a new tick. Position is raw f32 (fine for most maps; quantize to i16 with a per-map origin/scale if size matters).

SNAPSHOT​

Written once per tracked part or model, player characters included. The capture is taken at SPAWN but held back while descendants keep arriving (a character still loading limbs and accessories) and retaken once the entity has been quiet for a second, then written; pending captures are flushed on despawn, stop and finalize. The player keeps the last one it decodes. u8 version (10), u8 isModel, str16 name, str16 sourceKey (unique path when recorded), str16 rootPath (path of the root part inside the entity), u16 unionCount + str16 union keys (the union's unique path when recorded), u16 meshCount + str16 mesh ids (so the player can prefetch MeshParts), then for models a rig block (u8 hasHumanoid, and if set u8 rigType, str16 shirt, str16 pants, str16 shirtGraphic, u8 hasBodyColors + 6×3 u8 colours, then u8 meshCount × CharacterMesh (u8 bodyPart, f64 meshId, f64 baseTextureId, f64 overlayTextureId asset ids)), then u16 count and per BasePart: u8 kind (0 Part, 1 MeshPart, 2 Wedge, 3 CornerWedge, 4 other, 5 Union), for unions u16 unionKeyIndex, then str16 name, str16 parentPath (slash-joined segments of the wrappers between the entity and the part, rebuilt as Folders), 3×f32 offset from root, 3×i16 rotation, 3×f32 size, 3×u8 color, u16 material, u8 transparency, u8 reflectance, u8 shape, 6×u8 surface types, for meshes str16 meshId, str16 textureId, 3×f32 meshSize, then an optional SpecialMesh block, an optional SurfaceAppearance block, a list of Decals/Textures and an effects list. After the parts comes a joint list: u16 count, per joint u8 kind, str16 name, u16 holderIndex then for kind 1 Weld / 2 Motor6D u16 part0Index, u16 part1Index, C0, C1, and for kind 3 AnimationConstraint / 4 RigidConstraint u16 part0Index, str16 attachment0Name, u16 part1Index, str16 attachment1Name (indices into the part list, 1-based; attachments are rebuilt from the effects list first). The player rebuilds this as an anchored proxy when no prototype is supplied; MeshPart meshes are rebuilt as real MeshParts through AssetService:CreateMeshPartAsync (Snapshot.prefetch, which Player.new calls and which yields the first time each mesh id is seen), with a scaled SpecialMesh as fallback; the root part always comes first so it becomes the proxy's PrimaryPart; joints are recreated so animations drive the rig, and rigs get a Humanoid so clothing renders. Union geometry cannot be serialized: on playback a key resolves to the UnionOperation at the recorded path, or to one linked manually through Snapshot.link (the plugin's Prefab links list), and is cloned; otherwise a plain Part of the same size stands in. Linking a snapshot's source key to a prefab (typically kept under ReplicatedStorage) makes the player clone that prefab instead of rebuilding the snapshot at all, which is the way to play back models that cannot be serialized faithfully; the clone's PrimaryPart is moved to the part named like the recorded root so recorded transforms land where the original did.

Effects list: u8 count, per effect u8 class (1 Attachment, 2 Sound, 3 ParticleEmitter, 4 Beam, 5 Trail, 6 Fire, 7 Smoke, 8 Sparkles, 9 PointLight, 10 SpotLight, 11 SurfaceLight), u8 parentIndex (0 = the part, else an earlier attachment), str16 name, u8 propCount and per property u8 propIndex (into the class's property list), u8 tag, value with tags 0 bool, 1 f32, 2 str16, 3 Color3, 4 Vector3, 5 Vector2, 6 NumberRange, 7 NumberSequence (u8 n × time/value/envelope f32), 8 ColorSequence (u8 n × f32 time + Color3), 9 enum name, 10 attachment reference by name. Watched properties (the recorder's Class.Property watch list, whose default covers part appearance, humanoid health and effect state such as Enabled, Playing, Rate, Size, Volume, Brightness) of the entity and the instances inside it are written as PROP ops whose name is a slash path inside the entity as the rebuilt proxy sees it, e.g. Body/Exhaust/Enabled or Hat/Handle/Transparency. Initial values of properties the snapshot already carries (part Color, Transparency, Size, Material, Reflectance and effect state) are not written; only changes are. Attributes of a tracked instance are written as @Name (and set back with SetAttribute), and a tracked model's scale as Scale, the ratio of Model:GetScale() to its value at spawn (applied with ScaleTo). A watched Sound's Played event writes <path>/TimePosition = 0 and <path>/Playing = true so restarts of an already playing sound replay. Which properties, attributes, scale, animations and descendants (properties and effects inside the entity) are recorded is set per recorder and overridable per instance through the FlashbackWatch, FlashbackAttributes, FlashbackScale, FlashbackAnimations and FlashbackDescendants attributes.

PROP typeTags​

0 bool, 1 number(f64), 2 string(strId), 3 Color3 (3×u8), 4 Vector3 (3×f32), 5 enum(u16 strId of name).

Descriptor strings​

A descriptor tells the player how to build a proxy. Convention:

  • model:<path.in.ReplicatedStorage.Prototypes> — clone a prototype
  • part:<Shape> — create a bare Part, PROP ops fill Size/Color/Material
  • char:<userId> — player character; rebuilt from its SNAPSHOT (limbs, accessories, clothing, body colours, joints), falling back to Players:CreateHumanoidModelFromUserId for recordings without one
  • sub:<path> — a BasePart inside a tracked ancestor; the player drives the part at that path inside the parent proxy instead of creating a new one. Tagged parts jointed to a tracked ancestor (Motor6D, Weld, WeldConstraint) are not written at all: the rig's own joints and recorded animation tracks move them. When every joint holding such a part disappears it is spawned as a sub: entity from that tick; the player removes the proxy's joint and drives it
  • model:<name> is also used for Tools carried inside a tracked entity: they spawn as child entities (parent id set) with their own snapshot when equipped and despawn when they leave
  • asset:<assetId> — InsertService (rarely worth it; prefer prototypes)

Paths​

Every path (sub: descriptors, PROP names, snapshot parent paths, source and union keys) is made of segments that are the instance name, suffixed with #n when siblings share that name (n counts same-named siblings in child order), e.g. Wheel#2/Transparency. A segment is resolved by exact name first, then by the Name#n form, so names containing # still work.

Size budget (rough)​

20 players @ 15 Hz, position+rotation each tick: 20 × 15 × (1+2+1+12+6) ≈ 6.6 KB/s ≈ 4 MB per 10 min raw. Delta thresholds (skip XFORM if moved < 0.05 studs / 0.5°) usually cut that 3–5×. Chunk at ~3.5 MB per DataStore key or push to your own backend.