t2-demo-parser 1.0.2 → 2.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 (39) hide show
  1. package/README.md +201 -87
  2. package/dist/DataBlockParsers.d.ts.map +1 -1
  3. package/dist/DataBlockParsers.js +51 -42
  4. package/dist/DataBlockParsers.js.map +1 -1
  5. package/dist/DataBlockParsers.test.js +32 -0
  6. package/dist/DataBlockParsers.test.js.map +1 -1
  7. package/dist/DemoParser.d.ts.map +1 -1
  8. package/dist/DemoParser.js +43 -8
  9. package/dist/DemoParser.js.map +1 -1
  10. package/dist/GhostManager.d.ts.map +1 -1
  11. package/dist/GhostManager.js +36 -68
  12. package/dist/GhostManager.js.map +1 -1
  13. package/dist/GhostStateAccumulator.d.ts +36 -0
  14. package/dist/GhostStateAccumulator.d.ts.map +1 -0
  15. package/dist/GhostStateAccumulator.js +124 -0
  16. package/dist/GhostStateAccumulator.js.map +1 -0
  17. package/dist/GhostStateAccumulator.test.d.ts +2 -0
  18. package/dist/GhostStateAccumulator.test.d.ts.map +1 -0
  19. package/dist/GhostStateAccumulator.test.js +201 -0
  20. package/dist/GhostStateAccumulator.test.js.map +1 -0
  21. package/dist/LiveParser.d.ts +39 -1
  22. package/dist/LiveParser.d.ts.map +1 -1
  23. package/dist/LiveParser.js +43 -1
  24. package/dist/LiveParser.js.map +1 -1
  25. package/dist/PacketParser.d.ts +23 -2
  26. package/dist/PacketParser.d.ts.map +1 -1
  27. package/dist/PacketParser.js +62 -19
  28. package/dist/PacketParser.js.map +1 -1
  29. package/dist/dataBlockDataTypes.d.ts +22 -21
  30. package/dist/dataBlockDataTypes.d.ts.map +1 -1
  31. package/dist/dataBlockDataTypes.js.map +1 -1
  32. package/dist/index.d.ts +4 -3
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +3 -2
  35. package/dist/index.js.map +1 -1
  36. package/dist/types.d.ts +11 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js.map +1 -1
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # t2-demo-parser
2
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).
3
+ Parser for Tribes 2 demo recordings (`.rec` files) and live server packet
4
+ streams. Extracts game state, player movement, ghost object lifecycles,
5
+ network events, and animation timelines from recordings made by the Tribes 2
6
+ client (build 25034, Torque engine), and parses the same packet format
7
+ arriving from a live connection (see `createLiveParser`).
6
8
 
7
9
  Designed for use in browser-based replay viewers. The async API keeps the main
8
10
  thread responsive: in browsers, decompression runs in a Web Worker via
@@ -18,14 +20,14 @@ const buffer = new Uint8Array(/* .rec file contents */);
18
20
  const parser = new DemoParser(buffer);
19
21
  const demo = await parser.parseFullDemo();
20
22
 
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
23
+ console.log(demo.header.demoLengthMs); // Duration in ms
24
+ console.log(demo.initialBlock.missionName); // e.g. "Rollercoaster"
25
+ console.log(demo.blocks.length); // Total block count
26
+ console.log(demo.initialBlock.dataBlocks.size); // DataBlock definitions
25
27
 
26
28
  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
