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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +823 -0
  3. package/dist/BitStream.d.ts +143 -0
  4. package/dist/BitStream.d.ts.map +1 -0
  5. package/dist/BitStream.js +369 -0
  6. package/dist/BitStream.js.map +1 -0
  7. package/dist/BitStream.test.d.ts +2 -0
  8. package/dist/BitStream.test.d.ts.map +1 -0
  9. package/dist/BitStream.test.js +419 -0
  10. package/dist/BitStream.test.js.map +1 -0
  11. package/dist/ClassRegistry.d.ts +100 -0
  12. package/dist/ClassRegistry.d.ts.map +1 -0
  13. package/dist/ClassRegistry.js +118 -0
  14. package/dist/ClassRegistry.js.map +1 -0
  15. package/dist/ClassRegistry.test.d.ts +2 -0
  16. package/dist/ClassRegistry.test.d.ts.map +1 -0
  17. package/dist/ClassRegistry.test.js +83 -0
  18. package/dist/ClassRegistry.test.js.map +1 -0
  19. package/dist/DataBlockParsers.d.ts +3 -0
  20. package/dist/DataBlockParsers.d.ts.map +1 -0
  21. package/dist/DataBlockParsers.js +2041 -0
  22. package/dist/DataBlockParsers.js.map +1 -0
  23. package/dist/DemoParser.d.ts +155 -0
  24. package/dist/DemoParser.d.ts.map +1 -0
  25. package/dist/DemoParser.js +943 -0
  26. package/dist/DemoParser.js.map +1 -0
  27. package/dist/DemoParser.test.d.ts +2 -0
  28. package/dist/DemoParser.test.d.ts.map +1 -0
  29. package/dist/DemoParser.test.js +472 -0
  30. package/dist/DemoParser.test.js.map +1 -0
  31. package/dist/EventParsers.d.ts +3 -0
  32. package/dist/EventParsers.d.ts.map +1 -0
  33. package/dist/EventParsers.js +603 -0
  34. package/dist/EventParsers.js.map +1 -0
  35. package/dist/GhostManager.d.ts +13 -0
  36. package/dist/GhostManager.d.ts.map +1 -0
  37. package/dist/GhostManager.js +2002 -0
  38. package/dist/GhostManager.js.map +1 -0
  39. package/dist/GhostTracker.test.d.ts +2 -0
  40. package/dist/GhostTracker.test.d.ts.map +1 -0
  41. package/dist/GhostTracker.test.js +64 -0
  42. package/dist/GhostTracker.test.js.map +1 -0
  43. package/dist/HuffmanProcessor.d.ts +12 -0
  44. package/dist/HuffmanProcessor.d.ts.map +1 -0
  45. package/dist/HuffmanProcessor.js +163 -0
  46. package/dist/HuffmanProcessor.js.map +1 -0
  47. package/dist/LiveParser.d.ts +16 -0
  48. package/dist/LiveParser.d.ts.map +1 -0
  49. package/dist/LiveParser.js +29 -0
  50. package/dist/LiveParser.js.map +1 -0
  51. package/dist/PacketParser.d.ts +104 -0
  52. package/dist/PacketParser.d.ts.map +1 -0
  53. package/dist/PacketParser.js +713 -0
  54. package/dist/PacketParser.js.map +1 -0
  55. package/dist/Timeline.d.ts +101 -0
  56. package/dist/Timeline.d.ts.map +1 -0
  57. package/dist/Timeline.js +251 -0
  58. package/dist/Timeline.js.map +1 -0
  59. package/dist/cli.d.ts +3 -0
  60. package/dist/cli.d.ts.map +1 -0
  61. package/dist/cli.js +372 -0
  62. package/dist/cli.js.map +1 -0
  63. package/dist/dataBlockDataTypes.d.ts +877 -0
  64. package/dist/dataBlockDataTypes.d.ts.map +1 -0
  65. package/dist/dataBlockDataTypes.js +2 -0
  66. package/dist/dataBlockDataTypes.js.map +1 -0
  67. package/dist/dataTypes.d.ts +31 -0
  68. package/dist/dataTypes.d.ts.map +1 -0
  69. package/dist/dataTypes.js +5 -0
  70. package/dist/dataTypes.js.map +1 -0
  71. package/dist/dump.d.ts +3 -0
  72. package/dist/dump.d.ts.map +1 -0
  73. package/dist/dump.js +36 -0
  74. package/dist/dump.js.map +1 -0
  75. package/dist/eventDataTypes.d.ts +199 -0
  76. package/dist/eventDataTypes.d.ts.map +1 -0
  77. package/dist/eventDataTypes.js +2 -0
  78. package/dist/eventDataTypes.js.map +1 -0
  79. package/dist/ghostDataTypes.d.ts +573 -0
  80. package/dist/ghostDataTypes.d.ts.map +1 -0
  81. package/dist/ghostDataTypes.js +2 -0
  82. package/dist/ghostDataTypes.js.map +1 -0
  83. package/dist/index.d.ts +17 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +9 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/types.d.ts +261 -0
  88. package/dist/types.d.ts.map +1 -0
  89. package/dist/types.js +204 -0
  90. package/dist/types.js.map +1 -0
  91. 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.