t2-demo-parser 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +823 -0
  3. package/dist/BitStream.d.ts +143 -0
  4. package/dist/BitStream.d.ts.map +1 -0
  5. package/dist/BitStream.js +369 -0
  6. package/dist/BitStream.js.map +1 -0
  7. package/dist/BitStream.test.d.ts +2 -0
  8. package/dist/BitStream.test.d.ts.map +1 -0
  9. package/dist/BitStream.test.js +419 -0
  10. package/dist/BitStream.test.js.map +1 -0
  11. package/dist/ClassRegistry.d.ts +100 -0
  12. package/dist/ClassRegistry.d.ts.map +1 -0
  13. package/dist/ClassRegistry.js +118 -0
  14. package/dist/ClassRegistry.js.map +1 -0
  15. package/dist/ClassRegistry.test.d.ts +2 -0
  16. package/dist/ClassRegistry.test.d.ts.map +1 -0
  17. package/dist/ClassRegistry.test.js +83 -0
  18. package/dist/ClassRegistry.test.js.map +1 -0
  19. package/dist/DataBlockParsers.d.ts +3 -0
  20. package/dist/DataBlockParsers.d.ts.map +1 -0
  21. package/dist/DataBlockParsers.js +2041 -0
  22. package/dist/DataBlockParsers.js.map +1 -0
  23. package/dist/DemoParser.d.ts +155 -0
  24. package/dist/DemoParser.d.ts.map +1 -0
  25. package/dist/DemoParser.js +943 -0
  26. package/dist/DemoParser.js.map +1 -0
  27. package/dist/DemoParser.test.d.ts +2 -0
  28. package/dist/DemoParser.test.d.ts.map +1 -0
  29. package/dist/DemoParser.test.js +472 -0
  30. package/dist/DemoParser.test.js.map +1 -0
  31. package/dist/EventParsers.d.ts +3 -0
  32. package/dist/EventParsers.d.ts.map +1 -0
  33. package/dist/EventParsers.js +603 -0
  34. package/dist/EventParsers.js.map +1 -0
  35. package/dist/GhostManager.d.ts +13 -0
  36. package/dist/GhostManager.d.ts.map +1 -0
  37. package/dist/GhostManager.js +2002 -0
  38. package/dist/GhostManager.js.map +1 -0
  39. package/dist/GhostTracker.test.d.ts +2 -0
  40. package/dist/GhostTracker.test.d.ts.map +1 -0
  41. package/dist/GhostTracker.test.js +64 -0
  42. package/dist/GhostTracker.test.js.map +1 -0
  43. package/dist/HuffmanProcessor.d.ts +12 -0
  44. package/dist/HuffmanProcessor.d.ts.map +1 -0
  45. package/dist/HuffmanProcessor.js +163 -0
  46. package/dist/HuffmanProcessor.js.map +1 -0
  47. package/dist/LiveParser.d.ts +16 -0
  48. package/dist/LiveParser.d.ts.map +1 -0
  49. package/dist/LiveParser.js +29 -0
  50. package/dist/LiveParser.js.map +1 -0
  51. package/dist/PacketParser.d.ts +104 -0
  52. package/dist/PacketParser.d.ts.map +1 -0
  53. package/dist/PacketParser.js +713 -0
  54. package/dist/PacketParser.js.map +1 -0
  55. package/dist/Timeline.d.ts +101 -0
  56. package/dist/Timeline.d.ts.map +1 -0
  57. package/dist/Timeline.js +251 -0
  58. package/dist/Timeline.js.map +1 -0
  59. package/dist/cli.d.ts +3 -0
  60. package/dist/cli.d.ts.map +1 -0
  61. package/dist/cli.js +372 -0
  62. package/dist/cli.js.map +1 -0
  63. package/dist/dataBlockDataTypes.d.ts +877 -0
  64. package/dist/dataBlockDataTypes.d.ts.map +1 -0
  65. package/dist/dataBlockDataTypes.js +2 -0
  66. package/dist/dataBlockDataTypes.js.map +1 -0
  67. package/dist/dataTypes.d.ts +31 -0
  68. package/dist/dataTypes.d.ts.map +1 -0
  69. package/dist/dataTypes.js +5 -0
  70. package/dist/dataTypes.js.map +1 -0
  71. package/dist/dump.d.ts +3 -0
  72. package/dist/dump.d.ts.map +1 -0
  73. package/dist/dump.js +36 -0
  74. package/dist/dump.js.map +1 -0
  75. package/dist/eventDataTypes.d.ts +199 -0
  76. package/dist/eventDataTypes.d.ts.map +1 -0
  77. package/dist/eventDataTypes.js +2 -0
  78. package/dist/eventDataTypes.js.map +1 -0
  79. package/dist/ghostDataTypes.d.ts +573 -0
  80. package/dist/ghostDataTypes.d.ts.map +1 -0
  81. package/dist/ghostDataTypes.js +2 -0
  82. package/dist/ghostDataTypes.js.map +1 -0
  83. package/dist/index.d.ts +17 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +9 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/types.d.ts +261 -0
  88. package/dist/types.d.ts.map +1 -0
  89. package/dist/types.js +204 -0
  90. package/dist/types.js.map +1 -0
  91. package/package.json +46 -0