+ console.log(timeline.controlObject.length); // Player position keyframes
30
+ console.log(timeline.ghostInstances.length); // Networked object lifecycles
29
31
  ```
30
32
 
31
33
  ## CLI
@@ -124,21 +126,21 @@ interface DemoFile {
124
126
 
125
127
  #### Properties
126
128
 
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. |
129
+ | Property | Type | Description |
130
+ | -------------- | ------------------ | ------------------------------------------------------------------------------------------------ |
131
+ | `loaded` | `boolean` | Whether `load()` has been called. |
132
+ | `header` | `DemoHeader` | File header (throws if not loaded). |
133
+ | `initialBlock` | `InitialBlockData` | Initial game state (throws if not loaded). |
134
+ | `blockCount` | `number` | Total blocks in the stream. Lazily computed on first access by scanning the decompressed buffer. |
135
+ | `blockCursor` | `number` | Number of blocks consumed so far. |
134
136
 
135
137
  #### Accessors
136
138
 
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. |
139
+ | Method | Returns | Description |
140
+ | ------------------- | --------------- | ----------------------------------------------- |
141
+ | `getRegistry()` | `ClassRegistry` | Parser registry with all bindings. |
142
+ | `getGhostTracker()` | `GhostTracker` | Current ghost state (mutated by `nextBlock()`). |
143
+ | `getPacketParser()` | `PacketParser` | Packet parser with parse statistics. |
142
144
 
143
145
  ---
144
146
 
@@ -146,10 +148,10 @@ interface DemoFile {
146
148
 
147
149
  ```typescript
148
150
  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
151
+ identString: string; // "Tribes2 Recording"
152
+ protocolVersion: number; // 0x330004
153
+ demoLengthMs: number; // Total recording duration in milliseconds
154
+ initialBlockSize: number; // Byte size of the initial block
153
155
  }
154
156
  ```
155
157
 
@@ -162,7 +164,7 @@ Snapshot of the game state at the moment recording started.
162
164
  ```typescript
