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.
- package/LICENSE +21 -0
- package/README.md +823 -0
- package/dist/BitStream.d.ts +143 -0
- package/dist/BitStream.d.ts.map +1 -0
- package/dist/BitStream.js +369 -0
- package/dist/BitStream.js.map +1 -0
- package/dist/BitStream.test.d.ts +2 -0
- package/dist/BitStream.test.d.ts.map +1 -0
- package/dist/BitStream.test.js +419 -0
- package/dist/BitStream.test.js.map +1 -0
- package/dist/ClassRegistry.d.ts +100 -0
- package/dist/ClassRegistry.d.ts.map +1 -0
- package/dist/ClassRegistry.js +118 -0
- package/dist/ClassRegistry.js.map +1 -0
- package/dist/ClassRegistry.test.d.ts +2 -0
- package/dist/ClassRegistry.test.d.ts.map +1 -0
- package/dist/ClassRegistry.test.js +83 -0
- package/dist/ClassRegistry.test.js.map +1 -0
- package/dist/DataBlockParsers.d.ts +3 -0
- package/dist/DataBlockParsers.d.ts.map +1 -0
- package/dist/DataBlockParsers.js +2041 -0
- package/dist/DataBlockParsers.js.map +1 -0
- package/dist/DemoParser.d.ts +155 -0
- package/dist/DemoParser.d.ts.map +1 -0
- package/dist/DemoParser.js +943 -0
- package/dist/DemoParser.js.map +1 -0
- package/dist/DemoParser.test.d.ts +2 -0
- package/dist/DemoParser.test.d.ts.map +1 -0
- package/dist/DemoParser.test.js +472 -0
- package/dist/DemoParser.test.js.map +1 -0
- package/dist/EventParsers.d.ts +3 -0
- package/dist/EventParsers.d.ts.map +1 -0
- package/dist/EventParsers.js +603 -0
- package/dist/EventParsers.js.map +1 -0
- package/dist/GhostManager.d.ts +13 -0
- package/dist/GhostManager.d.ts.map +1 -0
- package/dist/GhostManager.js +2002 -0
- package/dist/GhostManager.js.map +1 -0
- package/dist/GhostTracker.test.d.ts +2 -0
- package/dist/GhostTracker.test.d.ts.map +1 -0
- package/dist/GhostTracker.test.js +64 -0
- package/dist/GhostTracker.test.js.map +1 -0
- package/dist/HuffmanProcessor.d.ts +12 -0
- package/dist/HuffmanProcessor.d.ts.map +1 -0
- package/dist/HuffmanProcessor.js +163 -0
- package/dist/HuffmanProcessor.js.map +1 -0
- package/dist/LiveParser.d.ts +16 -0
- package/dist/LiveParser.d.ts.map +1 -0
- package/dist/LiveParser.js +29 -0
- package/dist/LiveParser.js.map +1 -0
- package/dist/PacketParser.d.ts +104 -0
- package/dist/PacketParser.d.ts.map +1 -0
- package/dist/PacketParser.js +713 -0
- package/dist/PacketParser.js.map +1 -0
- package/dist/Timeline.d.ts +101 -0
- package/dist/Timeline.d.ts.map +1 -0
- package/dist/Timeline.js +251 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +372 -0
- package/dist/cli.js.map +1 -0
- package/dist/dataBlockDataTypes.d.ts +877 -0
- package/dist/dataBlockDataTypes.d.ts.map +1 -0
- package/dist/dataBlockDataTypes.js +2 -0
- package/dist/dataBlockDataTypes.js.map +1 -0
- package/dist/dataTypes.d.ts +31 -0
- package/dist/dataTypes.d.ts.map +1 -0
- package/dist/dataTypes.js +5 -0
- package/dist/dataTypes.js.map +1 -0
- package/dist/dump.d.ts +3 -0
- package/dist/dump.d.ts.map +1 -0
- package/dist/dump.js +36 -0
- package/dist/dump.js.map +1 -0
- package/dist/eventDataTypes.d.ts +199 -0
- package/dist/eventDataTypes.d.ts.map +1 -0
- package/dist/eventDataTypes.js +2 -0
- package/dist/eventDataTypes.js.map +1 -0
- package/dist/ghostDataTypes.d.ts +573 -0
- package/dist/ghostDataTypes.d.ts.map +1 -0
- package/dist/ghostDataTypes.js +2 -0
- package/dist/ghostDataTypes.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +261 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +204 -0
- package/dist/types.js.map +1 -0
- 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
|