t2-demo-parser 2.1.0 → 3.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/README.md +170 -7
  2. package/dist/BitStream.d.ts +14 -3
  3. package/dist/BitStream.d.ts.map +1 -1
  4. package/dist/BitStream.js +28 -6
  5. package/dist/BitStream.js.map +1 -1
  6. package/dist/BitWriter.d.ts +29 -0
  7. package/dist/BitWriter.d.ts.map +1 -0
  8. package/dist/BitWriter.js +76 -0
  9. package/dist/BitWriter.js.map +1 -0
  10. package/dist/ClassRegistry.d.ts +23 -28
  11. package/dist/ClassRegistry.d.ts.map +1 -1
  12. package/dist/ClassRegistry.js +75 -84
  13. package/dist/ClassRegistry.js.map +1 -1
  14. package/dist/DataBlockParsers.d.ts.map +1 -1
  15. package/dist/DataBlockParsers.js +51 -76
  16. package/dist/DataBlockParsers.js.map +1 -1
  17. package/dist/DemoParser.d.ts +86 -23
  18. package/dist/DemoParser.d.ts.map +1 -1
  19. package/dist/DemoParser.js +409 -264
  20. package/dist/DemoParser.js.map +1 -1
  21. package/dist/EventParsers.d.ts.map +1 -1
  22. package/dist/EventParsers.js +87 -58
  23. package/dist/EventParsers.js.map +1 -1
  24. package/dist/GhostManager.d.ts +7 -1
  25. package/dist/GhostManager.d.ts.map +1 -1
  26. package/dist/GhostManager.js +231 -146
  27. package/dist/GhostManager.js.map +1 -1
  28. package/dist/HuffmanProcessor.d.ts +15 -0
  29. package/dist/HuffmanProcessor.d.ts.map +1 -1
  30. package/dist/HuffmanProcessor.js +45 -0
  31. package/dist/HuffmanProcessor.js.map +1 -1
  32. package/dist/LiveParser.d.ts +20 -1
  33. package/dist/LiveParser.d.ts.map +1 -1
  34. package/dist/LiveParser.js +31 -12
  35. package/dist/LiveParser.js.map +1 -1
  36. package/dist/PacketParser.d.ts +22 -2
  37. package/dist/PacketParser.d.ts.map +1 -1
  38. package/dist/PacketParser.js +111 -48
  39. package/dist/PacketParser.js.map +1 -1
  40. package/dist/Timeline.d.ts +5 -2
  41. package/dist/Timeline.d.ts.map +1 -1
  42. package/dist/Timeline.js +25 -12
  43. package/dist/Timeline.js.map +1 -1
  44. package/dist/cli.js +3 -0
  45. package/dist/cli.js.map +1 -1
  46. package/dist/dataBlockDataTypes.d.ts +111 -112
  47. package/dist/dataBlockDataTypes.d.ts.map +1 -1
  48. package/dist/dataBlockDataTypes.js.map +1 -1
  49. package/dist/defaultRegistry.d.ts +9 -0
  50. package/dist/defaultRegistry.d.ts.map +1 -0
  51. package/dist/defaultRegistry.js +43 -0
  52. package/dist/defaultRegistry.js.map +1 -0
  53. package/dist/eventDataTypes.d.ts +74 -72
  54. package/dist/eventDataTypes.d.ts.map +1 -1
  55. package/dist/eventDataTypes.js.map +1 -1
  56. package/dist/ghostDataTypes.d.ts +125 -131
  57. package/dist/ghostDataTypes.d.ts.map +1 -1
  58. package/dist/ghostDataTypes.js.map +1 -1
  59. package/dist/index.d.ts +6 -4
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +4 -2
  62. package/dist/index.js.map +1 -1
  63. package/dist/types.d.ts +51 -2
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/types.js +6 -0
  66. package/dist/types.js.map +1 -1
  67. package/package.json +4 -1
  68. package/dist/BitStream.test.d.ts +0 -2
  69. package/dist/BitStream.test.d.ts.map +0 -1
  70. package/dist/BitStream.test.js +0 -419
  71. package/dist/BitStream.test.js.map +0 -1
  72. package/dist/ClassRegistry.test.d.ts +0 -2
  73. package/dist/ClassRegistry.test.d.ts.map +0 -1
  74. package/dist/ClassRegistry.test.js +0 -83
  75. package/dist/ClassRegistry.test.js.map +0 -1
  76. package/dist/DataBlockParsers.test.d.ts +0 -2
  77. package/dist/DataBlockParsers.test.d.ts.map +0 -1
  78. package/dist/DataBlockParsers.test.js +0 -294
  79. package/dist/DataBlockParsers.test.js.map +0 -1
  80. package/dist/DemoParser.test.d.ts +0 -2
  81. package/dist/DemoParser.test.d.ts.map +0 -1
  82. package/dist/DemoParser.test.js +0 -472
  83. package/dist/DemoParser.test.js.map +0 -1
  84. package/dist/GhostStateAccumulator.test.d.ts +0 -2
  85. package/dist/GhostStateAccumulator.test.d.ts.map +0 -1
  86. package/dist/GhostStateAccumulator.test.js +0 -201
  87. package/dist/GhostStateAccumulator.test.js.map +0 -1
  88. package/dist/GhostTracker.test.d.ts +0 -2
  89. package/dist/GhostTracker.test.d.ts.map +0 -1
  90. package/dist/GhostTracker.test.js +0 -64
  91. package/dist/GhostTracker.test.js.map +0 -1
