t2-demo-parser 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +823 -0
- package/dist/BitStream.d.ts +143 -0
- package/dist/BitStream.d.ts.map +1 -0
- package/dist/BitStream.js +369 -0
- package/dist/BitStream.js.map +1 -0
- package/dist/BitStream.test.d.ts +2 -0
- package/dist/BitStream.test.d.ts.map +1 -0
- package/dist/BitStream.test.js +419 -0
- package/dist/BitStream.test.js.map +1 -0
- package/dist/ClassRegistry.d.ts +100 -0
- package/dist/ClassRegistry.d.ts.map +1 -0
- package/dist/ClassRegistry.js +118 -0
- package/dist/ClassRegistry.js.map +1 -0
- package/dist/ClassRegistry.test.d.ts +2 -0
- package/dist/ClassRegistry.test.d.ts.map +1 -0
- package/dist/ClassRegistry.test.js +83 -0
- package/dist/ClassRegistry.test.js.map +1 -0
- package/dist/DataBlockParsers.d.ts +3 -0
- package/dist/DataBlockParsers.d.ts.map +1 -0
- package/dist/DataBlockParsers.js +2041 -0
- package/dist/DataBlockParsers.js.map +1 -0
- package/dist/DemoParser.d.ts +155 -0
- package/dist/DemoParser.d.ts.map +1 -0
- package/dist/DemoParser.js +943 -0
- package/dist/DemoParser.js.map +1 -0
- package/dist/DemoParser.test.d.ts +2 -0
- package/dist/DemoParser.test.d.ts.map +1 -0
- package/dist/DemoParser.test.js +472 -0
- package/dist/DemoParser.test.js.map +1 -0
- package/dist/EventParsers.d.ts +3 -0
- package/dist/EventParsers.d.ts.map +1 -0
- package/dist/EventParsers.js +603 -0
- package/dist/EventParsers.js.map +1 -0
- package/dist/GhostManager.d.ts +13 -0
- package/dist/GhostManager.d.ts.map +1 -0
- package/dist/GhostManager.js +2002 -0
- package/dist/GhostManager.js.map +1 -0
- package/dist/GhostTracker.test.d.ts +2 -0
- package/dist/GhostTracker.test.d.ts.map +1 -0
- package/dist/GhostTracker.test.js +64 -0
- package/dist/GhostTracker.test.js.map +1 -0
- package/dist/HuffmanProcessor.d.ts +12 -0
- package/dist/HuffmanProcessor.d.ts.map +1 -0
- package/dist/HuffmanProcessor.js +163 -0
- package/dist/HuffmanProcessor.js.map +1 -0
- package/dist/LiveParser.d.ts +16 -0
- package/dist/LiveParser.d.ts.map +1 -0
- package/dist/LiveParser.js +29 -0
- package/dist/LiveParser.js.map +1 -0
- package/dist/PacketParser.d.ts +104 -0
- package/dist/PacketParser.d.ts.map +1 -0
- package/dist/PacketParser.js +713 -0
- package/dist/PacketParser.js.map +1 -0
- package/dist/Timeline.d.ts +101 -0
- package/dist/Timeline.d.ts.map +1 -0
- package/dist/Timeline.js +251 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +372 -0
- package/dist/cli.js.map +1 -0
- package/dist/dataBlockDataTypes.d.ts +877 -0
- package/dist/dataBlockDataTypes.d.ts.map +1 -0
- package/dist/dataBlockDataTypes.js +2 -0
- package/dist/dataBlockDataTypes.js.map +1 -0
- package/dist/dataTypes.d.ts +31 -0
- package/dist/dataTypes.d.ts.map +1 -0
- package/dist/dataTypes.js +5 -0
- package/dist/dataTypes.js.map +1 -0
- package/dist/dump.d.ts +3 -0
- package/dist/dump.d.ts.map +1 -0
- package/dist/dump.js +36 -0
- package/dist/dump.js.map +1 -0
- package/dist/eventDataTypes.d.ts +199 -0
- package/dist/eventDataTypes.d.ts.map +1 -0
- package/dist/eventDataTypes.js +2 -0
- package/dist/eventDataTypes.js.map +1 -0
- package/dist/ghostDataTypes.d.ts +573 -0
- package/dist/ghostDataTypes.d.ts.map +1 -0
- package/dist/ghostDataTypes.js +2 -0
- package/dist/ghostDataTypes.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +261 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +204 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
# t2-demo-parser
|
|
2
|
+
|
|
3
|
+
Parser for Tribes 2 demo recordings (`.rec` files). Extracts game state, player
|
|
4
|
+
movement, ghost object lifecycles, network events, and animation timelines from
|
|
5
|
+
recordings made by the Tribes 2 client (build 25034, Torque engine).
|
|
6
|
+
|
|
7
|
+
Designed for use in browser-based replay viewers. The async API keeps the main
|
|
8
|
+
thread responsive: in browsers, decompression runs in a Web Worker via
|
|
9
|
+
[fflate](https://github.com/101arrowz/fflate), and blocks are parsed lazily
|
|
10
|
+
one at a time.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { DemoParser, buildTimeline } from "t2-demo-parser";
|
|
16
|
+
|
|
17
|
+
const buffer = new Uint8Array(/* .rec file contents */);
|
|
18
|
+
const parser = new DemoParser(buffer);
|
|
19
|
+
const demo = await parser.parseFullDemo();
|
|
20
|
+
|
|
21
|
+
console.log(demo.header.demoLengthMs); // Duration in ms
|
|
22
|
+
console.log(demo.initialBlock.missionName); // e.g. "Rollercoaster"
|
|
23
|
+
console.log(demo.blocks.length); // Total block count
|
|
24
|
+
console.log(demo.initialBlock.dataBlocks.size); // DataBlock definitions
|
|
25
|
+
|
|
26
|
+
const timeline = buildTimeline(demo, parser.getRegistry());
|
|
27
|
+
console.log(timeline.controlObject.length); // Player position keyframes
|
|
28
|
+
console.log(timeline.ghostInstances.length); // Networked object lifecycles
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CLI
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx t2-demo-parser path/to/demo.rec
|
|
35
|
+
npx t2-demo-parser path/to/demo.rec --json # export timeline JSON
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Concepts
|
|
41
|
+
|
|
42
|
+
A `.rec` file contains a snapshot of the game state at the moment recording
|
|
43
|
+
started (the **initial block**), followed by a compressed stream of
|
|
44
|
+
**blocks** — network packets, player inputs, and timing info — representing
|
|
45
|
+
everything the client received from the server during the recording.
|
|
46
|
+
|
|
47
|
+
- **Header**: file signature, protocol version, total duration, initial block size.
|
|
48
|
+
- **Initial block**: DataBlock definitions (shapes, sounds, projectile types),
|
|
49
|
+
player scores, target info, connection state, initial ghost objects, and the
|
|
50
|
+
mission/map name.
|
|
51
|
+
- **Block stream**: deflate-compressed sequence of blocks, each tagged with a
|
|
52
|
+
type and size. Block types are packets (server-to-client network data), move
|
|
53
|
+
inputs (player controls), info blocks (tick/FOV), and send-packet triggers.
|
|
54
|
+
- **Ghosts**: Torque's term for networked game objects. The server creates,
|
|
55
|
+
updates, and destroys ghosts; each has a class (Player, Turret, Projectile,
|
|
56
|
+
etc.) and an index (0–1023). Ghost state is delta-compressed with bitmasks.
|
|
57
|
+
- **Events**: one-shot network messages (remote commands, audio triggers,
|
|
58
|
+
DataBlock transfers, etc.).
|
|
59
|
+
- **DataBlocks**: static definitions shared between client and server
|
|
60
|
+
(PlayerData, WeaponData, VehicleData, etc.), referenced by ghost objects.
|
|
61
|
+
- **Timeline**: a high-level view extracted after parsing — timestamped
|
|
62
|
+
keyframes for every ghost and the control object, suitable for driving
|
|
63
|
+
Three.js animations.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## API reference
|
|
68
|
+
|
|
69
|
+
### `DemoParser`
|
|
70
|
+
|
|
71
|
+
Main entry point. Construct with a `Uint8Array` of the `.rec` file contents.
|
|
72
|
+
All parser bindings (53 ghost classes, 54 DataBlock classes, 26 event classes)
|
|
73
|
+
are set up deterministically in the constructor.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const parser = new DemoParser(buffer: Uint8Array);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### `async load(): Promise<LoadResult>`
|
|
80
|
+
|
|
81
|
+
Parse the header and initial block, then asynchronously decompress the block
|
|
82
|
+
stream. Does **not** parse any blocks — use `nextBlock()` to consume them.
|
|
83
|
+
|
|
84
|
+
Idempotent: calling `load()` again returns the cached result.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
interface LoadResult {
|
|
88
|
+
header: DemoHeader;
|
|
89
|
+
initialBlock: InitialBlockData;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### `nextBlock(): DemoBlock | undefined`
|
|
94
|
+
|
|
95
|
+
Read and parse the next block from the decompressed stream. Returns `undefined`
|
|
96
|
+
when the stream is exhausted. Each call advances `blockCursor` by one.
|
|
97
|
+
|
|
98
|
+
Blocks are transient — only one exists in memory at a time (previous blocks are
|
|
99
|
+
eligible for GC unless you retain a reference).
|
|
100
|
+
|
|
101
|
+
#### `processBlocks(count: number): number`
|
|
102
|
+
|
|
103
|
+
Fast-forward through `count` blocks, processing each (updating ghost tracker,
|
|
104
|
+
connection state, etc.) but not retaining them. Returns the number of blocks
|
|
105
|
+
actually processed, which may be less than `count` if the stream runs out.
|
|
106
|
+
|
|
107
|
+
#### `reset(): void`
|
|
108
|
+
|
|
109
|
+
Reset the block stream to the beginning, re-seed the ghost tracker from the
|
|
110
|
+
initial block's ghosts, and create a fresh `PacketParser`. Allows replaying
|
|
111
|
+
the entire block stream from scratch.
|
|
112
|
+
|
|
113
|
+
#### `async parseFullDemo(): Promise<DemoFile>`
|
|
114
|
+
|
|
115
|
+
Convenience method: `load()` + drain all blocks into an array.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
interface DemoFile {
|
|
119
|
+
header: DemoHeader;
|
|
120
|
+
initialBlock: InitialBlockData;
|
|
121
|
+
blocks: DemoBlock[];
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Properties
|
|
126
|
+
|
|
127
|
+
| Property | Type | Description |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `loaded` | `boolean` | Whether `load()` has been called. |
|
|
130
|
+
| `header` | `DemoHeader` | File header (throws if not loaded). |
|
|
131
|
+
| `initialBlock` | `InitialBlockData` | Initial game state (throws if not loaded). |
|
|
132
|
+
| `blockCount` | `number` | Total blocks in the stream. Lazily computed on first access by scanning the decompressed buffer. |
|
|
133
|
+
| `blockCursor` | `number` | Number of blocks consumed so far. |
|
|
134
|
+
|
|
135
|
+
#### Accessors
|
|
136
|
+
|
|
137
|
+
| Method | Returns | Description |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `getRegistry()` | `ClassRegistry` | Parser registry with all bindings. |
|
|
140
|
+
| `getGhostTracker()` | `GhostTracker` | Current ghost state (mutated by `nextBlock()`). |
|
|
141
|
+
| `getPacketParser()` | `PacketParser` | Packet parser with parse statistics. |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### `DemoHeader`
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
interface DemoHeader {
|
|
149
|
+
identString: string; // "Tribes2 Recording"
|
|
150
|
+
protocolVersion: number; // 0x330004
|
|
151
|
+
demoLengthMs: number; // Total recording duration in milliseconds
|
|
152
|
+
initialBlockSize: number; // Byte size of the initial block
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### `InitialBlockData`
|
|
159
|
+
|
|
160
|
+
Snapshot of the game state at the moment recording started.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
interface InitialBlockData {
|
|
164
|
+
// DataBlocks (static game definitions)
|
|
165
|
+
dataBlocks: Map<number, ParsedDataBlock>; // objectId → parsed DataBlock
|
|
166
|
+
dataBlockCount: number;
|
|
167
|
+
dataBlockHeaders: DataBlockHeader[];
|
|
168
|
+
|
|
169
|
+
// Player/team state
|
|
170
|
+
scoreEntries: ScoreEntry[];
|
|
171
|
+
targetEntries: TargetEntry[];
|
|
172
|
+
sensorGroupColors: SensorGroupColor[];
|
|
173
|
+
|
|
174
|
+
// Connection state
|
|
175
|
+
connectionState: ConnectionProtocolState;
|
|
176
|
+
roundTripTime: number;
|
|
177
|
+
packetLoss: number;
|
|
178
|
+
|
|
179
|
+
// Ghost objects present at recording start
|
|
180
|
+
initialGhosts: GhostUpdate[];
|
|
181
|
+
initialEvents: NetEventInfo[];
|
|
182
|
+
|
|
183
|
+
// Control object (the recording player)
|
|
184
|
+
controlObjectGhostIndex: number; // -1 if none
|
|
185
|
+
controlObjectData?: Record<string, unknown>;
|
|
186
|
+
firstPerson: boolean;
|
|
187
|
+
|
|
188
|
+
// Mission info
|
|
189
|
+
missionName: string;
|
|
190
|
+
missionCRC: number;
|
|
191
|
+
|
|
192
|
+
// Misc
|
|
193
|
+
taggedStrings: Map<number, string>;
|
|
194
|
+
demoValues: string[];
|
|
195
|
+
connectionFields: number[];
|
|
196
|
+
stateArray: number[];
|
|
197
|
+
pathManager: PathManagerEntry[];
|
|
198
|
+
notifyCount: number;
|
|
199
|
+
nextRecvEventSeq: number;
|
|
200
|
+
ghostingSequence: number;
|
|
201
|
+
|
|
202
|
+
// Validation
|
|
203
|
+
phase2Valid?: boolean;
|
|
204
|
+
phase2Error?: string;
|
|
205
|
+
phase2TrailingBits?: number;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### `DemoBlock`
|
|
212
|
+
|
|
213
|
+
A single block from the compressed block stream.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
interface DemoBlock {
|
|
217
|
+
index: number;
|
|
218
|
+
type: number; // BlockTypePacket (0), BlockTypeSendPacket (1),
|
|
219
|
+
// BlockTypeMove (2), or BlockTypeInfo (3)
|
|
220
|
+
size: number; // Payload size in bytes
|
|
221
|
+
data: Uint8Array; // Raw payload
|
|
222
|
+
parsed?: PacketData | Move | InfoBlock;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### `PacketData`
|
|
229
|
+
|
|
230
|
+
Parsed contents of a network packet block.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
interface PacketData {
|
|
234
|
+
dnetHeader: DnetHeader;
|
|
235
|
+
rateInfo: RateInfo;
|
|
236
|
+
gameState: GameState;
|
|
237
|
+
events: NetEventInfo[];
|
|
238
|
+
ghosts: GhostUpdate[];
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### `GameState`
|
|
243
|
+
|
|
244
|
+
Per-packet game state from the server.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
interface GameState {
|
|
248
|
+
lastMoveAck: number;
|
|
249
|
+
damageFlash?: number;
|
|
250
|
+
whiteOut?: number;
|
|
251
|
+
pinged: boolean;
|
|
252
|
+
jammed: boolean;
|
|
253
|
+
controlObjectGhostIndex?: number;
|
|
254
|
+
controlObjectData?: Record<string, unknown>;
|
|
255
|
+
compressionPoint?: { x: number; y: number; z: number };
|
|
256
|
+
cameraFov?: number;
|
|
257
|
+
targetVisibility?: { index: number; mask: number }[];
|
|
258
|
+
|
|
259
|
+
// Seeker/lock-on fields
|
|
260
|
+
selfLocked?: boolean;
|
|
261
|
+
selfHomed?: boolean;
|
|
262
|
+
seekerTracking?: boolean;
|
|
263
|
+
seekerTrackingPos?: { x: number; y: number; z: number };
|
|
264
|
+
seekerMode?: number;
|
|
265
|
+
seekerObjectGhostIndex?: number;
|
|
266
|
+
targetPos?: { x: number; y: number; z: number };
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### `GhostUpdate`
|
|
271
|
+
|
|
272
|
+
A create, update, or delete operation on a ghost object.
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
interface GhostUpdate {
|
|
276
|
+
index: number; // Ghost slot (0–1023)
|
|
277
|
+
type: "create" | "update" | "delete";
|
|
278
|
+
classId?: number; // Set on create
|
|
279
|
+
updateBitsStart: number;
|
|
280
|
+
updateBitsEnd: number;
|
|
281
|
+
parsedData?: Record<string, unknown>; // Class-specific parsed fields
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### `NetEventInfo`
|
|
286
|
+
|
|
287
|
+
A network event received from the server.
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
interface NetEventInfo {
|
|
291
|
+
classId: number;
|
|
292
|
+
guaranteed: boolean;
|
|
293
|
+
sequenceNumber?: number;
|
|
294
|
+
dataBitsStart: number;
|
|
295
|
+
dataBitsEnd: number;
|
|
296
|
+
parsedData?: Record<string, unknown>;
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### `Move`
|
|
303
|
+
|
|
304
|
+
Raw 64-byte player input struct (from type 2 blocks).
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
interface Move {
|
|
308
|
+
x: number; y: number; z: number; // Acceleration
|
|
309
|
+
yaw: number; pitch: number; roll: number; // Rotation
|
|
310
|
+
px: number; py: number; pz: number; // Previous acceleration
|
|
311
|
+
pyaw: number; ppitch: number; proll: number;
|
|
312
|
+
id: number;
|
|
313
|
+
sendCount: number;
|
|
314
|
+
freeLook: boolean;
|
|
315
|
+
trigger: boolean[]; // 6 trigger keys (fire, jet, jump, etc.)
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### `ParsedDataBlock`
|
|
322
|
+
|
|
323
|
+
A static game object definition parsed from the initial block.
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
interface ParsedDataBlock {
|
|
327
|
+
classId: number;
|
|
328
|
+
className: string; // e.g. "PlayerData", "WheeledVehicleData"
|
|
329
|
+
objectId: number;
|
|
330
|
+
data: Record<string, unknown>; // Class-specific fields (shapeName, etc.)
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
### Timeline functions
|
|
337
|
+
|
|
338
|
+
#### `buildTimeline(demo, registry): DemoTimeline`
|
|
339
|
+
|
|
340
|
+
Extract a time-indexed timeline from a fully parsed `DemoFile`. Timestamps are
|
|
341
|
+
derived by distributing packets evenly across the demo duration.
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { buildTimeline } from "t2-demo-parser";
|
|
345
|
+
|
|
346
|
+
const timeline = buildTimeline(demo, parser.getRegistry());
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
#### `getTimelineStats(timeline): TimelineStats`
|
|
350
|
+
|
|
351
|
+
Compute summary statistics from a timeline.
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
interface TimelineStats {
|
|
355
|
+
durationMs: number;
|
|
356
|
+
tickIntervalMs: number;
|
|
357
|
+
packetCount: number;
|
|
358
|
+
controlObjectKeyframes: number;
|
|
359
|
+
totalGhostInstances: number;
|
|
360
|
+
ghostInstancesByClass: Map<string, number>;
|
|
361
|
+
ghostKeyframesByClass: Map<string, number>;
|
|
362
|
+
ghostsWithPosition: number;
|
|
363
|
+
totalEvents: number;
|
|
364
|
+
eventsByType: Map<string, number>;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### `exportTimeline(timeline): ExportTimeline`
|
|
369
|
+
|
|
370
|
+
Convert a timeline to a compact JSON-serializable format for Three.js.
|
|
371
|
+
Positions and rotations are packed into arrays, and only ghost instances
|
|
372
|
+
with position data are included.
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
interface ExportTimeline {
|
|
376
|
+
durationMs: number;
|
|
377
|
+
tickIntervalMs: number;
|
|
378
|
+
controlObject: { t: number; p?: [number, number, number]; v?: [number, number, number] }[];
|
|
379
|
+
ghosts: ExportGhostInstance[];
|
|
380
|
+
events: GameEvent[];
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### `DemoTimeline`
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
interface DemoTimeline {
|
|
388
|
+
durationMs: number;
|
|
389
|
+
tickIntervalMs: number;
|
|
390
|
+
packetCount: number;
|
|
391
|
+
ghostInstances: GhostInstance[];
|
|
392
|
+
controlObject: ControlObjectKeyframe[];
|
|
393
|
+
events: GameEvent[];
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
#### `GhostInstance`
|
|
398
|
+
|
|
399
|
+
A continuous lifecycle of a ghost object, from creation to deletion.
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
interface GhostInstance {
|
|
403
|
+
ghostIndex: number;
|
|
404
|
+
classId: number;
|
|
405
|
+
className: string;
|
|
406
|
+
spawnTime: number;
|
|
407
|
+
despawnTime?: number;
|
|
408
|
+
keyframes: GhostKeyframe[];
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
#### `GhostKeyframe`
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
interface GhostKeyframe {
|
|
416
|
+
time: number;
|
|
417
|
+
position?: Vec3;
|
|
418
|
+
rotation?: Quat | Vec3;
|
|
419
|
+
velocity?: Vec3;
|
|
420
|
+
data?: Record<string, unknown>;
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
#### `ControlObjectKeyframe`
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
interface ControlObjectKeyframe {
|
|
428
|
+
time: number;
|
|
429
|
+
ghostIndex: number;
|
|
430
|
+
position?: Vec3;
|
|
431
|
+
velocity?: Vec3;
|
|
432
|
+
data?: Record<string, unknown>;
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### `PacketParser`
|
|
439
|
+
|
|
440
|
+
Available via `parser.getPacketParser()`. Exposes parse statistics.
|
|
441
|
+
|
|
442
|
+
| Property | Type | Description |
|
|
443
|
+
|---|---|---|
|
|
444
|
+
| `packetsParsed` | `number` | Total packets successfully parsed. |
|
|
445
|
+
| `ghostCreatesParsed` | `number` | Ghost create operations parsed. |
|
|
446
|
+
| `ghostUpdatesParsed` | `number` | Ghost update operations parsed. |
|
|
447
|
+
| `ghostDeletes` | `number` | Ghost delete operations. |
|
|
448
|
+
| `ghostsFailed` | `number` | Ghost operations that failed to parse. |
|
|
449
|
+
| `ghostsTrackerDiverged` | `number` | Ghost tracker inconsistencies detected. |
|
|
450
|
+
| `eventsParsed` | `number` | Events parsed. |
|
|
451
|
+
| `eventsFailed` | `number` | Events that failed to parse. |
|
|
452
|
+
| `controlObjectParsed` | `number` | Control object updates parsed. |
|
|
453
|
+
| `controlObjectFailed` | `number` | Control object updates that failed. |
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### `createLiveParser(): LiveParserKit`
|
|
458
|
+
|
|
459
|
+
Create a parser stack for live server connections. Sets up the same deterministic
|
|
460
|
+
registry bindings as `DemoParser` but without requiring a `.rec` file. Useful for
|
|
461
|
+
parsing packets from a live Tribes 2 connection (e.g. via a network proxy).
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
import { createLiveParser } from "t2-demo-parser";
|
|
465
|
+
|
|
466
|
+
const { registry, ghostTracker, packetParser } = createLiveParser();
|
|
467
|
+
// Feed raw packet data through packetParser...
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
interface LiveParserKit {
|
|
472
|
+
registry: ClassRegistry;
|
|
473
|
+
ghostTracker: GhostTracker;
|
|
474
|
+
packetParser: PacketParser;
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
### `BitStream`
|
|
481
|
+
|
|
482
|
+
Low-level bit-packed data reader used internally. Exported for advanced use
|
|
483
|
+
cases like reading raw block payloads or implementing custom parsers.
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
import { BitStream } from "t2-demo-parser";
|
|
487
|
+
|
|
488
|
+
const bs = new BitStream(uint8Array);
|
|
489
|
+
const flag = bs.readFlag();
|
|
490
|
+
const value = bs.readInt(10);
|
|
491
|
+
const str = bs.readString();
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
### `GhostTracker`
|
|
497
|
+
|
|
498
|
+
Tracks the live state of all ghost objects. Available via
|
|
499
|
+
`parser.getGhostTracker()` or from `createLiveParser()`.
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
const tracker = parser.getGhostTracker();
|
|
503
|
+
const ghost = tracker.getGhost(index); // GhostEntry | undefined
|
|
504
|
+
const all = tracker.getAllGhosts(); // Map<number, GhostEntry>
|
|
505
|
+
tracker.size(); // Number of active ghosts
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
interface GhostEntry {
|
|
510
|
+
classId: number;
|
|
511
|
+
className: string;
|
|
512
|
+
state: Record<string, unknown>;
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
### Constants
|
|
519
|
+
|
|
520
|
+
Block type constants for filtering `DemoBlock.type`:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import {
|
|
524
|
+
BlockTypePacket, // 0 — network packet
|
|
525
|
+
BlockTypeSendPacket, // 1 — send-packet trigger (no data)
|
|
526
|
+
BlockTypeMove, // 2 — 64-byte player input
|
|
527
|
+
BlockTypeInfo, // 3 — 8-byte timing/FOV
|
|
528
|
+
} from "t2-demo-parser";
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Network protocol constants are also exported:
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
import {
|
|
535
|
+
MaxGhostCount, // 1024
|
|
536
|
+
GhostIdBitSize, // 10
|
|
537
|
+
NetStringTableMaxStrings, // 4096
|
|
538
|
+
StringIdBitSize, // 12
|
|
539
|
+
NetEventClassBitSize, // 6
|
|
540
|
+
NetEventClassFirst, // 255
|
|
541
|
+
NetObjectClassBitSize, // 7
|
|
542
|
+
NetObjectClassFirst, // 0
|
|
543
|
+
MaxPacketDataSize, // 1500
|
|
544
|
+
MaxTriggerKeys, // 6
|
|
545
|
+
DataBlockObjectIdFirst, // 3
|
|
546
|
+
DataBlockObjectIdBitSize, // 10
|
|
547
|
+
DataBlockClassFirst, // 128
|
|
548
|
+
DataBlockClassBitSize, // 7
|
|
549
|
+
} from "t2-demo-parser";
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Class name arrays (sorted by C `strcmp`, matching the deterministic classId
|
|
553
|
+
assignment order):
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
import {
|
|
557
|
+
NetObjectClassNames, // 53 ghost class names
|
|
558
|
+
DataBlockClassNames, // 54 DataBlock class names
|
|
559
|
+
NetEventClassNames, // 26 event class names
|
|
560
|
+
} from "t2-demo-parser";
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Guides
|
|
566
|
+
|
|
567
|
+
### Get demo metadata without parsing blocks
|
|
568
|
+
|
|
569
|
+
Use `load()` to parse the header and initial block. This is fast (header +
|
|
570
|
+
initial block + async decompress) and gives you everything you need to set up
|
|
571
|
+
a scene before playing through the recording.
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
const parser = new DemoParser(buffer);
|
|
575
|
+
const { header, initialBlock } = await parser.load();
|
|
576
|
+
|
|
577
|
+
// Duration
|
|
578
|
+
const durationMs = header.demoLengthMs;
|
|
579
|
+
const durationStr = `${Math.floor(durationMs / 60000)}m${
|
|
580
|
+
Math.floor((durationMs % 60000) / 1000).toString().padStart(2, "0")}s`;
|
|
581
|
+
|
|
582
|
+
// Mission / map name
|
|
583
|
+
const mission = initialBlock.missionName; // e.g. "Rollercoaster"
|
|
584
|
+
|
|
585
|
+
// Game mode / mission type — look in DemoValues
|
|
586
|
+
// $DemoValue_0 is typically the game mode (e.g. "CTFGame")
|
|
587
|
+
const gameMode = initialBlock.demoValues[0];
|
|
588
|
+
|
|
589
|
+
// Teams and players
|
|
590
|
+
for (const score of initialBlock.scoreEntries) {
|
|
591
|
+
console.log(`Team ${score.teamId}: client ${score.clientId}, score ${score.score}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Player names from the target table
|
|
595
|
+
for (const target of initialBlock.targetEntries) {
|
|
596
|
+
if (target.name) {
|
|
597
|
+
console.log(`${target.name} (team ${target.sensorGroup})`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// DataBlocks (shapes, vehicles, weapons loaded for this mission)
|
|
602
|
+
for (const [id, db] of initialBlock.dataBlocks) {
|
|
603
|
+
if (db.data.shapeName) {
|
|
604
|
+
console.log(`${db.className}: ${db.data.shapeName}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Initial ghost objects (world state at recording start)
|
|
609
|
+
console.log(`${initialBlock.initialGhosts.length} ghosts at start`);
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Stream blocks one at a time (low memory)
|
|
613
|
+
|
|
614
|
+
After `load()`, call `nextBlock()` in a loop. Each block is parsed on demand
|
|
615
|
+
and can be released immediately. This avoids holding the entire block array
|
|
616
|
+
in memory.
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
const parser = new DemoParser(buffer);
|
|
620
|
+
await parser.load();
|
|
621
|
+
|
|
622
|
+
let block;
|
|
623
|
+
while ((block = parser.nextBlock())) {
|
|
624
|
+
if (block.type === BlockTypePacket && block.parsed) {
|
|
625
|
+
const pkt = block.parsed as PacketData;
|
|
626
|
+
// Process packet...
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Seek to a specific time
|
|
632
|
+
|
|
633
|
+
There's no random-access index — blocks must be processed sequentially to
|
|
634
|
+
maintain ghost tracker state. To seek, reset and fast-forward.
|
|
635
|
+
|
|
636
|
+
Move blocks (type 2) are sent at a fixed 32ms tick rate. Count them to estimate
|
|
637
|
+
elapsed time.
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { BlockTypeMove } from "t2-demo-parser";
|
|
641
|
+
|
|
642
|
+
const parser = new DemoParser(buffer);
|
|
643
|
+
await parser.load();
|
|
644
|
+
|
|
645
|
+
function seekTo(targetMs: number) {
|
|
646
|
+
parser.reset();
|
|
647
|
+
let moveCount = 0;
|
|
648
|
+
let block;
|
|
649
|
+
while ((block = parser.nextBlock())) {
|
|
650
|
+
if (block.type === BlockTypeMove) moveCount++;
|
|
651
|
+
if (moveCount * 32 >= targetMs) break;
|
|
652
|
+
}
|
|
653
|
+
return moveCount * 32; // Actual time reached
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const actualMs = seekTo(60000); // Seek to ~1 minute
|
|
657
|
+
// Ghost tracker now reflects state at that point.
|
|
658
|
+
// Continue calling nextBlock() to play forward from here.
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Build a Three.js animation timeline
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
import {
|
|
665
|
+
DemoParser,
|
|
666
|
+
buildTimeline,
|
|
667
|
+
getTimelineStats,
|
|
668
|
+
exportTimeline,
|
|
669
|
+
} from "t2-demo-parser";
|
|
670
|
+
|
|
671
|
+
const parser = new DemoParser(buffer);
|
|
672
|
+
const demo = await parser.parseFullDemo();
|
|
673
|
+
const timeline = buildTimeline(demo, parser.getRegistry());
|
|
674
|
+
|
|
675
|
+
// Summary
|
|
676
|
+
const stats = getTimelineStats(timeline);
|
|
677
|
+
console.log(`${stats.controlObjectKeyframes} player keyframes`);
|
|
678
|
+
console.log(`${stats.totalGhostInstances} ghost instances`);
|
|
679
|
+
console.log(`${stats.ghostsWithPosition} with position data`);
|
|
680
|
+
|
|
681
|
+
// Iterate ghost lifecycles
|
|
682
|
+
for (const inst of timeline.ghostInstances) {
|
|
683
|
+
console.log(`${inst.className} #${inst.ghostIndex}: ${inst.keyframes.length} keyframes`);
|
|
684
|
+
for (const kf of inst.keyframes) {
|
|
685
|
+
if (kf.position) {
|
|
686
|
+
// Feed into Three.js KeyframeTrack...
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Export compact JSON for Three.js
|
|
692
|
+
const exported = exportTimeline(timeline);
|
|
693
|
+
// exported.controlObject — player position/velocity keyframes
|
|
694
|
+
// exported.ghosts — ghost position/rotation/velocity keyframes
|
|
695
|
+
// exported.events — timestamped game events
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### Replay and compare (deterministic)
|
|
699
|
+
|
|
700
|
+
Parsing is fully deterministic. Resetting and replaying produces identical
|
|
701
|
+
results.
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
const parser = new DemoParser(buffer);
|
|
705
|
+
await parser.load();
|
|
706
|
+
|
|
707
|
+
// First pass
|
|
708
|
+
while (parser.nextBlock()) {}
|
|
709
|
+
const stats1 = parser.getPacketParser().packetsParsed;
|
|
710
|
+
|
|
711
|
+
// Reset and replay
|
|
712
|
+
parser.reset();
|
|
713
|
+
while (parser.nextBlock()) {}
|
|
714
|
+
const stats2 = parser.getPacketParser().packetsParsed;
|
|
715
|
+
|
|
716
|
+
console.log(stats1 === stats2); // true
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Access parse statistics
|
|
720
|
+
|
|
721
|
+
After parsing, check the `PacketParser` stats to verify data quality.
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
const parser = new DemoParser(buffer);
|
|
725
|
+
await parser.parseFullDemo();
|
|
726
|
+
const pp = parser.getPacketParser();
|
|
727
|
+
|
|
728
|
+
console.log(`Packets: ${pp.packetsParsed}`);
|
|
729
|
+
console.log(`Ghost creates: ${pp.ghostCreatesParsed}`);
|
|
730
|
+
console.log(`Ghost updates: ${pp.ghostUpdatesParsed}`);
|
|
731
|
+
console.log(`Ghost deletes: ${pp.ghostDeletes}`);
|
|
732
|
+
console.log(`Ghost failures: ${pp.ghostsFailed}`);
|
|
733
|
+
console.log(`Events: ${pp.eventsParsed}`);
|
|
734
|
+
console.log(`Control object: ${pp.controlObjectParsed}`);
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
A healthy parse has 0 ghost failures and 0 tracker divergences.
|
|
738
|
+
|
|
739
|
+
### Identify ghost objects by class
|
|
740
|
+
|
|
741
|
+
The 53 ghost classes (Player, Turret, FlyingVehicle, etc.) are bound
|
|
742
|
+
deterministically. Use the registry to look up class names.
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
const registry = parser.getRegistry();
|
|
746
|
+
|
|
747
|
+
// From a ghost update in a packet:
|
|
748
|
+
if (ghost.type === "create" && ghost.classId !== undefined) {
|
|
749
|
+
const entry = registry.getGhostParser(ghost.classId);
|
|
750
|
+
console.log(entry?.name); // e.g. "Player", "Turret", "LinearProjectile"
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// From the ghost tracker (live state):
|
|
754
|
+
const tracker = parser.getGhostTracker();
|
|
755
|
+
const ghostEntry = tracker.getGhost(ghostIndex);
|
|
756
|
+
if (ghostEntry) {
|
|
757
|
+
console.log(ghostEntry.className, ghostEntry.classId);
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Debug logging
|
|
762
|
+
|
|
763
|
+
The parser uses the [`debug`](https://www.npmjs.com/package/debug) package.
|
|
764
|
+
Enable namespaces to see detailed parse output:
|
|
765
|
+
|
|
766
|
+
```bash
|
|
767
|
+
# All parser output
|
|
768
|
+
DEBUG=t2-demo-parser* npx t2-demo-parser demo.rec
|
|
769
|
+
|
|
770
|
+
# Just initial block parsing
|
|
771
|
+
DEBUG=t2-demo-parser:initial npx t2-demo-parser demo.rec
|
|
772
|
+
|
|
773
|
+
# Just block stream parsing
|
|
774
|
+
DEBUG=t2-demo-parser:blocks npx t2-demo-parser demo.rec
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
## Supported classes
|
|
780
|
+
|
|
781
|
+
### Ghost classes (53)
|
|
782
|
+
|
|
783
|
+
AIObjective, AudioEmitter, BeaconObject, BombProjectile, Camera, Debris,
|
|
784
|
+
ELFProjectile, EnergyProjectile, FireballAtmosphere, FlareProjectile,
|
|
785
|
+
FlyingVehicle, ForceFieldBare, GameBase, GrenadeProjectile, HoverVehicle,
|
|
786
|
+
InteriorInstance, Item, Lightning, LinearFlareProjectile, LinearProjectile,
|
|
787
|
+
Marker, MissionArea, MissionMarker, ParticleEmissionDummy, PhysicalZone,
|
|
788
|
+
Player, Precipitation, Projectile, RepairProjectile, ScopeAlwaysShape,
|
|
789
|
+
SeekerProjectile, ShapeBase, ShockLanceProjectile, Shockwave,
|
|
790
|
+
SimpleNetObject, Sky, SniperProjectile, SpawnSphere, Splash, StaticShape,
|
|
791
|
+
StationFXPersonal, StationFXVehicle, Sun, TSStatic, TargetProjectile,
|
|
792
|
+
TerrainBlock, TracerProjectile, Trigger, Turret, VehicleBlocker,
|
|
793
|
+
WaterBlock, WayPoint, WheeledVehicle.
|
|
794
|
+
|
|
795
|
+
### DataBlock classes (54)
|
|
796
|
+
|
|
797
|
+
AudioDescription, AudioEnvironment, AudioProfile, AudioSampleEnvironment,
|
|
798
|
+
BombProjectileData, CameraData, CannedChatItem, CommanderIconData,
|
|
799
|
+
DebrisData, DecalData, ELFProjectileData, EffectProfile,
|
|
800
|
+
EnergyProjectileData, ExplosionData, FireballAtmosphereData,
|
|
801
|
+
FlareProjectileData, FlyingVehicleData, ForceFieldBareData, GameBaseData,
|
|
802
|
+
GrenadeProjectileData, HoverVehicleData, ItemData, JetEffectData,
|
|
803
|
+
LightningData, LinearFlareProjectileData, LinearProjectileData,
|
|
804
|
+
MissionMarkerData, ParticleData, ParticleEmissionDummyData,
|
|
805
|
+
ParticleEmitterData, PlayerData, PrecipitationData, ProjectileData,
|
|
806
|
+
RepairProjectileData, RunningLightData, SeekerProjectileData, SensorData,
|
|
807
|
+
ShapeBaseData, ShapeBaseImageData, ShockLanceProjectileData,
|
|
808
|
+
ShockwaveData, SimDataBlock, SniperProjectileData, SplashData,
|
|
809
|
+
StaticShapeData, StationFXPersonalData, StationFXVehicleData,
|
|
810
|
+
TSShapeConstructor, TargetProjectileData, TracerProjectileData,
|
|
811
|
+
TriggerData, TurretData, TurretImageData, WheeledVehicleData.
|
|
812
|
+
|
|
813
|
+
### Event classes (26)
|
|
814
|
+
|
|
815
|
+
CRCChallengeEvent, CRCChallengeResponseEvent, FogChallengeEvent,
|
|
816
|
+
GhostAlwaysObjectEvent, GhostingMessageEvent, GravityEvent,
|
|
817
|
+
LightningStrikeEvent, NetStringEvent, PathManagerEvent,
|
|
818
|
+
RemoteCommandEvent, RemoveClientTargetTypeEvent,
|
|
819
|
+
ResetClientTargetsEvent, SensorGroupColorEvent, SetMissionCRCEvent,
|
|
820
|
+
SetObjectActiveImageEvent, SetSensorGroupEvent, SetServerTargetEvent,
|
|
821
|
+
Sim2DAudioEvent, Sim3DAudioEvent, SimDataBlockEvent,
|
|
822
|
+
SimTargetAudioEvent, SimVoiceStreamEvent, SimpleMessageEvent,
|
|
823
|
+
TargetFreeEvent, TargetInfoEvent, TargetToEvent.
|