163
165
  interface InitialBlockData {
164
166
  // DataBlocks (static game definitions)
165
- dataBlocks: Map<number, ParsedDataBlock>; // objectId → parsed DataBlock
167
+ dataBlocks: Map<number, ParsedDataBlock>; // objectId → parsed DataBlock
166
168
  dataBlockCount: number;
167
169
  dataBlockHeaders: DataBlockHeader[];
168
170
 
@@ -181,8 +183,11 @@ interface InitialBlockData {
181
183
  initialEvents: NetEventInfo[];
182
184
 
183
185
  // Control object (the recording player)
184
- controlObjectGhostIndex: number; // -1 if none
186
+ controlObjectGhostIndex: number; // -1 if none
185
187
  controlObjectData?: Record<string, unknown>;
188
+ // Compression point established by the control object's state (its
189
+ // position), seeding compressed-point decodes in the first packets.
190
+ initialCompressionPoint?: { x: number; y: number; z: number };
186
191
  firstPerson: boolean;
187
192
 
188
193
  // Mission info
@@ -215,10 +220,10 @@ A single block from the compressed block stream.
215
220
  ```typescript
216
221
  interface DemoBlock {
217
222
  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
223
+ type: number; // BlockTypePacket (0), BlockTypeSendPacket (1),
224
+ // BlockTypeMove (2), or BlockTypeInfo (3)
225
+ size: number; // Payload size in bytes
226
+ data: Uint8Array; // Raw payload
222
227
  parsed?: PacketData | Move | InfoBlock;
223
228
  }
224
229
  ```
@@ -251,6 +256,8 @@ interface GameState {
251
256
  pinged: boolean;
252
257
  jammed: boolean;
253
258
  controlObjectGhostIndex?: number;
259
+ controlObjectDataStart?: number; // Bit offsets of the control object
260
+ controlObjectDataEnd?: number; // update within the packet
254
261
  controlObjectData?: Record<string, unknown>;
255
262
  compressionPoint?: { x: number; y: number; z: number };
256
263
  cameraFov?: number;
@@ -273,12 +280,12 @@ A create, update, or delete operation on a ghost object.
273
280
 
274
281
  ```typescript
275
282
  interface GhostUpdate {
276
- index: number; // Ghost slot (0–1023)
283
+ index: number; // Ghost slot (0–1023)
277
284
  type: "create" | "update" | "delete";
278
- classId?: number; // Set on create
285
+ classId?: number; // Set on create
279
286
  updateBitsStart: number;
280
287
  updateBitsEnd: number;
281
- parsedData?: Record<string, unknown>; // Class-specific parsed fields
288
+ parsedData?: Record<string, unknown>; // Class-specific parsed fields
282
289
  }
283
290
  ```
284
291
 
@@ -290,10 +297,13 @@ A network event received from the server.
290
297
  interface NetEventInfo {
291
298
  classId: number;
292
299
  guaranteed: boolean;
293
- sequenceNumber?: number;
300
+ sequenceNumber?: number; // 7-bit wire sequence (guaranteed events)
301
+ absoluteSequenceNumber?: number; // Unwrapped full sequence
294
302
  dataBitsStart: number;
295
303
  dataBitsEnd: number;
296
304
  parsedData?: Record<string, unknown>;
305
+ failed?: boolean; // Event could not be parsed; stream
306
+ // position after it is unreliable
297
307
  }
298
308
  ```
299
309
 
@@ -305,14 +315,24 @@ Raw 64-byte player input struct (from type 2 blocks).
305
315
 
306
316
  ```typescript
307
317
  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;
318
+ x: number;
319
+ y: number;
320
+ z: number; // Movement input per axis (float)
321
+ yaw: number;
322
+ pitch: number;
323
+ roll: number; // Rotation deltas in radians
324
+ // (server adds them each tick)
325
+ px: number;
326
+ py: number;
327
+ pz: number; // Packed integer forms of x/y/z
328
+ pyaw: number;
329
+ ppitch: number;
330
+ proll: number; // Packed rotation: fractional
331
+ // turns × 65536 (16-bit range)
312
332
  id: number;
313
333
  sendCount: number;
314
334
  freeLook: boolean;
315
- trigger: boolean[]; // 6 trigger keys (fire, jet, jump, etc.)
335
+ trigger: boolean[]; // 6 trigger keys (fire, jet, jump, etc.)
316
336
  }
317
337
  ```
318
338
 
@@ -325,9 +345,9 @@ A static game object definition parsed from the initial block.
325
345
  ```typescript
326
346
  interface ParsedDataBlock {
327
347
  classId: number;
328
- className: string; // e.g. "PlayerData", "WheeledVehicleData"
348
+ className: string; // e.g. "PlayerData", "WheeledVehicleData"
329
349
  objectId: number;
330
- data: Record<string, unknown>; // Class-specific fields (shapeName, etc.)
350
+ data: Record<string, unknown>; // Class-specific fields (shapeName, etc.)
331
351
  }
332
352
  ```
333
353
 
@@ -375,7 +395,11 @@ with position data are included.
375
395
  interface ExportTimeline {
376
396
  durationMs: number;
377
397
  tickIntervalMs: number;
378
- controlObject: { t: number; p?: [number, number, number]; v?: [number, number, number] }[];
398
+ controlObject: {
399
+ t: number;
400
+ p?: [number, number, number];
401
+ v?: [number, number, number];
402
+ }[];
379
403
  ghosts: ExportGhostInstance[];
380
404
  events: GameEvent[];
381
405
  }
@@ -439,22 +463,40 @@ interface ControlObjectKeyframe {
439
463
 
440
464
  Available via `parser.getPacketParser()`. Exposes parse statistics.
441
465
 
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. |
466
+ | Property | Type | Description |
467
+ | ----------------------- | -------- | --------------------------------------------------------------- |
468
+ | `packetsParsed` | `number` | Total packets successfully parsed. |
469
+ | `ghostCreatesParsed` | `number` | Ghost create operations parsed. |
470
+ | `ghostUpdatesParsed` | `number` | Ghost update operations parsed. |
471
+ | `ghostDeletes` | `number` | Ghost delete operations. |
472
+ | `ghostsFailed` | `number` | Ghost operations that failed to parse. |
473
+ | `ghostsTrackerDiverged` | `number` | Ghost tracker inconsistencies detected. |
474
+ | `eventsParsed` | `number` | Events parsed. |
475
+ | `eventsFailed` | `number` | Events that failed to parse. |
476
+ | `controlObjectParsed` | `number` | Control object updates parsed. |
477
+ | `controlObjectFailed` | `number` | Control object updates that failed. |
478
+ | `protocolRejected` | `number` | Packets rejected by the dnet protocol window. |
479
+ | `protocolNoDispatch` | `number` | Packets accepted but not dispatched (duplicates/out-of-window). |
480
+
481
+ #### State export (for seeding another parser)
482
+
483
+ These getters capture **all cross-packet parser state**, so a second parser
484
+ seeded with the same values (plus the ghost tracker contents and datablock
485
+ map) continues the stream in bit-lockstep with this one — the basis for
486
+ late-joiner catch-up in live streaming.
487
+
488
+ | Method | Returns |
489
+ | ----------------------------------- | ------------------------------------------------ |
490
+ | `getConnectionProtocolState()` | `ConnectionProtocolState` (dnet sequence window) |
491
+ | `getNextRecvEventSeq()` | `number` |
492
+ | `getPendingGuaranteedEvents()` | Out-of-order guaranteed events awaiting dispatch |
493
+ | `getCompressionPoint()` | `{ x, y, z }` |
494
+ | `getDataBlockDataMap()` | `Map<number, ParsedData> \| undefined` |
495
+ | `setConnectionProtocolState(state)` | — (also a constructor option) |
454
496
 
455
497
  ---
456
498
 
457
- ### `createLiveParser(): LiveParserKit`
499
+ ### `createLiveParser(seed?): LiveParserKit`
458
500
 
459
501
  Create a parser stack for live server connections. Sets up the same deterministic
460
502
  registry bindings as `DemoParser` but without requiring a `.rec` file. Useful for
@@ -475,6 +517,69 @@ interface LiveParserKit {
475
517
  }
476
518
  ```
477
519
 
520
+ With a **seed**, the stack resumes an in-progress stream from another
521
+ parser's exported state, continuing at a packet boundary in bit-lockstep
522
+ with the exporter (the late-joiner catch-up scenario — see
523
+ `GhostStateAccumulator` below for producing the ghost seeds):
524
+
525
+ ```typescript
526
+ interface LiveParserSeed {
527
+ dataBlocks?: Iterable<[number, ParsedData]>; // objectId → parsed datablock
528
+ ghosts?: Iterable<{ index: number; classId: number }>;
529
+ connectionProtocolState?: ConnectionProtocolState;
530
+ nextRecvEventSeq?: number;
531
+ compressionPoint?: { x: number; y: number; z: number };
532
+ pendingGuaranteedEvents?: Array<{
533
+ absoluteSequenceNumber: number;
534
+ event: NetEventInfo;
535
+ }>;
536
+ }
537
+ ```
538
+
539
+ ### `passiveObserverProtocolState(firstPacketByte): ConnectionProtocolState`
540
+
541
+ Protocol state for a parser that passively observes the server→client
542
+ stream while something else (e.g. a relay) owns the client→server side.
543
+ Sets `lastSendSeq` high so ack validation never rejects packets that ack
544
+ sequences the observer didn't send. Intended for the first packets of a
545
+ connection; to attach mid-stream, seed `connectionProtocolState` from the
546
+ exporting parser instead.
547
+
548
+ ```typescript
549
+ import { createLiveParser, passiveObserverProtocolState } from "t2-demo-parser";
550
+
551
+ const { packetParser } = createLiveParser();
552
+ // On the first received packet:
553
+ packetParser.setConnectionProtocolState(passiveObserverProtocolState(data[0]));
554
+ ```
555
+
556
+ ---
557
+
558
+ ### `GhostStateAccumulator`
559
+
560
+ Maintains one merged full `parsedData` per live ghost by folding each
561
+ packet's creates/updates/deletes (plus `GhostAlwaysObjectEvent` creates and
562
+ EndGhosting clears). `toInitialGhosts()` yields entries shaped like a demo
563
+ recording's `initialGhosts` — the full-state ghost list a `.rec` starts
564
+ with when recorded mid-match — for hydrating a late joiner.
565
+
566
+ ```typescript
567
+ import { GhostStateAccumulator, mergeGhostParsedData } from "t2-demo-parser";
568
+
569
+ const accumulator = new GhostStateAccumulator();
570
+ // After each parsed packet:
571
+ accumulator.applyPacket(packetData);
572
+
573
+ accumulator.toInitialGhosts(); // GhostUpdate[] with full merged parsedData
574
+ accumulator.getGhostSeeds(); // { index, classId }[] for createLiveParser
575
+ accumulator.size();
576
+ accumulator.clear();
577
+ ```
578
+
579
+ `mergeGhostParsedData(base, update)` is the underlying merge rule: arrays
580
+ whose entries carry a numeric `index` (threads, images, sounds) merge
581
+ sparsely by index; other values are last-write-wins.
582
+
478
583
  ---
479
584
 
480
585
  ### `BitStream`
@@ -500,9 +605,9 @@ Tracks the live state of all ghost objects. Available via
500
605
 
501
606
  ```typescript
502
607
  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
608
+ const ghost = tracker.getGhost(index); // GhostEntry | undefined
609
+ const all = tracker.getAllGhosts(); // Map<number, GhostEntry>
610
+ tracker.size(); // Number of active ghosts
506
611
  ```
507
612
 
508
613
  ```typescript
@@ -521,10 +626,10 @@ Block type constants for filtering `DemoBlock.type`:
521
626
 
522
627
  ```typescript
523
628
  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
629
+ BlockTypePacket, // 0 — network packet
630
+ BlockTypeSendPacket, // 1 — send-packet trigger (no data)
631
+ BlockTypeMove, // 2 — 64-byte player input
632
+ BlockTypeInfo, // 3 — 8-byte timing/FOV
528
633
  } from "t2-demo-parser";
