t2-demo-parser 1.0.3 → 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.
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":"DemoParser.d.ts","sourceRoot":"","sources":["../src/DemoParser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAwB,MAAM,mBAAmB,CAAC;AAuBvE,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,SAAS,EACT,gBAAgB,EAWhB,UAAU,EACX,MAAM,YAAY,CAAC;AAMpB,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,IAAI,CAAW;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IAEnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAC,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAC,CAAmB;IACzC,OAAO,CAAC,iBAAiB,CAAC,CAAa;IACvC,OAAO,CAAC,iBAAiB,CAAC,CAAW;IACrC,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,EAAE,UAAU;IA+D9B,WAAW,IAAI,aAAa;IAI5B,eAAe,IAAI,YAAY;IAI/B,eAAe,IAAI,YAAY;IAM/B,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,MAAM,IAAI,UAAU,CAGvB;IAED,IAAI,YAAY,IAAI,gBAAgB,CAGnC;IAED,IAAI,UAAU,IAAI,MAAM,CAkBvB;IAED,IAAI,WAAW,IAAI,MAAM,CAGxB;IAID;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC;IAwDjC;;;;;OAKG;IACH,SAAS,IAAI,SAAS,GAAG,SAAS;IAwDlC;;;;;OAKG;IACH,KAAK,IAAI,IAAI;IAWb;;;;;OAKG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAUpC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA6BzB;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC;IAQxC,OAAO,CAAC,UAAU;IAwBlB;4DACwD;IACxD,OAAO,CAAC,gBAAgB;IA0OxB;;;OAGG;IACH,OAAO,CAAC,cAAc;IActB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAQtB;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IA6EhC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,eAAe;IAoBvB;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,mBAAmB;IAyD3B,OAAO,CAAC,mBAAmB;IA8F3B;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IA2DtB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IAuCjC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,WAAW;IA6BnB;;;OAGG;IACH,OAAO,CAAC,aAAa;CAOtB"}
1
+ {"version":3,"file":"DemoParser.d.ts","sourceRoot":"","sources":["../src/DemoParser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAwB,MAAM,mBAAmB,CAAC;AAuBvE,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,SAAS,EACT,gBAAgB,EAWhB,UAAU,EACX,MAAM,YAAY,CAAC;AAMpB,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,IAAI,CAAW;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IAEnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAC,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAC,CAAmB;IACzC,OAAO,CAAC,iBAAiB,CAAC,CAAa;IACvC,OAAO,CAAC,iBAAiB,CAAC,CAAW;IACrC,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,EAAE,UAAU;IAyE9B,WAAW,IAAI,aAAa;IAI5B,eAAe,IAAI,YAAY;IAI/B,eAAe,IAAI,YAAY;IAM/B,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,MAAM,IAAI,UAAU,CAGvB;IAED,IAAI,YAAY,IAAI,gBAAgB,CAGnC;IAED,IAAI,UAAU,IAAI,MAAM,CAkBvB;IAED,IAAI,WAAW,IAAI,MAAM,CAGxB;IAID;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC;IA4DjC;;;;;OAKG;IACH,SAAS,IAAI,SAAS,GAAG,SAAS;IA2DlC;;;;;OAKG;IACH,KAAK,IAAI,IAAI;IAWb;;;;;OAKG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAUpC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA8BzB;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC;IAQxC,OAAO,CAAC,UAAU;IAwBlB;4DACwD;IACxD,OAAO,CAAC,gBAAgB;IA4TxB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAuBtB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAQtB;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IA6EhC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,eAAe;IAyBvB;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,mBAAmB;IAmE3B,OAAO,CAAC,mBAAmB;IAuH3B;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAsEtB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA4CjC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,WAAW;IA0CnB;;;OAGG;IACH,OAAO,CAAC,aAAa;CAOtB"}
@@ -270,6 +270,7 @@ export class DemoParser {
270
270
  dataBlockDataMap,
271
271
  connectionProtocolState: initialBlock.connectionState,
272
272
  nextRecvEventSeq: initialBlock.nextRecvEventSeq,
273
+ compressionPoint: initialBlock.initialCompressionPoint,
273
274
  });
274
275
  this.ghostTracker = gt;
275
276
  this.packetParser = pp;