@@ -1,4 +1,6 @@
1
+ import createDebug from "debug";
1
2
  import { MaxTriggerKeys } from "./types.js";
3
+ const debugTracker = createDebug("t2-demo-parser:ghost-tracker");
2
4
  // ============================================================
3
5
  // Ghost Tracker — tracks ghost lifecycle (create/update/delete)
4
6
  // ============================================================
@@ -10,8 +12,18 @@ export class GhostTracker {
10
12
  hasGhost(index) {
11
13
  return this.ghosts.has(index);
12
14
  }
15
+ /**
16
+ * Record a ghost at `index`. Re-creating an occupied index is normal
17
+ * (the server recycles slots after a delete we may not have seen), but
18
+ * a class change is logged since it is the earliest sign of a tracker
19
+ * that has drifted from the server.
20
+ */
13
21
  createGhost(index, classId, className) {
14
- const entry = { classId, className, state: {} };
22
+ const existing = this.ghosts.get(index);
23
+ if (existing && existing.classId !== classId) {
24
+ debugTracker("ghost %d re-created as %s (classId %d), was %s (classId %d)", index, className, classId, existing.className, existing.classId);
25
+ }
26
+ const entry = { classId, className };
15
27
  this.ghosts.set(index, entry);
16
28
  return entry;
17
29
  }
@@ -61,6 +73,21 @@ function readPackedColorF(bs) {
61
73
  * Tribes 2 object/datablock references are serialized via FUN_00436ce0/FUN_00436d10
62
74
  * as raw 11-bit object ids (nextPow2(0x800) -> bitCount 11).
63
75
  */
76
+ /**
77
+ * Ghost index reference written with GhostIdBitSize (10) bits, e.g.
78
+ * Player mount object, Item collision object, projectile source object
79
+ * in classes that call writeInt(idx, 10) directly.
80
+ */
81
+ function readGhostRef10(bs) {
82
+ return bs.readInt(10);
83
+ }
84
+ /**
85
+ * Source object + image slot pair written via writeRangedU32(0, 1024)
86
+ * (11 bits, resolveGhost-style) and writeRangedU32(0, 7) (3 bits).
87
+ */
88
+ function readSourceObjectSlot(bs) {
89
+ return { sourceObject: readObjectRef11(bs), sourceSlot: bs.readInt(3) };
90
+ }
64
91
  function readObjectRef11(bs) {
65
92
  return bs.readInt(11);
66
93
  }
@@ -237,7 +264,7 @@ function readShapeBaseUpdate(bs, isInitial, conn) {
237
264
  if (bs.readFlag()) {
238
265
  if (bs.readFlag()) {
239
266
  // Mounting
240
- result.mountObject = bs.readInt(10);
267
+ result.mountObject = readGhostRef10(bs);
241
268
  result.mountNode = bs.readInt(5); // NumMountPointBits=5
242
269
  }
243
270
  else {
@@ -326,19 +353,11 @@ function playerReadPacketData(bs, conn) {
326
353
  }
327
354
  if (bs.readFlag()) {
328
355
  // Not mounted — read full position/velocity
329
- const pos = {
330
- x: bs.readF32(),
331
- y: bs.readF32(),
332
- z: bs.readF32(),
333
- };
356
+ const pos = bs.readPoint3F();
334
357
  result.position = pos;
335
358
  // Update compression point (this IS the compression point)
336
359
  conn.compressionPoint = pos;
337
- result.velocity = {
338
- x: bs.readF32(),
339
- y: bs.readF32(),
340
- z: bs.readF32(),
341
- };
360
+ result.velocity = bs.readPoint3F();
342
361
  result.jumpSurfaceLastContact = bs.readInt(4);
343
362
  }
344
363
  result.headX = bs.readF32();
@@ -347,20 +366,21 @@ function playerReadPacketData(bs, conn) {
347
366
  if (bs.readFlag()) {
348
367
  // Has control object (e.g., vehicle being piloted)
349
368
  // Binary FUN_005dab20: readInt(10) + resolveGhost + obj->readPacketData(conn, stream)
350
- const controlGhostIndex = bs.readInt(10);
369
+ const controlGhostIndex = readGhostRef10(bs);
351
370
  result.controlObjectGhost = controlGhostIndex;
352
- // Recursively call the control object's readPacketData.
353
- // Look up the ghost class and find its readPacketData parser.
371
+ // Recursively call the control object's readPacketData. The engine
372
+ // resolves the ghost and calls its virtual readPacketData with no
373
+ // fallback; if we cannot do the same the rest of the packet is
374
+ // unreadable, so fail loudly rather than continue misaligned.
354
375
  const controlGhost = conn.ghostTracker.getGhost(controlGhostIndex);
355
- const controlParser = controlGhost
356
- ? conn.getGhostParser?.(controlGhost.classId)
357
- : undefined;
358
- if (controlParser?.readPacketData) {
359
- const prevGhostIndex = conn.currentGhostIndex;
360
- conn.currentGhostIndex = controlGhostIndex;
361
- result.controlObjectData = controlParser.readPacketData(bs, conn);
362
- conn.currentGhostIndex = prevGhostIndex;
376
+ if (!controlGhost) {
377
+ throw new Error(`Player readPacketData: piloted ghost ${controlGhostIndex} is not tracked`);
363
378
  }
379
+ const controlParser = conn.getGhostParser?.(controlGhost.classId);
380
+ if (!controlParser?.readPacketData) {
381
+ throw new Error(`Player readPacketData: piloted ghost ${controlGhostIndex} (${controlGhost.className}) has no readPacketData parser`);
382
+ }
383
+ result.controlObjectData = controlParser.readPacketData(bs, conn);
364
384
  }
365
385
  result.disableMove = bs.readFlag();
366
386
  result.pilot = bs.readFlag();
@@ -375,7 +395,6 @@ function vehicleUnpackUpdate(bs, isInitial, conn) {
375
395
  // Control object shortcut — server writes true and returns early,
376
396
  // meaning no further Vehicle data follows in the stream.
377
397
  if (bs.readFlag()) {
378
- result._controlledEarlyReturn = true;
379
398
  return result;
380
399
  }
381
400
  // Steering
@@ -404,6 +423,37 @@ function vehicleUnpackUpdate(bs, isInitial, conn) {
404
423
  }
405
424
  return result;
406
425
  }
426
+ /**
427
+ * ShapeBase::readPacketData (FUN_005ee9f0): GameBase (empty) + energy
428
+ * level (F32, via setEnergyLevel) + recharge rate (F32).
429
+ */
430
+ function shapeBaseReadPacketData(bs, _conn) {
431
+ return {
432
+ energyLevel: bs.readF32(),
433
+ rechargeRate: bs.readF32(),
434
+ };
435
+ }
436
+ /**
437
+ * GameBase::readPacketData (FUN_005e32c0) is empty. Every GameBase-derived
438
+ * class that does not override it reads nothing when it is the control
439
+ * object.
440
+ */
441
+ function gameBaseReadPacketData(_bs, _conn) {
442
+ return {};
443
+ }
444
+ /**
445
+ * Turret::readPacketData (FUN_00655d70): ShapeBase::readPacketData, then a
446
+ * ranged 0..4 state (getBinLog2(getNextPow2(5)) = 3 bits) and three F32s
447
+ * that the engine copies into its current barrel-rotation state.
448
+ */
449
+ function turretReadPacketData(bs, conn) {
450
+ const base = shapeBaseReadPacketData(bs, conn);
451
+ return {
452
+ ...base,
453
+ turretState: bs.readRangedU32(0, 4),
454
+ rotationValues: [bs.readF32(), bs.readF32(), bs.readF32()],
455
+ };
456
+ }
407
457
  function vehicleReadPacketData(bs, conn) {
408
458
  // Vehicle::readPacketData (binary FUN_0060d740)
409
459
  // ShapeBase::readPacketData → GameBase::readPacketData (empty) + energy + recharge
@@ -412,7 +462,7 @@ function vehicleReadPacketData(bs, conn) {
412
462
  result.rechargeRate = bs.readF32();
413
463
  // Vehicle-specific rigid body state
414
464
  result.steering = { x: bs.readF32(), y: bs.readF32() };
415
- const linPos = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
465
+ const linPos = bs.readPoint3F();
416
466
  result.linPosition = linPos;
417
467
  result.angPosition = {
418
468
  x: bs.readF32(),
@@ -483,7 +533,7 @@ function itemUnpackUpdate(bs, isInitial, conn) {
483
533
  }
484
534
  // ThrowSrcMask
485
535
  if (bs.readFlag()) {
486
- result.collisionObject = bs.readInt(10);
536
+ result.collisionObject = readGhostRef10(bs);
487
537
  }
488
538
  // RotationMask (only if !rotate)
489
539
  if (bs.readFlag()) {
@@ -593,13 +643,12 @@ function debrisUnpackUpdate(bs, isInitial, conn) {
593
643
  // 2 × string
594
644
  result.string0 = bs.readString();
595
645
  result.string1 = bs.readString();
596
- // 3 conditional object references
646
+ // 3 conditional object references (-1 when absent)
597
647
  const refs = [];
598
- for (let i = 0; i < 2; i++) {
648
+ for (let i = 0; i < 3; i++) {
599
649
  refs.push(bs.readFlag() ? readObjectRef11(bs) : -1);
600
650
  }
601
651
  result.objectRefs = refs;
602
- result.objectRef2 = bs.readFlag() ? readObjectRef11(bs) : -1;
603
652
  return result;
604
653
  }
605
654
  function projectileUnpackUpdate(bs, isInitial, conn) {
@@ -611,51 +660,40 @@ function projectileUnpackUpdate(bs, isInitial, conn) {
611
660
  // ============================================================
612
661
  function bombProjectileUnpackUpdate(bs, _isInitial, _conn) {
613
662
  // BombProjectile vtable +0x4c -> FUN_00636050.
614
- const result = {};
615
- // Parent GameBase::unpackUpdate
616
- if (bs.readFlag()) {
617
- result.dataBlockId = readObjectRef11(bs);
618
- }
619
- if (bs.readFlag()) {
620
- const hasTargetId = bs.readFlag();
621
- result.targetId = hasTargetId ? bs.readInt(9) : -1;
622
- }
663
+ const result = readGameBaseUpdate(bs, _isInitial, _conn);
623
664
  if (!bs.readFlag()) {
624
665
  if (bs.readFlag()) {
625
- result.position = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
626
- result.velocity = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
666
+ result.position = bs.readPoint3F();
667
+ result.velocity = bs.readPoint3F();
627
668
  }
628
669
  if (!bs.readFlag()) {
629
670
  return result;
630
671
  }
631
- result.endPoint = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
632
- result.endNormal = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
672
+ result.endPoint = bs.readPoint3F();
673
+ result.endNormal = bs.readPoint3F();
633
674
  return result;
634
675
  }
635
- result.position = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
636
- result.velocity = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
676
+ result.position = bs.readPoint3F();
677
+ result.velocity = bs.readPoint3F();
637
678
  result.currTick = bs.readInt(12); // nextPow2(0x1000) -> 12 bits
638
679
  if (bs.readFlag()) {
639
680
  result.resetFlag = true; // FUN_00636ae0 side-effect-only
640
681
  }
641
682
  if (bs.readFlag()) {
642
- result.explodePoint = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
643
- result.explodeNormal = {
644
- x: bs.readF32(),
645
- y: bs.readF32(),
646
- z: bs.readF32(),
647
- };
683
+ result.explodePoint = bs.readPoint3F();
684
+ result.explodeNormal = bs.readPoint3F();
648
685
  }
649
686
  if (bs.readFlag()) {
650
- result.sourceObject = bs.readInt(11); // nextPow2(0x401) -> 11 bits
651
- result.sourceSlot = bs.readInt(3); // nextPow2(8) -> 3 bits
687
+ // nextPow2(0x401) -> 11 bits, nextPow2(8) -> 3 bits
688
+ ({ sourceObject: result.sourceObject, sourceSlot: result.sourceSlot } =
689
+ readSourceObjectSlot(bs));
652
690
  }
653
691
  else {
654
692
  result.sourceObject = -1;
655
693
  result.sourceSlot = -1;
656
694
  }
657
695
  if (bs.readFlag()) {
658
- result.vehicleObject = bs.readInt(11); // nextPow2(0x401) -> 11 bits
696
+ result.vehicleObject = readObjectRef11(bs); // nextPow2(0x401) -> 11 bits
659
697
  }
660
698
  else {
661
699
  result.vehicleObject = 0;
@@ -676,11 +714,11 @@ function grenadeUnpackUpdate(bs, isInitial, conn) {
676
714
  result.explodeNormal = bs.readPoint3F();
677
715
  }
678
716
  if (bs.readFlag()) {
679
- result.sourceObject = bs.readRangedU32(0, 1024);
680
- result.sourceSlot = bs.readRangedU32(0, 7);
717
+ ({ sourceObject: result.sourceObject, sourceSlot: result.sourceSlot } =
718
+ readSourceObjectSlot(bs));
681
719
  }
682
720
  if (bs.readFlag()) {
683
- result.vehicleObject = bs.readRangedU32(0, 1024);
721
+ result.vehicleObject = readObjectRef11(bs); // writeRangedU32(0, 1024)
684
722
  }
685
723
  }
686
724
  else {
@@ -708,34 +746,22 @@ function seekerUnpackUpdate(bs, isInitial, conn) {
708
746
  const isExplosion = bs.readFlag();
709
747
  if (isExplosion) {
710
748
  // FUN_00639370: explode(position, normal) — SeekerProjectile detonation.
711
- result.explodePosition = {
712
- x: bs.readF32(),
713
- y: bs.readF32(),
714
- z: bs.readF32(),
715
- };
716
- result.explodeNormal = {
717
- x: bs.readF32(),
718
- y: bs.readF32(),
719
- z: bs.readF32(),
720
- };
749
+ result.explodePosition = bs.readPoint3F();
750
+ result.explodeNormal = bs.readPoint3F();
721
751
  return result;
722
752
  }
723
- result.position = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
724
- result.velocity = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
753
+ result.position = bs.readPoint3F();
754
+ result.velocity = bs.readPoint3F();
725
755
  const hasTargetInfo = bs.readFlag();
726
756
  if (hasTargetInfo) {
727
757
  const hasTargetGhost = bs.readFlag();
728
758
  if (!hasTargetGhost) {
729
- result.targetDirection = {
730
- x: bs.readF32(),
731
- y: bs.readF32(),
732
- z: bs.readF32(),
733
- };
759
+ result.targetDirection = bs.readPoint3F();
734
760
  result.targetMode = 1;
735
761
  }
736
762
  else {
737
763
  // getBitCount(getNextPow2(0x401)) -> 11 bits
738
- result.targetGhost = bs.readInt(11);
764
+ result.targetGhost = readObjectRef11(bs);
739
765
  result.targetMode = 0;
740
766
  }
741
767
  }
@@ -745,13 +771,14 @@ function seekerUnpackUpdate(bs, isInitial, conn) {
745
771
  return result;
746
772
  }
747
773
  // 0x0063c010 full-state path
748
- result.position = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
749
- result.velocity = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
750
- result.orientation = { x: bs.readF32(), y: bs.readF32(), z: bs.readF32() };
774
+ result.position = bs.readPoint3F();
775
+ result.velocity = bs.readPoint3F();
776
+ result.orientation = bs.readPoint3F();
751
777
  // Optional source object + slot
752
778
  if (bs.readFlag()) {
753
- result.sourceObject = bs.readInt(11); // getBitCount(getNextPow2(0x401))
754
- result.sourceSlot = bs.readInt(3); // getBitCount(getNextPow2(8))
779
+ // getBitCount(getNextPow2(0x401)) = 11, getBitCount(getNextPow2(8)) = 3
780
+ ({ sourceObject: result.sourceObject, sourceSlot: result.sourceSlot } =
781
+ readSourceObjectSlot(bs));
755
782
  }
756
783
  else {
757
784
  result.sourceObject = -1;
@@ -761,15 +788,11 @@ function seekerUnpackUpdate(bs, isInitial, conn) {
761
788
  if (bs.readFlag()) {
762
789
  const hasTargetGhost = bs.readFlag();
763
790
  if (!hasTargetGhost) {
764
- result.targetDirection = {
765
- x: bs.readF32(),
766
- y: bs.readF32(),
767
- z: bs.readF32(),
768
- };
791
+ result.targetDirection = bs.readPoint3F();
769
792
  result.targetMode = 1;
770
793
  }
771
794
  else {
772
- result.targetGhost = bs.readInt(11); // getBitCount(getNextPow2(0x401))
795
+ result.targetGhost = readObjectRef11(bs); // getBitCount(getNextPow2(0x401))
773
796
  result.targetMode = 0;
774
797
  }
775
798
  }
@@ -883,11 +906,7 @@ function cameraReadPacketData(bs, conn) {
883
906
  // Parent::readPacketData + absolute camera transform basis + mode-specific orbit payload.
884
907
  result.energyLevel = bs.readF32();
885
908
  result.rechargeRate = bs.readF32();
886
- const pos = {
887
- x: bs.readF32(),
888
- y: bs.readF32(),
889
- z: bs.readF32(),
890
- };
909
+ const pos = bs.readPoint3F();
891
910
  result.position = pos;
892
911
  result.rotX = bs.readF32();
893
912
  result.rotZ = bs.readF32();
@@ -902,7 +921,7 @@ function cameraReadPacketData(bs, conn) {
902
921
  // OrbitObjectMode: observing flag + always-read 10-bit target ghost index.
903
922
  if (cameraMode === 3) {
904
923
  result.observingClientObject = bs.readFlag();
905
- result.orbitObjectGhostIndex = bs.readInt(10);
924
+ result.orbitObjectGhostIndex = readGhostRef10(bs);
906
925
  }
907
926
  // OrbitPointMode: NetConnection::readCompressed using current compression point.
908
927
  if (cameraMode === 4) {
@@ -935,7 +954,7 @@ function linearProjectileUnpackUpdate(bs, isInitial, conn) {
935
954
  result.direction = bs.readNormalVector(14); // binary: 0xe
936
955
  result.currTick = bs.readRangedU32(0, 511); // nextPow2(0x200)=9 bits
937
956
  if (bs.readFlag()) {
938
- result.sourceObject = bs.readInt(10); // nextPow2(0x400)=10 bits
957
+ result.sourceObject = readGhostRef10(bs); // nextPow2(0x400)=10 bits
939
958
  result.sourceSlot = bs.readRangedU32(0, 7); // nextPow2(8)=3 bits
940
959
  if (bs.readFlag()) {
941
960
  result.excessVel = bs.readRangedU32(0, 255); // nextPow2(0x100)=8 bits
@@ -943,7 +962,7 @@ function linearProjectileUnpackUpdate(bs, isInitial, conn) {
943
962
  }
944
963
  }
945
964
  if (bs.readFlag()) {
946
- result.vehicleObject = bs.readInt(10); // nextPow2(0x400)=10 bits
965
+ result.vehicleObject = readGhostRef10(bs); // nextPow2(0x400)=10 bits
947
966
  }
948
967
  }
949
968
  }
@@ -963,9 +982,9 @@ function elfProjectileUnpackUpdate(bs, isInitial, conn) {
963
982
  const result = readGameBaseUpdate(bs, isInitial, conn);
964
983
  if (bs.readFlag()) {
965
984
  if (bs.readFlag()) {
966
- result.sourceObject = bs.readRangedU32(0, 1024);
967
- result.sourceSlot = bs.readRangedU32(0, 7);
968
- result.targetObject = bs.readRangedU32(0, 1024);
985
+ ({ sourceObject: result.sourceObject, sourceSlot: result.sourceSlot } =
986
+ readSourceObjectSlot(bs));
987
+ result.targetObject = readObjectRef11(bs); // writeRangedU32(0, 1024)
969
988
  }
970
989
  }
971
990
  return result;
@@ -979,9 +998,9 @@ function repairProjectileUnpackUpdate(bs, isInitial, conn) {
979
998
  if (bs.readFlag()) {
980
999
  // InitialUpdateMask
981
1000
  if (bs.readFlag()) {
982
- result.sourceObject = bs.readRangedU32(0, 1024);
983
- result.sourceSlot = bs.readRangedU32(0, 7);
984
- result.repairingObject = bs.readRangedU32(0, 1024);
1001
+ ({ sourceObject: result.sourceObject, sourceSlot: result.sourceSlot } =
1002
+ readSourceObjectSlot(bs));
1003
+ result.repairingObject = readObjectRef11(bs); // writeRangedU32(0, 1024)
985
1004
  }
986
1005
  }
987
1006
  // Non-initial update: no data
@@ -999,26 +1018,33 @@ function targetProjectileUnpackUpdate(bs, isInitial, conn) {
999
1018
  result.endPos = bs.readPoint3F();
1000
1019
  result.truncated = bs.readFlag();
1001
1020
  if (bs.readFlag()) {
1002
- result.sourceObject = bs.readRangedU32(0, 1024);
1003
- result.sourceSlot = bs.readRangedU32(0, 7);
1021
+ ({ sourceObject: result.sourceObject, sourceSlot: result.sourceSlot } =
1022
+ readSourceObjectSlot(bs));
1004
1023
  result.clientOwned = bs.readFlag();
1005
1024
  }
1006
1025
  }
1007
1026
  else {
1008
- // Swing update
1009
- if (bs.readFlag()) {
1010
- result.sourceObject = bs.readRangedU32(0, 1024);
1011
- result.sourceSlot = bs.readRangedU32(0, 7);
1012
- result.clientOwned = bs.readFlag();
1013
- }
1014
- else {
1015
- result.initialPosition = bs.readPoint3F();
1016
- }
1017
- result.endPos = bs.readPoint3F();
1018
- result.truncated = bs.readFlag();
1027
+ readBeamProjectileSwing(bs, result);
1019
1028
  }
1020
1029
  return result;
1021
1030
  }
1031
+ /**
1032
+ * Non-initial update shared by TargetProjectile and SniperProjectile:
1033
+ * either a source object/slot pair or a fresh start point, then the end
1034
+ * point and truncation flag.
1035
+ */
1036
+ function readBeamProjectileSwing(bs, result) {
1037
+ if (bs.readFlag()) {
1038
+ ({ sourceObject: result.sourceObject, sourceSlot: result.sourceSlot } =
1039
+ readSourceObjectSlot(bs));
1040
+ result.clientOwned = bs.readFlag();
1041
+ }
1042
+ else {
1043
+ result.initialPosition = bs.readPoint3F();
1044
+ }
1045
+ result.endPos = bs.readPoint3F();
1046
+ result.truncated = bs.readFlag();
1047
+ }
1022
1048
  // ============================================================
1023
1049
  // WayPoint ghost parser
1024
1050
  // ============================================================
@@ -1055,10 +1081,10 @@ function spawnSphereUnpackUpdate(bs, isInitial, conn) {
1055
1081
  // ============================================================
1056
1082
  // ForceFieldBare ghost parser
1057
1083
  // ============================================================
1058
- function forceFieldBareUnpackUpdate(bs, _isInitial, _conn) {
1084
+ function forceFieldBareUnpackUpdate(bs, isInitial, conn) {
1059
1085
  // ForceFieldBare::unpackUpdate (FUN_00676d30):
1060
1086
  // GameBase parent, then two-level transform flags, then StateChangeMask.
1061
- const result = readGameBaseUpdate(bs, _isInitial, _conn);
1087
+ const result = readGameBaseUpdate(bs, isInitial, conn);
1062
1088
  // InitialUpdateMask flag — if true, reads transform+scale (initial path)
1063
1089
  if (bs.readFlag()) {
1064
1090
  result.transform = bs.readAffineTransform();
@@ -1104,6 +1130,20 @@ function tsStaticUnpackUpdate(bs, _isInitial, _conn) {
1104
1130
  // ============================================================
1105
1131
  // TerrainBlock ghost parser
1106
1132
  // ============================================================
1133
+ /** U32 count + count × U32. The count is only trusted if that many
1134
+ * words can still fit in the stream (a corrupt count would otherwise
1135
+ * spin until memory runs out, since exhausted reads return 0). */
1136
+ function readEmptySquareRuns(bs) {
1137
+ const size = bs.readU32();
1138
+ if (size > bs.getRemainingBits() / 32) {
1139
+ throw new Error(`Invalid TerrainBlock emptySquareRun count: ${size}`);
1140
+ }
1141
+ const runs = [];
1142
+ for (let i = 0; i < size; i++) {
1143
+ runs.push(bs.readU32());
1144
+ }
1145
+ return runs;
1146
+ }
1107
1147
  function terrainBlockUnpackUpdate(bs, _isInitial, _conn) {
1108
1148
  // TerrainBlock extends SceneObject (NOT GameBase).
1109
1149
  // V12 source: terrain/terrData.cc lines 910-941
@@ -1119,25 +1159,17 @@ function terrainBlockUnpackUpdate(bs, _isInitial, _conn) {
1119
1159
  result.terrFileName = bs.readString();
1120
1160
  result.detailTextureName = bs.readString();
1121
1161
  result.squareSize = bs.readU32();
1122
- const size = bs.readU32();
1123
- const emptySquareRuns = [];
1124
- for (let i = 0; i < size; i++) {
1125
- emptySquareRuns.push(bs.readU32());
1126
- }
1127
- result.emptySquareRuns = emptySquareRuns;
1128
- result.emptySquareRunCount = size;
1162
+ const runs = readEmptySquareRuns(bs);
1163
+ result.emptySquareRuns = runs;
1164
+ result.emptySquareRunCount = runs.length;
1129
1165
  }
1130
1166
  else {
1131
1167
  // Normal update
1132
1168
  if (bs.readFlag()) {
1133
1169
  // EmptyMask
1134
- const size = bs.readU32();
1135
- const emptySquareRuns = [];
1136
- for (let i = 0; i < size; i++) {
1137
- emptySquareRuns.push(bs.readU32());
1138
- }
1139
- result.emptySquareRuns = emptySquareRuns;
1140
- result.emptySquareRunCount = size;
1170
+ const runs = readEmptySquareRuns(bs);
1171
+ result.emptySquareRuns = runs;
1172
+ result.emptySquareRunCount = runs.length;
1141
1173
  }
1142
1174
  }
1143
1175
  return result;
@@ -1471,23 +1503,13 @@ function sniperProjectileUnpackUpdate(bs, isInitial, conn) {
1471
1503
  result.truncated = bs.readFlag();
1472
1504
  result.hitWater = bs.readFlag();
1473
1505
  if (bs.readFlag()) {
1474
- result.sourceObject = bs.readRangedU32(0, 1024);
1475
- result.sourceSlot = bs.readRangedU32(0, 7);
1506
+ ({ sourceObject: result.sourceObject, sourceSlot: result.sourceSlot } =
1507
+ readSourceObjectSlot(bs));
1476
1508
  result.clientOwned = bs.readFlag();
1477
1509
  }
1478
1510
  }
1479
1511
  else {
1480
- // Swing update
1481
- if (bs.readFlag()) {
1482
- result.sourceObject = bs.readRangedU32(0, 1024);
1483
- result.sourceSlot = bs.readRangedU32(0, 7);
1484
- result.clientOwned = bs.readFlag();
1485
- }
1486
- else {
1487
- result.initialPosition = bs.readPoint3F();
1488
- }
1489
- result.endPos = bs.readPoint3F();
1490
- result.truncated = bs.readFlag();
1512
+ readBeamProjectileSwing(bs, result);
1491
1513
  }
1492
1514
  return result;
1493
1515
  }
@@ -1499,7 +1521,7 @@ function shockLanceProjectileUnpackUpdate(bs, isInitial, conn) {
1499
1521
  const result = readGameBaseUpdate(bs, isInitial, conn);
1500
1522
  // Target (always written)
1501
1523
  if (bs.readFlag()) {
1502
- result.targetObject = bs.readRangedU32(0, 1024);
1524
+ result.targetObject = readObjectRef11(bs); // writeRangedU32(0, 1024)
1503
1525
  }
1504
1526
  // Initial update
1505
1527
  if (bs.readFlag()) {
@@ -1507,8 +1529,8 @@ function shockLanceProjectileUnpackUpdate(bs, isInitial, conn) {
1507
1529
  result.end = bs.readPoint3F();
1508
1530
  result.hitObject = bs.readFlag();
1509
1531
  if (bs.readFlag()) {
1510
- result.sourceObject = bs.readRangedU32(0, 1024);
1511
- result.sourceSlot = bs.readRangedU32(0, 7);
1532
+ ({ sourceObject: result.sourceObject, sourceSlot: result.sourceSlot } =
1533
+ readSourceObjectSlot(bs));
1512
1534
  }
1513
1535
  }
1514
1536
  return result;
@@ -1717,7 +1739,7 @@ function stationFXPersonalUnpackUpdate(bs, isInitial, conn) {
1717
1739
  if (bs.readFlag()) {
1718
1740
  // InitialUpdateMask
1719
1741
  if (bs.readFlag()) {
1720
- result.stationObject = bs.readRangedU32(0, 1024);
1742
+ result.stationObject = readObjectRef11(bs); // writeRangedU32(0, 1024)
1721
1743
  }
1722
1744
  }
1723
1745
  return result;
@@ -1768,7 +1790,7 @@ export function registerGhostParsers(registry) {
1768
1790
  registry.catalogGhost({
1769
1791
  name: "ShapeBase",
1770
1792
  unpackUpdate: readShapeBaseUpdate,
1771
- readPacketData: vehicleReadPacketData,
1793
+ readPacketData: shapeBaseReadPacketData,
1772
1794
  });
1773
1795
  registry.catalogGhost({
1774
1796
  name: "Vehicle",
@@ -1824,6 +1846,7 @@ export function registerGhostParsers(registry) {
1824
1846
  registry.catalogGhost({
1825
1847
  name: "Turret",
1826
1848
  unpackUpdate: turretUnpackUpdate,
1849
+ readPacketData: turretReadPacketData,
1827
1850
  });
1828
1851
  registry.catalogGhost({
1829
1852
  name: "InteriorInstance",
@@ -1966,5 +1989,67 @@ export function registerGhostParsers(registry) {
1966
1989
  name: "StationFXVehicle",
1967
1990
  unpackUpdate: stationFXPersonalUnpackUpdate,
1968
1991
  });
1992
+ applyInheritedReadPacketData(registry);
1993
+ }
1994
+ /**
1995
+ * Ghost classes that inherit readPacketData rather than overriding it, from
1996
+ * the build 25034 vtables (slot +0x108) cross-checked against the V12
1997
+ * class hierarchy. The engine dispatches virtually, so a StaticShape used
1998
+ * as the control object reads ShapeBase's two floats and a Trigger reads
1999
+ * nothing. Classes absent from both lists are not GameBase-derived and
2000
+ * have no readPacketData at all.
2001
+ */
2002
+ const SHAPEBASE_READ_PACKET_DATA_HEIRS = [
2003
+ "AIObjective",
2004
+ "BeaconObject",
2005
+ "Item",
2006
+ "MissionMarker",
2007
+ "ScopeAlwaysShape",
2008
+ "SpawnSphere",
2009
+ "StaticShape",
2010
+ "WayPoint",
2011
+ ];
2012
+ const GAMEBASE_READ_PACKET_DATA_HEIRS = [
2013
+ "BombProjectile",
2014
+ "Debris",
2015
+ "ELFProjectile",
2016
+ "EnergyProjectile",
2017
+ "FireballAtmosphere",
2018
+ "FlareProjectile",
2019
+ "ForceFieldBare",
2020
+ "GameBase",
2021
+ "GrenadeProjectile",
2022
+ "Lightning",
2023
+ "LinearFlareProjectile",
2024
+ "LinearProjectile",
2025
+ "ParticleEmissionDummy",
2026
+ "Precipitation",
2027
+ "Projectile",
2028
+ "RepairProjectile",
2029
+ "SeekerProjectile",
2030
+ "ShockLanceProjectile",
2031
+ "Shockwave",
2032
+ "SniperProjectile",
2033
+ "Splash",
2034
+ "StationFXPersonal",
2035
+ "StationFXVehicle",
2036
+ "TargetProjectile",
2037
+ "TracerProjectile",
2038
+ "Trigger",
2039
+ ];
2040
+ function applyInheritedReadPacketData(registry) {
2041
+ const catalog = registry.getGhostCatalog();
2042
+ for (const name of SHAPEBASE_READ_PACKET_DATA_HEIRS) {
2043
+ const entry = catalog.get(name);
2044
+ if (entry && !entry.readPacketData) {
2045
+ entry.readPacketData = shapeBaseReadPacketData;
2046
+ }
2047
+ }
2048
+ for (const name of GAMEBASE_READ_PACKET_DATA_HEIRS) {
2049
+ const entry = catalog.get(name);
2050
+ if (entry && !entry.readPacketData) {
2051
+ entry.readPacketData = gameBaseReadPacketData;
2052
+ }
2053
+ }
1969
2054
  }
1970
2055
  //# sourceMappingURL=GhostManager.js.map