529
634
  ```
530
635
 
@@ -532,20 +637,22 @@ Network protocol constants are also exported:
532
637
 
533
638
  ```typescript
534
639
  import {
535
- MaxGhostCount, // 1024
536
- GhostIdBitSize, // 10
640
+ MaxGhostCount, // 1024
641
+ GhostIdBitSize, // 10
537
642
  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
643
+ StringIdBitSize, // 12
644
+ NetEventClassBitSize, // 6
645
+ NetEventClassFirst, // 255
646
+ NetObjectClassBitSize, // 7
647
+ NetObjectClassFirst, // 0
648
+ MaxPacketDataSize, // 1500
649
+ MaxTriggerKeys, // 6
650
+ MoveCountBits, // 5
651
+ MaxMoveCount, // 30
652
+ DataBlockObjectIdFirst, // 3
546
653
  DataBlockObjectIdBitSize, // 10
547
- DataBlockClassFirst, // 128
548
- DataBlockClassBitSize, // 7
654
+ DataBlockClassFirst, // 128
655
+ DataBlockClassBitSize, // 7
549
656
  } from "t2-demo-parser";
550
657
  ```
551
658
 
@@ -554,9 +661,9 @@ assignment order):
554
661
 
555
662
  ```typescript
556
663
  import {
557
- NetObjectClassNames, // 53 ghost class names
558
- DataBlockClassNames, // 54 DataBlock class names
559
- NetEventClassNames, // 26 event class names
664
+ NetObjectClassNames, // 53 ghost class names
665
+ DataBlockClassNames, // 54 DataBlock class names
666
+ NetEventClassNames, // 26 event class names
560
667
  } from "t2-demo-parser";