@@ -406,13 +407,15 @@ export class DemoParser {
406
407
  let initialGhosts = [];
407
408
  let controlObjectGhostIndex = -1;
408
409
  let controlObjectData;
410
+ let initialCompressionPoint;
409
411
  let missionName = "";
410
412
  let missionCRC = 0;
411
413
  let phase2Error;
412
414
  try {
413
415
  debugInitial("phase2 start bit=%d remaining=%d", bs.getCurPos(), totalBits - bs.getCurPos());
414
416
  // B.10f Events
415
- ({ nextRecvEventSeq, events: initialEvents } = this.readEventStartBlock(bs));
417
+ ({ nextRecvEventSeq, events: initialEvents } =
418
+ this.readEventStartBlock(bs));
416
419
  debugInitial("after initial events bit=%d count=%d", bs.getCurPos(), initialEvents.length);
417
420
  // B.10g Ghosts
418
421
  const ghostResult = this.readGhostStartBlock(bs, dataBlocks);
@@ -428,11 +431,20 @@ export class DemoParser {
428
431
  if (ghost) {
429
432
  const parser = this.registry.getGhostParser(ghost.classId);
430
433
  if (parser?.readPacketData) {
434
+ // getGhostParser enables the nested vehicle readPacketData
435
+ // when the recorder was piloting at recording start — without
436
+ // it those bytes go unread and every later read in the
437
+ // initial block (mission name, CRC) is misaligned.
431
438
  const conn = {
432
439
  compressionPoint: { x: 0, y: 0, z: 0 },
433
440
  ghostTracker: ibGhostTracker,
441
+ getGhostParser: (classId) => this.registry.getGhostParser(classId),
434
442
  };
435
443
  controlObjectData = parser.readPacketData(bs, conn);
444
+ // The control object's readPacketData establishes the
445
+ // connection's compression point (its position) — carry it
446
+ // into the packet parser seed.
447
+ initialCompressionPoint = conn.compressionPoint;
436
448
  debugInitial("after control readPacketData bit=%d parser=%s", bs.getCurPos(), parser.name);
437
449
  }
438
450
  }
@@ -456,9 +468,7 @@ export class DemoParser {
456
468
  }
457
469
  const remaining = totalBits - bs.getCurPos();
458
470
  const missionPrintableRatio = missionName.length > 0
459
- ? missionName
460
- .split("")
461
- .filter((c) => {
471
+ ? missionName.split("").filter((c) => {
462
472
  const code = c.charCodeAt(0);
463
473
  return code >= 0x20 && code <= 0x7e;
464
474
  }).length / missionName.length
@@ -490,6 +500,7 @@ export class DemoParser {
490
500
  initialEvents,
491
501
  controlObjectGhostIndex,
492
502
  controlObjectData,
503
+ initialCompressionPoint,
493
504
  missionName,
494
505
  missionCRC,
495
506
  phase2TrailingBits: remaining,
@@ -513,7 +524,16 @@ export class DemoParser {
513
524
  const triggerFlags = [];
514
525
  for (let i = 0; i < 6; i++)
515
526
  triggerFlags.push(bs.readFlag());
516
- return { clientId, teamId, score, field0, field1, field2, isBot, triggerFlags };
527
+ return {
528
+ clientId,
529
+ teamId,
530
+ score,
531
+ field0,
532
+ field1,
533
+ field2,
534
+ isBot,
535
+ triggerFlags,
536
+ };
517
537
  }
518
538
  /**
519
539
  * Read DemoValues from FUN_005fb5c0 lines 388966-388980.
@@ -696,6 +716,7 @@ export class DemoParser {
696
716
  guaranteed: true,
697
717
  dataBitsStart,
698
718
  dataBitsEnd: dataBitsStart,
719
+ failed: true,
699
720
  });
700
721
  break;
701
722
  }
@@ -707,6 +728,7 @@ export class DemoParser {
707
728
  guaranteed: true,
708
729
  dataBitsStart,
709
730
  dataBitsEnd: dataBitsStart,
731
+ failed: true,
710
732
  });
711
733
  break;
712
734
  }
@@ -923,9 +945,22 @@ export class DemoParser {
923
945
  trigger.push(data[57 + i] !== 0);
924
946
  }
925
947
  return {
926
- px, py, pz, pyaw, ppitch, proll,
927
- x, y, z, yaw, pitch, roll,
928
- id, sendCount, freeLook, trigger,
948
+ px,
949
+ py,
950
+ pz,
951
+ pyaw,
952
+ ppitch,
953
+ proll,
954
+ x,
955
+ y,
956
+ z,
957
+ yaw,
958
+ pitch,
959
+ roll,
960
+ id,
961
+ sendCount,
962
+ freeLook,
963
+ trigger,
929
964
  };
930
965
  }
931
966
  /**