NSR - themeldingwars/Documentation GitHub Wiki

The Network Stream Replay (NSR) files are better known as replay files since they hold the streaming data which allows to replay in-game recordings.

The NSR format uses the little endian byte order, as such all the numbers are found "reversed" in the hex dump.

The streaming format inside the packets changes rather often, so a replay is only meaningful to a client that speaks the same streaming protocol version. That version is recorded in the description header, see Protocol version. The container itself (the sections described below) has been stable across the builds we have looked at.

010 Editor Template

Implementations

  • Reader and Writer have been implemented in FauFau: Nsr.cs
  • NSRViewer parses the same structures in NSR/
  • nsrinfo is a minimal C++ reader for the header and metadata

Tools

  • NSRViewer - View Firefall Network Stream Relay (NSR) files
  • nsrinfo - C++ CLI tool to extract basic NSR info

Compression

The NSR file is compressed with gzip and can for example get unpacked with 7zip. It's however important to use the gzip and not the normal zip algorithm for compressing the unpacked file again.

7z a -tgzip name.nsr name

Everything below describes the file after it has been decompressed. An uncompressed NSR starts with NSRD, a compressed one with the gzip magic (1F 8B 08, read as the little endian uint 559903 if you want to test for it cheaply).

Layout

NSRD description   48 bytes, at offset 0
NSRI index         variable, starts right after the description
Meta               variable
Packets            until end of file

The description holds the offsets of the other three sections, so nothing has to be guessed, but in practice the sections always appear in this order and back to back.

Format

NSRD - Description

Always 48 bytes.

Field Type Notes
magic char[4] NSRD
version uint32 5
metaOffset uint32 Offset of the meta section. Equal to the combined size of the description and the index
metaLength uint32 Size of the meta section
indexOffset uint32 Offset of the index section, 48, which is also the size of the description
dataOffset uint32 Offset of the first packet
unknown uint32 Always 0
protocolVersion uint32 Streaming protocol version, see Protocol version
timestamp uint64 Unix epoch in microseconds, UTC
unknown uint32 Always 5000
unknown uint32 Always 0

nsrinfo calls metaOffset the header size and NSRViewer calls it the header length, which is the same number seen from the other side: the meta section begins where the header ends.

NSRI - Index

Field Type Notes
magic char[4] NSRI
version uint32 5
unknown uint64 Always 0
count uint32 Number of offsets that follow
indexOffset uint32 Offset of the first entry of offsets, i.e. the next byte
offsets uint32[count] Absolute file offsets of packets, used to seek in a replay

Meta

Everything the client shows about a recording before you play it, plus the position the recording was made from. Strings are null terminated and not length prefixed.

Field Type Notes
version uint32 4
zoneId uint32 See Zones
description cstring The recording description, e.g. (no recording description)
localDateString cstring Local time of the recorder, e.g. Fri Mar 20 17:43:28 2015
position float[3] Where in the zone the recording was made
rotation float[4] Quaternion
characterGuid uint64 See Firefall Guid System
characterName cstring
unknown byte[18]
firefallVersionString cstring e.g. Firefall (v1.5.1962)
timestamp uint64 Unix epoch in microseconds, UTC
month uint32 Real world month
day uint32 Real world day
realYear uint32 Real world year
fictionalYear uint32 In-fiction year, the game is set roughly 200 years ahead
fictionalTime float Clock time as a float, e.g. 22.38 for 22:38
fictionalDateString cstring e.g. Friday, March 20.697 Zulu 2238
padding byte[] Zero padding so the string plus the padding occupies exactly 128 bytes
unknown byte[18]
ingameTime double Per the 010 template
unknown byte[5]

The last 31 bytes are read as one opaque block by FauFau. The split into 18 / double / 5 comes from the 010 template and has not been cross checked.

Packet

Packets run back to back from dataOffset to the end of the file.

Field Type Notes
timestamp uint32 Relative recording time
length uint16 Length of data
messageId uint16 GSS message id, see Views, Updates and Keyframes
data byte[length] The message body

NSRViewer reads timestamp as two uint16 "keyframe order" values instead. Both readings consume the same four bytes.

data starts with the 8 byte entity id the message applies to. The low byte of that id is the entity type, which for networked entities is the controller or view type. That is what lets a tool sort a replay by view without decoding the message bodies:

Id View Id View
0 Generic 26 Vehicle
1 Character 27 Vehicle_BaseController
2 Character_BaseController 28 Vehicle_CombatController
3 Character_NPCController 29 Vehicle_ObserverView
4 Character_MissionAndMarkerController 30 Vehicle_CombatView
5 Character_CombatController 31 Vehicle_MovementView
6 Character_LocalEffectsController 32 Anchor
7 Character_SpectatorController 33 Anchor_AIObserverView
8 Character_ObserverView 34 Deployable
9 Character_EquipmentView 35 Deployable_ObserverView
10 Character_AIObserverView 36 Deployable_NPCObserverView
11 Character_CombatView 37 Deployable_HardpointView
12 Character_MovementView 38 Turret
13 Character_TinyObjectView 39 Turret_BaseController
14 Character_DynamicProjectileView 40 Turret_ObserverView
15 Melding 45 Outpost_ObserverView
16 Melding_ObserverView 48 ResourceNode_ObserverView
17 MeldingBubble 51 CarryableObject_ObserverView
18 MeldingBubble_ObserverView 53 LootStoreExtension_LootObjectView
19 AreaVisualData
20 AreaVisualData_ObserverView
21 AreaVisualData_ParticleEffectsView
22 AreaVisualData_MapMarkerView
23 AreaVisualData_TinyObjectView
24 AreaVisualData_LootObjectView
25 AreaVisualData_ForceShieldView

The gaps are ids we have not seen in a replay. The authoritative list for a given build is the view enum dumped from the client, for example the 1962 dump in Sift.

Protocol version

protocolVersion in the description is the GSS protocol version, not the build number. It is the same opaque uint16 value the client and server agree on for the GSS channels, stored here in a uint32. 19551 is the version used by the 1962 client, which is what AeroMessages currently targets. The raw values are not ordered chronologically, so the only way to map one to a build is a lookup table such as the one Aero generates from the Sift protocol dumps.

Writing a replay

FauFau can generate a file from scratch, which is the shortest description of what a minimal valid replay needs:

  1. Write 48 bytes of nothing, the description gets filled in last.
  2. Write the index at offset 48, remembering the offset as indexOffset.
  3. Write the meta section, remembering its offset and length.
  4. Remember the current offset as dataOffset and write the packets.
  5. Seek back to 0 and write the description now that all four values are known.
  6. gzip the whole thing.
⚠️ **GitHub.com Fallback** ⚠️