561
668
  ```
562
669
 
@@ -576,11 +683,14 @@ const { header, initialBlock } = await parser.load();
576
683
 
577
684
  // Duration
578
685
  const durationMs = header.demoLengthMs;
579
- const durationStr = `${Math.floor(durationMs / 60000)}m${
580
- Math.floor((durationMs % 60000) / 1000).toString().padStart(2, "0")}s`;
686
+ const durationStr = `${Math.floor(durationMs / 60000)}m${Math.floor(
687
+ (durationMs % 60000) / 1000,
688
+ )
689
+ .toString()
690
+ .padStart(2, "0")}s`;
581
691
 
582
692
  // Mission / map name
583
- const mission = initialBlock.missionName; // e.g. "Rollercoaster"
693
+ const mission = initialBlock.missionName; // e.g. "Rollercoaster"
584
694
 
585
695
  // Game mode / mission type — look in DemoValues
586
696
  // $DemoValue_0 is typically the game mode (e.g. "CTFGame")
@@ -588,7 +698,9 @@ const gameMode = initialBlock.demoValues[0];
588
698
 
589
699
  // Teams and players
590
700
  for (const score of initialBlock.scoreEntries) {
591
- console.log(`Team ${score.teamId}: client ${score.clientId}, score ${score.score}`);
701
+ console.log(
702
+ `Team ${score.teamId}: client ${score.clientId}, score ${score.score}`,
703
+ );
592
704
  }
593
705
 
594
706
  // Player names from the target table
@@ -650,10 +762,10 @@ function seekTo(targetMs: number) {
650
762
  if (block.type === BlockTypeMove) moveCount++;
651
763
  if (moveCount * 32 >= targetMs) break;
652
764
  }
653
- return moveCount * 32; // Actual time reached
765
+ return moveCount * 32; // Actual time reached
654
766
  }
655
767
 
656
- const actualMs = seekTo(60000); // Seek to ~1 minute
768
+ const actualMs = seekTo(60000); // Seek to ~1 minute
657
769
  // Ghost tracker now reflects state at that point.
658
770
  // Continue calling nextBlock() to play forward from here.
659
771
  ```