@@ -0,0 +1,713 @@
1
+ import createDebug from "debug";
2
+ import { BitStream } from "./BitStream.js";
3
+ import { DataPacket, MaxGhostCount, GhostMsgEndGhosting, NetEventClassBitSize, NetEventClassFirst, NetObjectClassBitSize, NetObjectClassFirst, } from "./types.js";
4
+ const debugGhosts = createDebug("t2-demo-parser:ghosts");
5
+ /**
6
+ * Parses individual network packets from demo recording blocks.
7
+ *
8
+ * Packet layout (from the server connection perspective):
9
+ * 1. dnet header (processRawPacket)
10
+ * 2. Rate info (handlePacket)
11
+ * 3. GameConnection::readPacket — game state, moves, etc.
12
+ * 4. NetConnection::readPacket — events, then ghosts
13
+ */
14
+ export class PacketParser {
15
+ registry;
16
+ ghostTracker;
17
+ compressionPoint = { x: 0, y: 0, z: 0 };
18
+ controlParserByGhostIndex = new Map();
19
+ dataBlockDataMap;
20
+ lastSeqRecvdAtSend = new Array(32).fill(0);
21
+ lastSeqRecvd = 0;
22
+ highestAckedSeq = 0;
23
+ lastSendSeq = 0;
24
+ recvAckMask = 0;
25
+ connectSequence = 0;
26
+ lastRecvAckAck = 0;
27
+ _connectionEstablished = false;
28
+ nextRecvEventSeq = 0;
29
+ pendingGuaranteedEvents = [];
30
+ // Stats
31
+ controlObjectParsed = 0;
32
+ controlObjectFailed = 0;
33
+ eventsParsed = 0;
34
+ eventsFailed = 0;
35
+ ghostCreatesParsed = 0;
36
+ ghostUpdatesParsed = 0;
37
+ ghostDeletes = 0;
38
+ ghostsFailed = 0;
39
+ /** Ghosts where our tracker says "new" but the classId is unregistered.
40
+ * This indicates a tracker divergence: the server sent UPDATE data
41
+ * (no classId prefix) for a ghost we don't have, so we misread the
42
+ * first 7 bits of update data as a classId. */
43
+ ghostsTrackerDiverged = 0;
44
+ packetsParsed = 0;
45
+ protocolRejected = 0;
46
+ protocolNoDispatch = 0;
47
+ constructor(registry, ghostTracker, options) {
48
+ this.registry = registry;
49
+ this.ghostTracker = ghostTracker;
50
+ this.dataBlockDataMap = options?.dataBlockDataMap;
51
+ if (options?.connectionProtocolState) {
52
+ this.setConnectionProtocolState(options.connectionProtocolState);
53
+ }
54
+ if (typeof options?.nextRecvEventSeq === "number") {
55
+ this.nextRecvEventSeq = options.nextRecvEventSeq >>> 0;
56
+ }
57
+ }
58
+ getCompressionPoint() {
59
+ return this.compressionPoint;
60
+ }
61
+ getDataBlockDataMap() {
62
+ return this.dataBlockDataMap;
63
+ }
64
+ getConnectionContext() {
65
+ const dbMap = this.dataBlockDataMap;
66
+ return {
67
+ compressionPoint: this.compressionPoint,
68
+ ghostTracker: this.ghostTracker,
69
+ getDataBlockParser: (classId) => this.registry.getDataBlockParser(classId),
70
+ getDataBlockData: dbMap
71
+ ? (objectId) => dbMap.get(objectId)
72
+ : undefined,
73
+ getGhostParser: (classId) => this.registry.getGhostParser(classId),
74
+ };
75
+ }
76
+ _setNextRecvEventSeq(nextRecvEventSeq) {
77
+ this.nextRecvEventSeq = nextRecvEventSeq >>> 0;
78
+ }
79
+ setConnectionProtocolState(state) {
80
+ this.lastSeqRecvdAtSend = state.lastSeqRecvdAtSend.slice(0, 32);
81
+ while (this.lastSeqRecvdAtSend.length < 32) {
82
+ this.lastSeqRecvdAtSend.push(0);
83
+ }
84
+ this.lastSeqRecvd = state.lastSeqRecvd >>> 0;
85
+ this.highestAckedSeq = state.highestAckedSeq >>> 0;
86
+ this.lastSendSeq = state.lastSendSeq >>> 0;
87
+ this.recvAckMask = state.ackMask >>> 0;
88
+ this.connectSequence = state.connectSequence >>> 0;
89
+ this.lastRecvAckAck = state.lastRecvAckAck >>> 0;
90
+ this._connectionEstablished = state.connectionEstablished;
91
+ }
92
+ /**
93
+ * Emulate ConnectionProtocol::buildSendPacketHeader (FUN_0043d2d0, data path).
94
+ * Demo block type 1 indicates a local send-packet trigger.
95
+ */
96
+ onSendPacketTrigger() {
97
+ this.lastSendSeq = (this.lastSendSeq + 1) >>> 0;
98
+ this.lastSeqRecvdAtSend[this.lastSendSeq & 0x1f] = this.lastSeqRecvd >>> 0;
99
+ }
100
+ /**
101
+ * Emulates ConnectionProtocol::processRawPacket (FUN_0043d4d0).
102
+ * A packet may be syntactically valid but still ignored for game payload
103
+ * dispatch due to sequence/connect-window checks or duplicate seq numbers.
104
+ */
105
+ applyProtocolHeader(dnetHeader) {
106
+ if (dnetHeader.connectSeqBit !== (this.connectSequence & 1)) {
107
+ return { accepted: false, dispatchData: false };
108
+ }
109
+ if (dnetHeader.ackByteCount > 4 || dnetHeader.packetType > 2) {
110
+ return { accepted: false, dispatchData: false };
111
+ }
112
+ let seqNumber = (dnetHeader.seqNumber | (this.lastSeqRecvd & 0xffff_fe00)) >>> 0;
113
+ if (seqNumber < this.lastSeqRecvd) {
114
+ seqNumber = (seqNumber + 0x200) >>> 0;
115
+ }
116
+ if (this.lastSeqRecvd + 0x1f < seqNumber) {
117
+ return { accepted: false, dispatchData: false };
118
+ }
119
+ let highestAck = (dnetHeader.highestAck | (this.highestAckedSeq & 0xffff_fe00)) >>> 0;
120
+ if (highestAck < this.highestAckedSeq) {
121
+ highestAck = (highestAck + 0x200) >>> 0;
122
+ }
123
+ if (this.lastSendSeq < highestAck) {
124
+ return { accepted: false, dispatchData: false };
125
+ }
126
+ const seqShift = (seqNumber - this.lastSeqRecvd) & 0x1f;
127
+ this.recvAckMask = (this.recvAckMask << seqShift) >>> 0;
128
+ if (dnetHeader.packetType === DataPacket) {
129
+ this.recvAckMask = (this.recvAckMask | 1) >>> 0;
130
+ }
131
+ for (let ackSeq = this.highestAckedSeq + 1; ackSeq <= highestAck; ackSeq++) {
132
+ const isAcked = (dnetHeader.ackMask & (1 << ((highestAck - ackSeq) & 0x1f))) !== 0;
133
+ if (isAcked) {
134
+ this.lastRecvAckAck = this.lastSeqRecvdAtSend[ackSeq & 0x1f] >>> 0;
135
+ }
136
+ }
137
+ if (seqNumber - this.lastRecvAckAck > 0x20) {
138
+ this.lastRecvAckAck = seqNumber - 0x20;
139
+ }
140
+ this.highestAckedSeq = highestAck;
141
+ const dispatchData = this.lastSeqRecvd !== seqNumber && dnetHeader.packetType === DataPacket;
142
+ this.lastSeqRecvd = seqNumber;
143
+ return { accepted: true, dispatchData };
144
+ }
145
+ parsePacket(data) {
146
+ const bs = new BitStream(data);
147
+ // 1. Parse dnet header
148
+ const dnetHeader = this.readDnetHeader(bs);
149
+ const protocol = this.applyProtocolHeader(dnetHeader);
150
+ this.packetsParsed++;
151
+ // ConnectionProtocol may reject this packet entirely (window/connect checks),
152
+ // or accept it but suppress game dispatch (duplicate seq / non-data packet).
153
+ if (!protocol.accepted) {
154
+ this.protocolRejected++;
155
+ return {
156
+ dnetHeader,
157
+ rateInfo: {},
158
+ gameState: this.emptyGameState(),
159
+ events: [],
160
+ ghosts: [],
161
+ };
162
+ }
163
+ if (!protocol.dispatchData) {
164
+ this.protocolNoDispatch++;
165
+ return {
166
+ dnetHeader,
167
+ rateInfo: {},
168
+ gameState: this.emptyGameState(),
169
+ events: [],
170
+ ghosts: [],
171
+ };
172
+ }
173
+ // 2. Rate info (NetConnection::handlePacket)
174
+ const rateInfo = this.readRateInfo(bs);
175
+ // 3. Game state (GameConnection::readPacket — server connection path)
176
+ bs.setStringBuffer(true);
177
+ const gameState = this.readGameState(bs);
178
+ // Only read events/ghosts if game state completed successfully.
179
+ // When game state bails (control object dirty with no parser), the
180
+ // stream position is wrong and we'd read garbage data.
181
+ const gameStateComplete = gameState.controlObjectDataStart === undefined ||
182
+ gameState.controlObjectData !== undefined;
183
+ // 4. Events (NetConnection::readPacket -> eventReadPacket)
184
+ const events = gameStateComplete ? this.readEvents(bs) : [];
185
+ // Only read ghosts if all events were successfully parsed.
186
+ // When an event can't be parsed, readEvents returns early and the
187
+ // stream position is wrong — reading ghosts would produce garbage.
188
+ const lastEvent = events[events.length - 1];
189
+ const eventsComplete = !lastEvent || lastEvent.dataBitsEnd !== lastEvent.dataBitsStart;
190
+ // 5. Ghosts (NetConnection::readPacket -> ghostReadPacket)
191
+ const ghostSectionStart = gameStateComplete && eventsComplete
192
+ ? bs.getCurPos()
193
+ : undefined;
194
+ const ghosts = gameStateComplete && eventsComplete
195
+ ? this.readGhosts(bs, dnetHeader.seqNumber)
196
+ : [];
197
+ bs.setStringBuffer(false);
198
+ return { dnetHeader, rateInfo, gameState, events, ghosts, ghostSectionStart };
199
+ }
200
+ /**
201
+ * Parse the dnet packet header (from processRawPacket in dnet.cc).
202
+ */
203
+ readDnetHeader(bs) {
204
+ const gameFlag = bs.readFlag();
205
+ const connectSeqBit = bs.readInt(1);
206
+ const seqNumber = bs.readInt(9);
207
+ const highestAck = bs.readInt(9);
208
+ const packetType = bs.readInt(2);
209
+ const ackByteCount = bs.readInt(3);
210
+ const ackMask = ackByteCount > 0 ? bs.readInt(8 * ackByteCount) : 0;
211
+ return {
212
+ gameFlag,
213
+ connectSeqBit,
214
+ seqNumber,
215
+ highestAck,
216
+ packetType,
217
+ ackByteCount,
218
+ ackMask,
219
+ };
220
+ }
221
+ /**
222
+ * Parse rate info from handlePacket in netConnection.cc.
223
+ */
224
+ readRateInfo(bs) {
225
+ const info = {};
226
+ if (bs.readFlag()) {
227
+ info.updateDelay = bs.readInt(10);
228
+ info.packetSize = bs.readInt(10);
229
+ }
230
+ if (bs.readFlag()) {
231
+ info.maxUpdateDelay = bs.readInt(10);
232
+ info.maxPacketSize = bs.readInt(10);
233
+ }
234
+ return info;
235
+ }
236
+ /**
237
+ * Parse game state from GameConnection::readPacket (server connection path).
238
+ */
239
+ readGameState(bs) {
240
+ const lastMoveAck = bs.readInt(32);
241
+ // Damage flash / whiteout
242
+ let damageFlash;
243
+ let whiteOut;
244
+ if (bs.readFlag()) {
245
+ if (bs.readFlag()) {
246
+ damageFlash = bs.readFloat(7);
247
+ }
248
+ if (bs.readFlag()) {
249
+ whiteOut = bs.readFloat(7) * 1.5;
250
+ }
251
+ }
252
+ // Lock/homing state
253
+ let selfLocked;
254
+ let selfHomed;
255
+ if (bs.readFlag()) {
256
+ selfLocked = bs.readFlag();
257
+ selfHomed = bs.readFlag();
258
+ }
259
+ // Seeker tracking state
260
+ let seekerTracking;
261
+ let seekerTrackingPos;
262
+ let seekerMode;
263
+ let seekerObjectGhostIndex;
264
+ let targetPos;
265
+ if (bs.readFlag()) {
266
+ seekerTracking = bs.readFlag();
267
+ // Binary 0x005fc020: when seekerTracking is true, reads 3×F32 (Point3F)
268
+ // for the seeker target position (stored at connection+0x8454)
269
+ if (seekerTracking) {
270
+ seekerTrackingPos = {
271
+ x: bs.readF32(),
272
+ y: bs.readF32(),
273
+ z: bs.readF32(),
274
+ };
275
+ }
276
+ seekerMode = bs.readRangedU32(0, 2);
277
+ if (seekerMode === 1) {
278
+ if (bs.readFlag()) {
279
+ seekerObjectGhostIndex = bs.readRangedU32(0, MaxGhostCount - 1);
280
+ }
281
+ }
282
+ else if (seekerMode === 2) {
283
+ targetPos = {
284
+ x: bs.readF32(),
285
+ y: bs.readF32(),
286
+ z: bs.readF32(),
287
+ };
288
+ }
289
+ }
290
+ // Sensor ping/jam flags
291
+ const pinged = bs.readFlag();
292
+ const jammed = bs.readFlag();
293
+ // Control object state
294
+ let controlObjectGhostIndex;
295
+ let controlObjectDataStart;
296
+ let controlObjectDataEnd;
297
+ let controlObjectData;
298
+ let compressionPoint;
299
+ if (bs.readFlag()) {
300
+ if (bs.readFlag()) {
301
+ // Control object is dirty — full update via readPacketData.
302
+ // In Tribes 2 (build 25034), only Player and Camera override
303
+ // writePacketData/readPacketData, so the control object is
304
+ // always one of these two classes. We try candidates in order:
305
+ // 1. Tracker classId (may be stale due to ghost index recycling)
306
+ // 2. Cached parser from previous successful parse of this index
307
+ // 3. Player (classId 25) — the normal control object
308
+ // 4. Camera (classId 4) — spectator mode
309
+ const gIndex = bs.readInt(10);
310
+ controlObjectGhostIndex = gIndex;
311
+ controlObjectDataStart = bs.getCurPos();
312
+ const start = bs.savePos();
313
+ const ghost = this.ghostTracker.getGhost(gIndex);
314
+ const preferredEntry = ghost
315
+ ? this.registry.getGhostParser(ghost.classId)
316
+ : undefined;
317
+ const cachedEntry = this.controlParserByGhostIndex.get(gIndex);
318
+ const playerEntry = this.registry.getGhostParser(25); // Player
319
+ const cameraEntry = this.registry.getGhostParser(4); // Camera
320
+ // Build candidate list (deduplicated, only those with readPacketData)
321
+ const candidates = [];
322
+ const seen = new Set();
323
+ const addCandidate = (entry) => {
324
+ if (!entry?.readPacketData)
325
+ return;
326
+ if (seen.has(entry.name))
327
+ return;
328
+ seen.add(entry.name);
329
+ candidates.push(entry);
330
+ };
331
+ addCandidate(preferredEntry);
332
+ addCandidate(cachedEntry);
333
+ addCandidate(playerEntry);
334
+ addCandidate(cameraEntry);
335
+ let parsed = false;
336
+ for (const entry of candidates) {
337
+ bs.restorePos(start);
338
+ try {
339
+ const conn = this.getConnectionContext();
340
+ const data = entry.readPacketData(bs, conn);
341
+ const bitsConsumed = bs.getCurPos() - controlObjectDataStart;
342
+ if (bitsConsumed <= 0 || bs.isError()) {
343
+ continue;
344
+ }
345
+ controlObjectData = data;
346
+ controlObjectDataEnd = bs.getCurPos();
347
+ this.controlParserByGhostIndex.set(gIndex, entry);
348
+ if (conn.compressionPoint !== this.compressionPoint) {
349
+ this.compressionPoint = conn.compressionPoint;
350
+ compressionPoint = this.compressionPoint;
351
+ }
352
+ this.controlObjectParsed++;
353
+ parsed = true;
354
+ break;
355
+ }
356
+ catch {
357
+ // Try next candidate.
358
+ }
359
+ }
360
+ if (!parsed) {
361
+ bs.restorePos(start);
362
+ // No parser — bail from game state
363
+ controlObjectDataEnd = controlObjectDataStart;
364
+ this.controlObjectFailed++;
365
+ return {
366
+ lastMoveAck,
367
+ damageFlash,
368
+ whiteOut,
369
+ selfLocked,
370
+ selfHomed,
371
+ seekerTracking,
372
+ seekerTrackingPos,
373
+ seekerMode,
374
+ seekerObjectGhostIndex,
375
+ targetPos,
376
+ pinged,
377
+ jammed,
378
+ controlObjectGhostIndex,
379
+ controlObjectDataStart,
380
+ controlObjectDataEnd,
381
+ controlObjectData,
382
+ targetVisibility: [],
383
+ };
384
+ }
385
+ }
386
+ else {
387
+ // Compression point update — always accept (engine doesn't validate)
388
+ compressionPoint = {
389
+ x: bs.readF32(),
390
+ y: bs.readF32(),
391
+ z: bs.readF32(),
392
+ };
393
+ this.compressionPoint = compressionPoint;
394
+ }
395
+ }
396
+ // Target visibility masks
397
+ const targetVisibility = [];
398
+ while (bs.readFlag()) {
399
+ targetVisibility.push({
400
+ index: bs.readInt(4),
401
+ mask: bs.readInt(32),
402
+ });
403
+ }
404
+ // Camera FOV
405
+ let cameraFov;
406
+ if (bs.readFlag()) {
407
+ cameraFov = bs.readInt(8);
408
+ }
409
+ return {
410
+ lastMoveAck,
411
+ damageFlash,
412
+ whiteOut,
413
+ selfLocked,
414
+ selfHomed,
415
+ seekerTracking,
416
+ seekerTrackingPos,
417
+ seekerMode,
418
+ seekerObjectGhostIndex,
419
+ targetPos,
420
+ pinged,
421
+ jammed,
422
+ controlObjectGhostIndex,
423
+ controlObjectDataStart,
424
+ controlObjectDataEnd,
425
+ controlObjectData,
426
+ compressionPoint,
427
+ targetVisibility: targetVisibility.length > 0 ? targetVisibility : undefined,
428
+ cameraFov,
429
+ };
430
+ }
431
+ /**
432
+ * Parse events from eventReadPacket in netEvent.cc.
433
+ *
434
+ * Two phases:
435
+ * 1. Unguaranteed events: while(readFlag) { classId(6b) + unpack }
436
+ * 2. Guaranteed events: while(readFlag) { seqFlag(1b), [seq(7b)], classId(6b) + unpack }
437
+ */
438
+ readEvents(bs) {
439
+ const dispatchedEvents = [];
440
+ let unguaranteedPhase = true;
441
+ let prevGuaranteedSeq = -2;
442
+ while (true) {
443
+ const bit = bs.readFlag();
444
+ if (unguaranteedPhase && !bit) {
445
+ // Transition to guaranteed phase. Binary immediately reads one more
446
+ // continuation bit for that phase.
447
+ unguaranteedPhase = false;
448
+ const guaranteedBit = bs.readFlag();
449
+ if (!guaranteedBit) {
450
+ this.dispatchGuaranteedEvents(dispatchedEvents);
451
+ break;
452
+ }
453
+ }
454
+ else if (!unguaranteedPhase && !bit) {
455
+ this.dispatchGuaranteedEvents(dispatchedEvents);
456
+ break;
457
+ }
458
+ else if (!bit) {
459
+ break;
460
+ }
461
+ let sequenceNumber;
462
+ let absoluteSequenceNumber;
463
+ if (!unguaranteedPhase) {
464
+ if (bs.readFlag()) {
465
+ sequenceNumber = (prevGuaranteedSeq + 1) & 0x7f;
466
+ }
467
+ else {
468
+ sequenceNumber = bs.readInt(7);
469
+ }
470
+ prevGuaranteedSeq = sequenceNumber;
471
+ absoluteSequenceNumber =
472
+ sequenceNumber | (this.nextRecvEventSeq & 0xffffff80);
473
+ if (absoluteSequenceNumber < this.nextRecvEventSeq) {
474
+ absoluteSequenceNumber += 0x80;
475
+ }
476
+ }
477
+ const classId = bs.readInt(NetEventClassBitSize) + NetEventClassFirst;
478
+ const dataBitsStart = bs.getCurPos();
479
+ const parserEntry = this.registry.getEventParser(classId);
480
+ let parsedData;
481
+ if (parserEntry) {
482
+ try {
483
+ const conn = this.getConnectionContext();
484
+ parsedData = parserEntry.unpack(bs, conn);
485
+ this.eventsParsed++;
486
+ }
487
+ catch {
488
+ this.eventsFailed++;
489
+ dispatchedEvents.push({
490
+ classId,
491
+ guaranteed: !unguaranteedPhase,
492
+ sequenceNumber,
493
+ absoluteSequenceNumber,
494
+ dataBitsStart,
495
+ dataBitsEnd: dataBitsStart,
496
+ });
497
+ return dispatchedEvents;
498
+ }
499
+ }
500
+ else {
501
+ // No parser — cannot advance safely.
502
+ dispatchedEvents.push({
503
+ classId,
504
+ guaranteed: !unguaranteedPhase,
505
+ sequenceNumber,
506
+ absoluteSequenceNumber,
507
+ dataBitsStart,
508
+ dataBitsEnd: dataBitsStart,
509
+ });
510
+ return dispatchedEvents;
511
+ }
512
+ const event = {
513
+ classId,
514
+ guaranteed: !unguaranteedPhase,
515
+ sequenceNumber,
516
+ absoluteSequenceNumber,
517
+ dataBitsStart,
518
+ dataBitsEnd: bs.getCurPos(),
519
+ parsedData,
520
+ };
521
+ if (unguaranteedPhase) {
522
+ dispatchedEvents.push(event);
523
+ if (parsedData) {
524
+ this.applyEventSideEffects(parsedData);
525
+ }
526
+ }
527
+ else if (absoluteSequenceNumber !== undefined) {
528
+ this.enqueueGuaranteedEvent(absoluteSequenceNumber, event);
529
+ }
530
+ }
531
+ return dispatchedEvents;
532
+ }
533
+ enqueueGuaranteedEvent(absoluteSequenceNumber, event) {
534
+ let insertAt = 0;
535
+ while (insertAt < this.pendingGuaranteedEvents.length &&
536
+ this.pendingGuaranteedEvents[insertAt].absoluteSequenceNumber <
537
+ absoluteSequenceNumber) {
538
+ insertAt++;
539
+ }
540
+ this.pendingGuaranteedEvents.splice(insertAt, 0, {
541
+ absoluteSequenceNumber,
542
+ event,
543
+ });
544
+ }
545
+ dispatchGuaranteedEvents(dispatchedEvents) {
546
+ while (this.pendingGuaranteedEvents.length > 0 &&
547
+ this.pendingGuaranteedEvents[0].absoluteSequenceNumber ===
548
+ this.nextRecvEventSeq) {
549
+ const queued = this.pendingGuaranteedEvents.shift();
550
+ if (!queued)
551
+ break;
552
+ this.nextRecvEventSeq = (this.nextRecvEventSeq + 1) >>> 0;
553
+ dispatchedEvents.push(queued.event);
554
+ if (queued.event.parsedData) {
555
+ this.applyEventSideEffects(queued.event.parsedData);
556
+ }
557
+ }
558
+ }
559
+ /**
560
+ * Apply event side effects that alter ghost index state.
561
+ * These effects occur before ghostReadPacket on the same packet.
562
+ */
563
+ applyEventSideEffects(parsedData) {
564
+ const eventType = parsedData.type;
565
+ if (eventType === "GhostingMessageEvent") {
566
+ const message = parsedData.message;
567
+ // Tribes 2 handleGhostMessage: EndGhosting clears all local ghosts
568
+ // and invalidates all datablocks (they get re-sent for the new mission).
569
+ if (typeof message === "number" && message === GhostMsgEndGhosting) {
570
+ this.ghostTracker.clear();
571
+ this.dataBlockDataMap?.clear();
572
+ }
573
+ return;
574
+ }
575
+ if (eventType === "GhostAlwaysObjectEvent") {
576
+ const ghostIndex = parsedData.ghostIndex;
577
+ const classId = parsedData.classId;
578
+ if (typeof ghostIndex === "number" && typeof classId === "number") {
579
+ const parserEntry = this.registry.getGhostParser(classId);
580
+ this.ghostTracker.createGhost(ghostIndex, classId, parserEntry?.name ?? `unknown_${classId}`);
581
+ }
582
+ }
583
+ // SimDataBlockEvent: store parsed DataBlock data so ghost parsers
584
+ // (e.g., WheeledVehicle wheel count) can look it up by objectId.
585
+ if (eventType === "SimDataBlockEvent" && this.dataBlockDataMap) {
586
+ const dbEvent = parsedData;
587
+ if (dbEvent.dataBlockData &&
588
+ typeof dbEvent.objectId === "number") {
589
+ this.dataBlockDataMap.set(dbEvent.objectId, dbEvent.dataBlockData);
590
+ }
591
+ }
592
+ }
593
+ /**
594
+ * Parse ghosts from ghostReadPacket in netGhost.cc.
595
+ */
596
+ readGhosts(bs, seqNumber) {
597
+ const ghosts = [];
598
+ if (!bs.readFlag()) {
599
+ return ghosts;
600
+ }
601
+ const idSize = bs.readInt(3) + 3;
602
+ while (bs.readFlag()) {
603
+ // If the stream ran out of data during the moreFlag read, the
604
+ // readFlag returned false (terminating the loop) with error set.
605
+ // Check here for the case where readFlag returned true but the
606
+ // subsequent header reads exhaust the stream.
607
+ if (bs.isError())
608
+ break;
609
+ const index = bs.readInt(idSize);
610
+ // If the stream ran out of data during header reads, stop processing.
611
+ // Without this check, exhausted reads return 0 (defaulting index to 0,
612
+ // classId to 0) and the ghost parser runs on empty data, producing
613
+ // a "successful" parse that consumed 0 bits.
614
+ if (bs.isError())
615
+ break;
616
+ if (bs.readFlag()) {
617
+ // Ghost is being deleted
618
+ this.ghostTracker.deleteGhost(index);
619
+ this.ghostDeletes++;
620
+ ghosts.push({
621
+ index,
622
+ type: "delete",
623
+ updateBitsStart: bs.getCurPos(),
624
+ updateBitsEnd: bs.getCurPos(),
625
+ });
626
+ continue;
627
+ }
628
+ const isNew = !this.ghostTracker.hasGhost(index);
629
+ let classId;
630
+ if (isNew) {
631
+ classId = bs.readInt(NetObjectClassBitSize) + NetObjectClassFirst;
632
+ }
633
+ else {
634
+ classId = this.ghostTracker.getGhost(index)?.classId;
635
+ }
636
+ const updateBitsStart = bs.getCurPos();
637
+ const parserEntry = classId !== undefined
638
+ ? this.registry.getGhostParser(classId)
639
+ : undefined;
640
+ // Detect tracker divergence: we think this is a new ghost (not in our
641
+ // tracker) but the classId is unregistered. This almost certainly means
642
+ // the server sent UPDATE data (no classId prefix) for a ghost our tracker
643
+ // lost, and we misread the first 7 bits of update data as a classId.
644
+ // This is engine-consistent — the real client would also fail here
645
+ // because classId lookup would return NULL, triggering packet drop.
646
+ if (isNew && !parserEntry) {
647
+ this.ghostsTrackerDiverged++;
648
+ debugGhosts("DIVERGED pkt=%d seq=%d idx=%d classId=%d bit=%d/%d trackerSize=%d " +
649
+ "(server sent UPDATE for ghost not in our tracker; 7-bit classId is actually update data)", this.packetsParsed, seqNumber, index, classId, updateBitsStart, bs.getMaxPos(), this.ghostTracker.size());
650
+ ghosts.push({
651
+ index,
652
+ type: "create",
653
+ classId,
654
+ updateBitsStart,
655
+ updateBitsEnd: updateBitsStart,
656
+ });
657
+ return ghosts;
658
+ }
659
+ let parsed = false;
660
+ if (parserEntry) {
661
+ try {
662
+ const conn = this.getConnectionContext();
663
+ conn.currentGhostIndex = index;
664
+ const parsedData = parserEntry.unpackUpdate(bs, isNew, conn);
665
+ const endPos = bs.getCurPos();
666
+ if (isNew && classId !== undefined) {
667
+ this.ghostTracker.createGhost(index, classId, parserEntry.name);
668
+ this.ghostCreatesParsed++;
669
+ }
670
+ else {
671
+ this.ghostUpdatesParsed++;
672
+ }
673
+ ghosts.push({
674
+ index,
675
+ type: isNew ? "create" : "update",
676
+ classId,
677
+ updateBitsStart,
678
+ updateBitsEnd: endPos,
679
+ parsedData,
680
+ });
681
+ parsed = true;
682
+ }
683
+ catch (e) {
684
+ this.ghostsFailed++;
685
+ const op = isNew ? "create" : "update";
686
+ const message = e instanceof Error ? e.message : String(e);
687
+ debugGhosts("FAIL pkt=%d seq=%d #%d idx=%d op=%s classId=%d parser=%s bit=%d/%d trackerSize=%d err=%s", this.packetsParsed, seqNumber, ghosts.length, index, op, classId, parserEntry.name, updateBitsStart, bs.getMaxPos(), this.ghostTracker.size(), message);
688
+ }
689
+ }
690
+ if (parsed)
691
+ continue;
692
+ debugGhosts("STOP pkt=%d seq=%d idx=%d op=%s classId=%d parser=%s bit=%d/%d", this.packetsParsed, seqNumber, index, isNew ? "create" : "update", classId, parserEntry?.name ?? "NONE", updateBitsStart, bs.getMaxPos());
693
+ // Record and stop — can't parse this ghost's data.
694
+ ghosts.push({
695
+ index,
696
+ type: isNew ? "create" : "update",
697
+ classId,
698
+ updateBitsStart,
699
+ updateBitsEnd: updateBitsStart,
700
+ });
701
+ return ghosts;
702
+ }
703
+ return ghosts;
704
+ }
705
+ emptyGameState() {
706
+ return {
707
+ lastMoveAck: 0,
708
+ pinged: false,
709
+ jammed: false,
710
+ };
711
+ }
712
+ }
713
+ //# sourceMappingURL=PacketParser.js.map