@@ -680,7 +792,9 @@ console.log(`${stats.ghostsWithPosition} with position data`);
680
792
 
681
793
  // Iterate ghost lifecycles
682
794
  for (const inst of timeline.ghostInstances) {
683
- console.log(`${inst.className} #${inst.ghostIndex}: ${inst.keyframes.length} keyframes`);
795
+ console.log(
796
+ `${inst.className} #${inst.ghostIndex}: ${inst.keyframes.length} keyframes`,
797
+ );
684
798
  for (const kf of inst.keyframes) {
685
799
  if (kf.position) {
686
800
  // Feed into Three.js KeyframeTrack...
@@ -713,7 +827,7 @@ parser.reset();
713
827
  while (parser.nextBlock()) {}
714
828
  const stats2 = parser.getPacketParser().packetsParsed;
715
829
 
716
- console.log(stats1 === stats2); // true
830
+ console.log(stats1 === stats2); // true
717
831
  ```
718
832
 
719
833
  ### Access parse statistics
@@ -747,7 +861,7 @@ const registry = parser.getRegistry();
747
861
  // From a ghost update in a packet:
748
862
  if (ghost.type === "create" && ghost.classId !== undefined) {
749
863
  const entry = registry.getGhostParser(ghost.classId);
750
- console.log(entry?.name); // e.g. "Player", "Turret", "LinearProjectile"
864
+ console.log(entry?.name); // e.g. "Player", "Turret", "LinearProjectile"
751
865
  }
752
866
 
753
867
  // From the ghost tracker (live state):
@@ -1 +1 @@
1
- {"version":3,"file":"DataBlockParsers.d.ts","sourceRoot":"","sources":["../src/DataBlockParsers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AA0uExD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CA2NtE"}
1
+ {"version":3,"file":"DataBlockParsers.d.ts","sourceRoot":"","sources":["../src/DataBlockParsers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAkvExD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CA2NtE"}
@@ -440,23 +440,25 @@ function playerDataUnpack(bs) {
440
440
  splashEmitters.push(readDataBlockRef(bs));
441
441
  }
442
442
  result.splashEmitters = splashEmitters;
443
- // 17. 11× F32 for ground impact shake (offsets 0x3fc-0x424)
444
- // V12 has 9 fields; binary has 11 (2 extra Tribes 2-specific)
445
- result.groundImpactMinSpeed = bs.readF32(); // 0x3fc
443
+ // 17. 11× F32 (offsets 0x3fc-0x424). Names binary-verified against
444
+ // initPersistFields: the two leading fields are the Tribes 2 heat
445
+ // signature rates (retail player.cs: 1/4 and 1/3), then the ground
446
+ // impact shake block. Demo values confirm: 0.25, 0.333…
447
+ result.heatDecayPerSec = bs.readF32(); // 0x3fc
448
+ result.heatIncreasePerSec = bs.readF32(); // 0x400
449
+ result.groundImpactMinSpeed = bs.readF32(); // 0x404
446
450
  result.groundImpactShakeFreq = {
447
- x: bs.readF32(), // 0x400
448
- y: bs.readF32(), // 0x404
449
- z: bs.readF32(), // 0x408
451
+ x: bs.readF32(), // 0x408
452
+ y: bs.readF32(), // 0x40c
453
+ z: bs.readF32(), // 0x410
450
454
  };
451
455
  result.groundImpactShakeAmp = {
452
- x: bs.readF32(), // 0x40c
453
- y: bs.readF32(), // 0x410
454
- z: bs.readF32(), // 0x414
456
+ x: bs.readF32(), // 0x414
457
+ y: bs.readF32(), // 0x418
458
+ z: bs.readF32(), // 0x41c
455
459
  };
456
- result.groundImpactShakeDuration = bs.readF32(); // 0x418
457
- result.groundImpactShakeFalloff = bs.readF32(); // 0x41c
458
- result.boundingRadius = bs.readF32(); // 0x420 (Tribes 2 extra)
459
- result.moveBubbleSize = bs.readF32(); // 0x424 (Tribes 2 extra)
460
+ result.groundImpactShakeDuration = bs.readF32(); // 0x420
461
+ result.groundImpactShakeFalloff = bs.readF32(); // 0x424
460
462
  return result;
461
463
  }
462
464
  // ============================================================
@@ -479,6 +481,8 @@ function vehicleDataUnpack(bs) {
479
481
  result.softImpactSpeed = bs.readF32(); // 0x380
480
482
  result.hardImpactSpeed = bs.readF32(); // 0x384 (900 dec)
481
483
  result.minRollSpeed = bs.readF32(); // 0x388
484
+ // Registered in the retail binary as "maxSteerinAngle" (engine typo);
485
+ // we keep the corrected spelling.
482
486
  result.maxSteeringAngle = bs.readF32(); // 0x38c
483
487
  result.maxDrag = bs.readF32(); // 0x3a4
484
488
  result.minDrag = bs.readF32(); // 0x3a0
@@ -1222,9 +1226,10 @@ function debrisDataUnpack(bs) {
1222
1226
  result.gravModifier = bs.readF32(); // 0x78
1223
1227
  result.terminalVelocity = bs.readF32(); // 0x7c
1224
1228
  result.ignoreWater = readBool(bs); // 0x80 (8-bit bool)
1225
- // 2 readString
1226
- result.shapeFileName = bs.readString(); // 0x8c
1227
- result.skinName = bs.readString(); // 0x84
1229
+ // 2 readString — names binary-verified: 0x8c is registered as
1230
+ // `texture`, 0x84 as `shapeName` (read in that wire order)
1231
+ result.texture = bs.readString(); // 0x8c
1232
+ result.shapeName = bs.readString(); // 0x84
1228
1233
  // 2 emitter refs (loop at 0xa4, 0xa8) + 1 explosion ref (0x94)
1229
1234
  result.emitter0 = readDataBlockRef(bs);
1230
1235
  result.emitter1 = readDataBlockRef(bs);
@@ -1502,15 +1507,17 @@ function audioEnvironmentUnpack(bs) {
1502
1507
  result.reflectionsDelay = readRangedF32(bs, 0, 0.3, 9); // 0x5c
1503
1508
  result.reverbDelay = readRangedF32(bs, 0, 0.1, 7); // 0x60
1504
1509
  result.roomVolume = readRangedS32(bs, -10000, 0); // 0x64
1505
- result.effectVolume = readRangedF32(bs, 0, 1, 9); // 0x6c (9 bits, confirmed in binary)
1506
- result.damping = readRangedF32(bs, 0, 2, 10); // 0x70 (10 bits, confirmed in binary)
1507
- result.environmentSize = readRangedF32(bs, 1, 100, 8); // 0x74 (8 bits, confirmed in binary)
1508
- result.environmentDiffusion = readRangedF32(bs, 0, 1, 10); // 0x78 (10 bits, confirmed in binary)
1509
- // NOTE: No airAbsorption field in binary (V12 has it, Tribes 2 binary does not)
1510
+ // Names binary-verified: initPersistFields maps 0x6c damping,
1511
+ // 0x70 environmentSize, 0x74 environmentDiffusion, 0x78 airAbsorption.
1512
+ result.damping = readRangedF32(bs, 0, 1, 9); // 0x6c (9 bits, confirmed in binary)
1513
+ result.environmentSize = readRangedF32(bs, 0, 2, 10); // 0x70 (10 bits, confirmed in binary)
1514
+ result.environmentDiffusion = readRangedF32(bs, 1, 100, 8); // 0x74 (8 bits, confirmed in binary)
1515
+ result.airAbsorption = readRangedF32(bs, 0, 1, 10); // 0x78 (10 bits, confirmed in binary)
1510
1516
  result.flags = bs.readInt(6); // 0x7c
1511
1517
  }
1512
- // Trailing field: always present after both branches (binary line 23471, offset 0x68)
1513
- result.effectVolumeHF = readRangedF32(bs, 0, 1, 8);
1518
+ // Trailing field: always present after both branches (offset 0x68 =
1519
+ // effectVolume per initPersistFields)
1520
+ result.effectVolume = readRangedF32(bs, 0, 1, 8);
1514
1521
  return result;
1515
1522
  }
1516
1523
  // ============================================================
@@ -1697,20 +1704,22 @@ function stationFXVehicleDataUnpack(bs) {
1697
1704
  y: bs.readF32(), // 0x90
1698
1705
  z: bs.readF32(), // 0x94
1699
1706
  };
1700
- // 11 strings
1701
- result.glowTexture = bs.readString(); // 0x98
1702
- // 4×(2 readString) pad textures
1703
- result.padTexture00 = bs.readString();
1704
- result.padTexture01 = bs.readString();
1705
- result.padTexture10 = bs.readString();
1706
- result.padTexture11 = bs.readString();
1707
- result.padTexture20 = bs.readString();
1708
- result.padTexture21 = bs.readString();
1709
- result.padTexture30 = bs.readString();
1710
- result.padTexture31 = bs.readString();
1707
+ // 11 strings — names binary-verified (initPersistFields: 0x98
1708
+ // glowNodeName, 0x9c leftNodeName[4], 0xac rightNodeName[4], 0xbc
1709
+ // texture[2]) and demo-verified ("GLOWFX", LFX1/RFX1…, stationGlow).
1710
+ result.glowNodeName = bs.readString(); // 0x98
1711
+ // 4×(left, right) node name pairs, interleaved on the wire
1712
+ result.leftNodeName0 = bs.readString();
1713
+ result.rightNodeName0 = bs.readString();
1714
+ result.leftNodeName1 = bs.readString();
1715
+ result.rightNodeName1 = bs.readString();
1716
+ result.leftNodeName2 = bs.readString();
1717
+ result.rightNodeName2 = bs.readString();
1718
+ result.leftNodeName3 = bs.readString();
1719
+ result.rightNodeName3 = bs.readString();
1711
1720
  // 2 readString
1712
- result.lightStartColor = bs.readString();
1713
- result.lightEndColor = bs.readString();
1721
+ result.texture0 = bs.readString();
1722
+ result.texture1 = bs.readString();
1714
1723
  return result;
1715
1724
  }
1716
1725
  // ============================================================
@@ -1729,12 +1738,12 @@ function stationFXPersonalDataUnpack(bs) {
1729
1738
  result.bottomAlpha = bs.readF32();
1730
1739
  result.glowSpeed = bs.readF32();
1731
1740
  result.scrollSpeed = bs.readF32();
1732
- // 2 readString
1733
- result.glowTexture = bs.readString();
1734
- result.padTexture = bs.readString();
1735
- // 2 readString
1736
- result.lightStartColor = bs.readString();
1737
- result.lightEndColor = bs.readString();
1741
+ // 4 strings — demo-verified ("FX1", "FX2", "special/stationLight"):
1742
+ // the two attach node names, then texture[2]
1743
+ result.leftNodeName = bs.readString();
1744
+ result.rightNodeName = bs.readString();
1745
+ result.texture0 = bs.readString();
1746
+ result.texture1 = bs.readString();
1738
1747
  return result;
1739
1748
  }
1740
1749
  // ============================================================