t2-demo-parser 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -87
- package/dist/DemoParser.d.ts.map +1 -1
- package/dist/DemoParser.js +43 -8
- package/dist/DemoParser.js.map +1 -1
- package/dist/GhostManager.d.ts.map +1 -1
- package/dist/GhostManager.js +36 -68
- package/dist/GhostManager.js.map +1 -1
- package/dist/GhostStateAccumulator.d.ts +36 -0
- package/dist/GhostStateAccumulator.d.ts.map +1 -0
- package/dist/GhostStateAccumulator.js +124 -0
- package/dist/GhostStateAccumulator.js.map +1 -0
- package/dist/GhostStateAccumulator.test.d.ts +2 -0
- package/dist/GhostStateAccumulator.test.d.ts.map +1 -0
- package/dist/GhostStateAccumulator.test.js +201 -0
- package/dist/GhostStateAccumulator.test.js.map +1 -0
- package/dist/LiveParser.d.ts +39 -1
- package/dist/LiveParser.d.ts.map +1 -1
- package/dist/LiveParser.js +43 -1
- package/dist/LiveParser.js.map +1 -1
- package/dist/PacketParser.d.ts +23 -2
- package/dist/PacketParser.d.ts.map +1 -1
- package/dist/PacketParser.js +62 -19
- package/dist/PacketParser.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# t2-demo-parser
|
|
2
2
|
|
|
3
|
-
Parser for Tribes 2 demo recordings (`.rec` files)
|
|
4
|
-
|
|
5
|
-
recordings made by the Tribes 2
|
|
3
|
+
Parser for Tribes 2 demo recordings (`.rec` files) and live server packet
|
|
4
|
+
streams. Extracts game state, player movement, ghost object lifecycles,
|
|
5
|
+
network events, and animation timelines from recordings made by the Tribes 2
|
|
6
|
+
client (build 25034, Torque engine), and parses the same packet format
|
|
7
|
+
arriving from a live connection (see `createLiveParser`).
|
|
6
8
|
|
|
7
9
|
Designed for use in browser-based replay viewers. The async API keeps the main
|
|
8
10
|
thread responsive: in browsers, decompression runs in a Web Worker via
|
|
@@ -18,14 +20,14 @@ const buffer = new Uint8Array(/* .rec file contents */);
|
|
|
18
20
|
const parser = new DemoParser(buffer);
|
|
19
21
|
const demo = await parser.parseFullDemo();
|
|
20
22
|
|
|
21
|
-
console.log(demo.header.demoLengthMs);
|
|
22
|
-
console.log(demo.initialBlock.missionName);
|
|
23
|
-
console.log(demo.blocks.length);
|
|
24
|
-
console.log(demo.initialBlock.dataBlocks.size);
|
|
23
|
+
console.log(demo.header.demoLengthMs); // Duration in ms
|
|
24
|
+
console.log(demo.initialBlock.missionName); // e.g. "Rollercoaster"
|
|
25
|
+
console.log(demo.blocks.length); // Total block count
|
|
26
|
+
console.log(demo.initialBlock.dataBlocks.size); // DataBlock definitions
|
|
25
27
|
|
|
26
28
|
const timeline = buildTimeline(demo, parser.getRegistry());
|
|
27
|
-
console.log(timeline.controlObject.length);
|
|
28
|
-
console.log(timeline.ghostInstances.length);
|
|
29
|
+
console.log(timeline.controlObject.length); // Player position keyframes
|
|
30
|
+
console.log(timeline.ghostInstances.length); // Networked object lifecycles
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
## CLI
|
|
@@ -124,21 +126,21 @@ interface DemoFile {
|
|
|
124
126
|
|
|
125
127
|
#### Properties
|
|
126
128
|
|
|
127
|
-
| Property
|
|
128
|
-
|
|
129
|
-
| `loaded`
|
|
130
|
-
| `header`
|
|
131
|
-
| `initialBlock` | `InitialBlockData` | Initial game state (throws if not loaded).
|
|
132
|
-
| `blockCount`
|
|
133
|
-
| `blockCursor`
|
|
129
|
+
| Property | Type | Description |
|
|
130
|
+
| -------------- | ------------------ | ------------------------------------------------------------------------------------------------ |
|
|
131
|
+
| `loaded` | `boolean` | Whether `load()` has been called. |
|
|
132
|
+
| `header` | `DemoHeader` | File header (throws if not loaded). |
|
|
133
|
+
| `initialBlock` | `InitialBlockData` | Initial game state (throws if not loaded). |
|
|
134
|
+
| `blockCount` | `number` | Total blocks in the stream. Lazily computed on first access by scanning the decompressed buffer. |
|
|
135
|
+
| `blockCursor` | `number` | Number of blocks consumed so far. |
|
|
134
136
|
|
|
135
137
|
#### Accessors
|
|
136
138
|
|
|
137
|
-
| Method
|
|
138
|
-
|
|
139
|
-
| `getRegistry()`
|
|
140
|
-
| `getGhostTracker()` | `GhostTracker`
|
|
141
|
-
| `getPacketParser()` | `PacketParser`
|
|
139
|
+
| Method | Returns | Description |
|
|
140
|
+
| ------------------- | --------------- | ----------------------------------------------- |
|
|
141
|
+
| `getRegistry()` | `ClassRegistry` | Parser registry with all bindings. |
|
|
142
|
+
| `getGhostTracker()` | `GhostTracker` | Current ghost state (mutated by `nextBlock()`). |
|
|
143
|
+
| `getPacketParser()` | `PacketParser` | Packet parser with parse statistics. |
|
|
142
144
|
|
|
143
145
|
---
|
|
144
146
|
|
|
@@ -146,10 +148,10 @@ interface DemoFile {
|
|
|
146
148
|
|
|
147
149
|
```typescript
|
|
148
150
|
interface DemoHeader {
|
|
149
|
-
identString: string;
|
|
150
|
-
protocolVersion: number;
|
|
151
|
-
demoLengthMs: number;
|
|
152
|
-
initialBlockSize: number;
|
|
151
|
+
identString: string; // "Tribes2 Recording"
|
|
152
|
+
protocolVersion: number; // 0x330004
|
|
153
|
+
demoLengthMs: number; // Total recording duration in milliseconds
|
|
154
|
+
initialBlockSize: number; // Byte size of the initial block
|
|
153
155
|
}
|
|
154
156
|
```
|
|
155
157
|
|
|
@@ -162,7 +164,7 @@ Snapshot of the game state at the moment recording started.
|
|
|
162
164
|
```typescript
|
|
163
165
|
interface InitialBlockData {
|
|
164
166
|
// DataBlocks (static game definitions)
|
|
165
|
-
dataBlocks: Map<number, ParsedDataBlock>;
|
|
167
|
+
dataBlocks: Map<number, ParsedDataBlock>; // objectId → parsed DataBlock
|
|
166
168
|
dataBlockCount: number;
|
|
167
169
|
dataBlockHeaders: DataBlockHeader[];
|
|
168
170
|
|
|
@@ -181,8 +183,11 @@ interface InitialBlockData {
|
|
|
181
183
|
initialEvents: NetEventInfo[];
|
|
182
184
|
|
|
183
185
|
// Control object (the recording player)
|
|
184
|
-
controlObjectGhostIndex: number;
|
|
186
|
+
controlObjectGhostIndex: number; // -1 if none
|
|
185
187
|
controlObjectData?: Record<string, unknown>;
|
|
188
|
+
// Compression point established by the control object's state (its
|
|
189
|
+
// position), seeding compressed-point decodes in the first packets.
|
|
190
|
+
initialCompressionPoint?: { x: number; y: number; z: number };
|
|
186
191
|
firstPerson: boolean;
|
|
187
192
|
|
|
188
193
|
// Mission info
|
|
@@ -215,10 +220,10 @@ A single block from the compressed block stream.
|
|
|
215
220
|
```typescript
|
|
216
221
|
interface DemoBlock {
|
|
217
222
|
index: number;
|
|
218
|
-
type: number;
|
|
219
|
-
|
|
220
|
-
size: number;
|
|
221
|
-
data: Uint8Array;
|
|
223
|
+
type: number; // BlockTypePacket (0), BlockTypeSendPacket (1),
|
|
224
|
+
// BlockTypeMove (2), or BlockTypeInfo (3)
|
|
225
|
+
size: number; // Payload size in bytes
|
|
226
|
+
data: Uint8Array; // Raw payload
|
|
222
227
|
parsed?: PacketData | Move | InfoBlock;
|
|
223
228
|
}
|
|
224
229
|
```
|
|
@@ -251,6 +256,8 @@ interface GameState {
|
|
|
251
256
|
pinged: boolean;
|
|
252
257
|
jammed: boolean;
|
|
253
258
|
controlObjectGhostIndex?: number;
|
|
259
|
+
controlObjectDataStart?: number; // Bit offsets of the control object
|
|
260
|
+
controlObjectDataEnd?: number; // update within the packet
|
|
254
261
|
controlObjectData?: Record<string, unknown>;
|
|
255
262
|
compressionPoint?: { x: number; y: number; z: number };
|
|
256
263
|
cameraFov?: number;
|
|
@@ -273,12 +280,12 @@ A create, update, or delete operation on a ghost object.
|
|
|
273
280
|
|
|
274
281
|
```typescript
|
|
275
282
|
interface GhostUpdate {
|
|
276
|
-
index: number;
|
|
283
|
+
index: number; // Ghost slot (0–1023)
|
|
277
284
|
type: "create" | "update" | "delete";
|
|
278
|
-
classId?: number;
|
|
285
|
+
classId?: number; // Set on create
|
|
279
286
|
updateBitsStart: number;
|
|
280
287
|
updateBitsEnd: number;
|
|
281
|
-
parsedData?: Record<string, unknown>;
|
|
288
|
+
parsedData?: Record<string, unknown>; // Class-specific parsed fields
|
|
282
289
|
}
|
|
283
290
|
```
|
|
284
291
|
|
|
@@ -290,10 +297,13 @@ A network event received from the server.
|
|
|
290
297
|
interface NetEventInfo {
|
|
291
298
|
classId: number;
|
|
292
299
|
guaranteed: boolean;
|
|
293
|
-
sequenceNumber?: number;
|
|
300
|
+
sequenceNumber?: number; // 7-bit wire sequence (guaranteed events)
|
|
301
|
+
absoluteSequenceNumber?: number; // Unwrapped full sequence
|
|
294
302
|
dataBitsStart: number;
|
|
295
303
|
dataBitsEnd: number;
|
|
296
304
|
parsedData?: Record<string, unknown>;
|
|
305
|
+
failed?: boolean; // Event could not be parsed; stream
|
|
306
|
+
// position after it is unreliable
|
|
297
307
|
}
|
|
298
308
|
```
|
|
299
309
|
|
|
@@ -305,14 +315,24 @@ Raw 64-byte player input struct (from type 2 blocks).
|
|
|
305
315
|
|
|
306
316
|
```typescript
|
|
307
317
|
interface Move {
|
|
308
|
-
x: number;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
318
|
+
x: number;
|
|
319
|
+
y: number;
|
|
320
|
+
z: number; // Movement input per axis (float)
|
|
321
|
+
yaw: number;
|
|
322
|
+
pitch: number;
|
|
323
|
+
roll: number; // Rotation deltas in radians
|
|
324
|
+
// (server adds them each tick)
|
|
325
|
+
px: number;
|
|
326
|
+
py: number;
|
|
327
|
+
pz: number; // Packed integer forms of x/y/z
|
|
328
|
+
pyaw: number;
|
|
329
|
+
ppitch: number;
|
|
330
|
+
proll: number; // Packed rotation: fractional
|
|
331
|
+
// turns × 65536 (16-bit range)
|
|
312
332
|
id: number;
|
|
313
333
|
sendCount: number;
|
|
314
334
|
freeLook: boolean;
|
|
315
|
-
trigger: boolean[];
|
|
335
|
+
trigger: boolean[]; // 6 trigger keys (fire, jet, jump, etc.)
|
|
316
336
|
}
|
|
317
337
|
```
|
|
318
338
|
|
|
@@ -325,9 +345,9 @@ A static game object definition parsed from the initial block.
|
|
|
325
345
|
```typescript
|
|
326
346
|
interface ParsedDataBlock {
|
|
327
347
|
classId: number;
|
|
328
|
-
className: string;
|
|
348
|
+
className: string; // e.g. "PlayerData", "WheeledVehicleData"
|
|
329
349
|
objectId: number;
|
|
330
|
-
data: Record<string, unknown>;
|
|
350
|
+
data: Record<string, unknown>; // Class-specific fields (shapeName, etc.)
|
|
331
351
|
}
|
|
332
352
|
```
|
|
333
353
|
|
|
@@ -375,7 +395,11 @@ with position data are included.
|
|
|
375
395
|
interface ExportTimeline {
|
|
376
396
|
durationMs: number;
|
|
377
397
|
tickIntervalMs: number;
|
|
378
|
-
controlObject: {
|
|
398
|
+
controlObject: {
|
|
399
|
+
t: number;
|
|
400
|
+
p?: [number, number, number];
|
|
401
|
+
v?: [number, number, number];
|
|
402
|
+
}[];
|
|
379
403
|
ghosts: ExportGhostInstance[];
|
|
380
404
|
events: GameEvent[];
|
|
381
405
|
}
|
|
@@ -439,22 +463,40 @@ interface ControlObjectKeyframe {
|
|
|
439
463
|
|
|
440
464
|
Available via `parser.getPacketParser()`. Exposes parse statistics.
|
|
441
465
|
|
|
442
|
-
| Property
|
|
443
|
-
|
|
444
|
-
| `packetsParsed`
|
|
445
|
-
| `ghostCreatesParsed`
|
|
446
|
-
| `ghostUpdatesParsed`
|
|
447
|
-
| `ghostDeletes`
|
|
448
|
-
| `ghostsFailed`
|
|
449
|
-
| `ghostsTrackerDiverged` | `number` | Ghost tracker inconsistencies detected.
|
|
450
|
-
| `eventsParsed`
|
|
451
|
-
| `eventsFailed`
|
|
452
|
-
| `controlObjectParsed`
|
|
453
|
-
| `controlObjectFailed`
|
|
466
|
+
| Property | Type | Description |
|
|
467
|
+
| ----------------------- | -------- | --------------------------------------------------------------- |
|
|
468
|
+
| `packetsParsed` | `number` | Total packets successfully parsed. |
|
|
469
|
+
| `ghostCreatesParsed` | `number` | Ghost create operations parsed. |
|
|
470
|
+
| `ghostUpdatesParsed` | `number` | Ghost update operations parsed. |
|
|
471
|
+
| `ghostDeletes` | `number` | Ghost delete operations. |
|
|
472
|
+
| `ghostsFailed` | `number` | Ghost operations that failed to parse. |
|
|
473
|
+
| `ghostsTrackerDiverged` | `number` | Ghost tracker inconsistencies detected. |
|
|
474
|
+
| `eventsParsed` | `number` | Events parsed. |
|
|
475
|
+
| `eventsFailed` | `number` | Events that failed to parse. |
|
|
476
|
+
| `controlObjectParsed` | `number` | Control object updates parsed. |
|
|
477
|
+
| `controlObjectFailed` | `number` | Control object updates that failed. |
|
|
478
|
+
| `protocolRejected` | `number` | Packets rejected by the dnet protocol window. |
|
|
479
|
+
| `protocolNoDispatch` | `number` | Packets accepted but not dispatched (duplicates/out-of-window). |
|
|
480
|
+
|
|
481
|
+
#### State export (for seeding another parser)
|
|
482
|
+
|
|
483
|
+
These getters capture **all cross-packet parser state**, so a second parser
|
|
484
|
+
seeded with the same values (plus the ghost tracker contents and datablock
|
|
485
|
+
map) continues the stream in bit-lockstep with this one — the basis for
|
|
486
|
+
late-joiner catch-up in live streaming.
|
|
487
|
+
|
|
488
|
+
| Method | Returns |
|
|
489
|
+
| ----------------------------------- | ------------------------------------------------ |
|
|
490
|
+
| `getConnectionProtocolState()` | `ConnectionProtocolState` (dnet sequence window) |
|
|
491
|
+
| `getNextRecvEventSeq()` | `number` |
|
|
492
|
+
| `getPendingGuaranteedEvents()` | Out-of-order guaranteed events awaiting dispatch |
|
|
493
|
+
| `getCompressionPoint()` | `{ x, y, z }` |
|
|
494
|
+
| `getDataBlockDataMap()` | `Map<number, ParsedData> \| undefined` |
|
|
495
|
+
| `setConnectionProtocolState(state)` | — (also a constructor option) |
|
|
454
496
|
|
|
455
497
|
---
|
|
456
498
|
|
|
457
|
-
### `createLiveParser(): LiveParserKit`
|
|
499
|
+
### `createLiveParser(seed?): LiveParserKit`
|
|
458
500
|
|
|
459
501
|
Create a parser stack for live server connections. Sets up the same deterministic
|
|
460
502
|
registry bindings as `DemoParser` but without requiring a `.rec` file. Useful for
|
|
@@ -475,6 +517,69 @@ interface LiveParserKit {
|
|
|
475
517
|
}
|
|
476
518
|
```
|
|
477
519
|
|
|
520
|
+
With a **seed**, the stack resumes an in-progress stream from another
|
|
521
|
+
parser's exported state, continuing at a packet boundary in bit-lockstep
|
|
522
|
+
with the exporter (the late-joiner catch-up scenario — see
|
|
523
|
+
`GhostStateAccumulator` below for producing the ghost seeds):
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
interface LiveParserSeed {
|
|
527
|
+
dataBlocks?: Iterable<[number, ParsedData]>; // objectId → parsed datablock
|
|
528
|
+
ghosts?: Iterable<{ index: number; classId: number }>;
|
|
529
|
+
connectionProtocolState?: ConnectionProtocolState;
|
|
530
|
+
nextRecvEventSeq?: number;
|
|
531
|
+
compressionPoint?: { x: number; y: number; z: number };
|
|
532
|
+
pendingGuaranteedEvents?: Array<{
|
|
533
|
+
absoluteSequenceNumber: number;
|
|
534
|
+
event: NetEventInfo;
|
|
535
|
+
}>;
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### `passiveObserverProtocolState(firstPacketByte): ConnectionProtocolState`
|
|
540
|
+
|
|
541
|
+
Protocol state for a parser that passively observes the server→client
|
|
542
|
+
stream while something else (e.g. a relay) owns the client→server side.
|
|
543
|
+
Sets `lastSendSeq` high so ack validation never rejects packets that ack
|
|
544
|
+
sequences the observer didn't send. Intended for the first packets of a
|
|
545
|
+
connection; to attach mid-stream, seed `connectionProtocolState` from the
|
|
546
|
+
exporting parser instead.
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
import { createLiveParser, passiveObserverProtocolState } from "t2-demo-parser";
|
|
550
|
+
|
|
551
|
+
const { packetParser } = createLiveParser();
|
|
552
|
+
// On the first received packet:
|
|
553
|
+
packetParser.setConnectionProtocolState(passiveObserverProtocolState(data[0]));
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
### `GhostStateAccumulator`
|
|
559
|
+
|
|
560
|
+
Maintains one merged full `parsedData` per live ghost by folding each
|
|
561
|
+
packet's creates/updates/deletes (plus `GhostAlwaysObjectEvent` creates and
|
|
562
|
+
EndGhosting clears). `toInitialGhosts()` yields entries shaped like a demo
|
|
563
|
+
recording's `initialGhosts` — the full-state ghost list a `.rec` starts
|
|
564
|
+
with when recorded mid-match — for hydrating a late joiner.
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
import { GhostStateAccumulator, mergeGhostParsedData } from "t2-demo-parser";
|
|
568
|
+
|
|
569
|
+
const accumulator = new GhostStateAccumulator();
|
|
570
|
+
// After each parsed packet:
|
|
571
|
+
accumulator.applyPacket(packetData);
|
|
572
|
+
|
|
573
|
+
accumulator.toInitialGhosts(); // GhostUpdate[] with full merged parsedData
|
|
574
|
+
accumulator.getGhostSeeds(); // { index, classId }[] for createLiveParser
|
|
575
|
+
accumulator.size();
|
|
576
|
+
accumulator.clear();
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
`mergeGhostParsedData(base, update)` is the underlying merge rule: arrays
|
|
580
|
+
whose entries carry a numeric `index` (threads, images, sounds) merge
|
|
581
|
+
sparsely by index; other values are last-write-wins.
|
|
582
|
+
|
|
478
583
|
---
|
|
479
584
|
|
|
480
585
|
### `BitStream`
|
|
@@ -500,9 +605,9 @@ Tracks the live state of all ghost objects. Available via
|
|
|
500
605
|
|
|
501
606
|
```typescript
|
|
502
607
|
const tracker = parser.getGhostTracker();
|
|
503
|
-
const ghost = tracker.getGhost(index);
|
|
504
|
-
const all = tracker.getAllGhosts();
|
|
505
|
-
tracker.size();
|
|
608
|
+
const ghost = tracker.getGhost(index); // GhostEntry | undefined
|
|
609
|
+
const all = tracker.getAllGhosts(); // Map<number, GhostEntry>
|
|
610
|
+
tracker.size(); // Number of active ghosts
|
|
506
611
|
```
|
|
507
612
|
|
|
508
613
|
```typescript
|
|
@@ -521,10 +626,10 @@ Block type constants for filtering `DemoBlock.type`:
|
|
|
521
626
|
|
|
522
627
|
```typescript
|
|
523
628
|
import {
|
|
524
|
-
BlockTypePacket,
|
|
525
|
-
BlockTypeSendPacket,
|
|
526
|
-
BlockTypeMove,
|
|
527
|
-
BlockTypeInfo,
|
|
629
|
+
BlockTypePacket, // 0 — network packet
|
|
630
|
+
BlockTypeSendPacket, // 1 — send-packet trigger (no data)
|
|
631
|
+
BlockTypeMove, // 2 — 64-byte player input
|
|
632
|
+
BlockTypeInfo, // 3 — 8-byte timing/FOV
|
|
528
633
|
} from "t2-demo-parser";
|
|
529
634
|
```
|
|
530
635
|
|
|
@@ -532,20 +637,22 @@ Network protocol constants are also exported:
|
|
|
532
637
|
|
|
533
638
|
```typescript
|
|
534
639
|
import {
|
|
535
|
-
MaxGhostCount,
|
|
536
|
-
GhostIdBitSize,
|
|
640
|
+
MaxGhostCount, // 1024
|
|
641
|
+
GhostIdBitSize, // 10
|
|
537
642
|
NetStringTableMaxStrings, // 4096
|
|
538
|
-
StringIdBitSize,
|
|
539
|
-
NetEventClassBitSize,
|
|
540
|
-
NetEventClassFirst,
|
|
541
|
-
NetObjectClassBitSize,
|
|
542
|
-
NetObjectClassFirst,
|
|
543
|
-
MaxPacketDataSize,
|
|
544
|
-
MaxTriggerKeys,
|
|
545
|
-
|
|
643
|
+
StringIdBitSize, // 12
|
|
644
|
+
NetEventClassBitSize, // 6
|
|
645
|
+
NetEventClassFirst, // 255
|
|
646
|
+
NetObjectClassBitSize, // 7
|
|
647
|
+
NetObjectClassFirst, // 0
|
|
648
|
+
MaxPacketDataSize, // 1500
|
|
649
|
+
MaxTriggerKeys, // 6
|
|
650
|
+
MoveCountBits, // 5
|
|
651
|
+
MaxMoveCount, // 30
|
|
652
|
+
DataBlockObjectIdFirst, // 3
|
|
546
653
|
DataBlockObjectIdBitSize, // 10
|
|
547
|
-
DataBlockClassFirst,
|
|
548
|
-
DataBlockClassBitSize,
|
|
654
|
+
DataBlockClassFirst, // 128
|
|
655
|
+
DataBlockClassBitSize, // 7
|
|
549
656
|
} from "t2-demo-parser";
|
|
550
657
|
```
|
|
551
658
|
|
|
@@ -554,9 +661,9 @@ assignment order):
|
|
|
554
661
|
|
|
555
662
|
```typescript
|
|
556
663
|
import {
|
|
557
|
-
NetObjectClassNames,
|
|
558
|
-
DataBlockClassNames,
|
|
559
|
-
NetEventClassNames,
|
|
664
|
+
NetObjectClassNames, // 53 ghost class names
|
|
665
|
+
DataBlockClassNames, // 54 DataBlock class names
|
|
666
|
+
NetEventClassNames, // 26 event class names
|
|
560
667
|
} from "t2-demo-parser";
|
|
561
668
|
```
|
|
562
669
|
|
|
@@ -576,11 +683,14 @@ const { header, initialBlock } = await parser.load();
|
|
|
576
683
|
|
|
577
684
|
// Duration
|
|
578
685
|
const durationMs = header.demoLengthMs;
|
|
579
|
-
const durationStr = `${Math.floor(durationMs / 60000)}m${
|
|
580
|
-
|
|
686
|
+
const durationStr = `${Math.floor(durationMs / 60000)}m${Math.floor(
|
|
687
|
+
(durationMs % 60000) / 1000,
|
|
688
|
+
)
|
|
689
|
+
.toString()
|
|
690
|
+
.padStart(2, "0")}s`;
|
|
581
691
|
|
|
582
692
|
// Mission / map name
|
|
583
|
-
const mission = initialBlock.missionName;
|
|
693
|
+
const mission = initialBlock.missionName; // e.g. "Rollercoaster"
|
|
584
694
|
|
|
585
695
|
// Game mode / mission type — look in DemoValues
|
|
586
696
|
// $DemoValue_0 is typically the game mode (e.g. "CTFGame")
|
|
@@ -588,7 +698,9 @@ const gameMode = initialBlock.demoValues[0];
|
|
|
588
698
|
|
|
589
699
|
// Teams and players
|
|
590
700
|
for (const score of initialBlock.scoreEntries) {
|
|
591
|
-
console.log(
|
|
701
|
+
console.log(
|
|
702
|
+
`Team ${score.teamId}: client ${score.clientId}, score ${score.score}`,
|
|
703
|
+
);
|
|
592
704
|
}
|
|
593
705
|
|
|
594
706
|
// Player names from the target table
|
|
@@ -650,10 +762,10 @@ function seekTo(targetMs: number) {
|
|
|
650
762
|
if (block.type === BlockTypeMove) moveCount++;
|
|
651
763
|
if (moveCount * 32 >= targetMs) break;
|
|
652
764
|
}
|
|
653
|
-
return moveCount * 32;
|
|
765
|
+
return moveCount * 32; // Actual time reached
|
|
654
766
|
}
|
|
655
767
|
|
|
656
|
-
const actualMs = seekTo(60000);
|
|
768
|
+
const actualMs = seekTo(60000); // Seek to ~1 minute
|
|
657
769
|
// Ghost tracker now reflects state at that point.
|
|
658
770
|
// Continue calling nextBlock() to play forward from here.
|
|
659
771
|
```
|
|
@@ -680,7 +792,9 @@ console.log(`${stats.ghostsWithPosition} with position data`);
|
|
|
680
792
|
|
|
681
793
|
// Iterate ghost lifecycles
|
|
682
794
|
for (const inst of timeline.ghostInstances) {
|
|
683
|
-
console.log(
|
|
795
|
+
console.log(
|
|
796
|
+
`${inst.className} #${inst.ghostIndex}: ${inst.keyframes.length} keyframes`,
|
|
797
|
+
);
|
|
684
798
|
for (const kf of inst.keyframes) {
|
|
685
799
|
if (kf.position) {
|
|
686
800
|
// Feed into Three.js KeyframeTrack...
|
|
@@ -713,7 +827,7 @@ parser.reset();
|
|
|
713
827
|
while (parser.nextBlock()) {}
|
|
714
828
|
const stats2 = parser.getPacketParser().packetsParsed;
|
|
715
829
|
|
|
716
|
-
console.log(stats1 === stats2);
|
|
830
|
+
console.log(stats1 === stats2); // true
|
|
717
831
|
```
|
|
718
832
|
|
|
719
833
|
### Access parse statistics
|
|
@@ -747,7 +861,7 @@ const registry = parser.getRegistry();
|
|
|
747
861
|
// From a ghost update in a packet:
|
|
748
862
|
if (ghost.type === "create" && ghost.classId !== undefined) {
|
|
749
863
|
const entry = registry.getGhostParser(ghost.classId);
|
|
750
|
-
console.log(entry?.name);
|
|
864
|
+
console.log(entry?.name); // e.g. "Player", "Turret", "LinearProjectile"
|
|
751
865
|
}
|
|
752
866
|
|
|
753
867
|
// From the ghost tracker (live state):
|
package/dist/DemoParser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DemoParser.d.ts","sourceRoot":"","sources":["../src/DemoParser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAwB,MAAM,mBAAmB,CAAC;AAuBvE,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,SAAS,EACT,gBAAgB,EAWhB,UAAU,EACX,MAAM,YAAY,CAAC;AAMpB,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,IAAI,CAAW;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IAEnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAC,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAC,CAAmB;IACzC,OAAO,CAAC,iBAAiB,CAAC,CAAa;IACvC,OAAO,CAAC,iBAAiB,CAAC,CAAW;IACrC,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,EAAE,UAAU;
|
|
1
|
+
{"version":3,"file":"DemoParser.d.ts","sourceRoot":"","sources":["../src/DemoParser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAwB,MAAM,mBAAmB,CAAC;AAuBvE,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,SAAS,EACT,gBAAgB,EAWhB,UAAU,EACX,MAAM,YAAY,CAAC;AAMpB,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,IAAI,CAAW;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IAEnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAC,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAC,CAAmB;IACzC,OAAO,CAAC,iBAAiB,CAAC,CAAa;IACvC,OAAO,CAAC,iBAAiB,CAAC,CAAW;IACrC,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,EAAE,UAAU;IAyE9B,WAAW,IAAI,aAAa;IAI5B,eAAe,IAAI,YAAY;IAI/B,eAAe,IAAI,YAAY;IAM/B,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,MAAM,IAAI,UAAU,CAGvB;IAED,IAAI,YAAY,IAAI,gBAAgB,CAGnC;IAED,IAAI,UAAU,IAAI,MAAM,CAkBvB;IAED,IAAI,WAAW,IAAI,MAAM,CAGxB;IAID;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC;IA4DjC;;;;;OAKG;IACH,SAAS,IAAI,SAAS,GAAG,SAAS;IA2DlC;;;;;OAKG;IACH,KAAK,IAAI,IAAI;IAWb;;;;;OAKG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAUpC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA8BzB;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC;IAQxC,OAAO,CAAC,UAAU;IAwBlB;4DACwD;IACxD,OAAO,CAAC,gBAAgB;IA4TxB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAuBtB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAQtB;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IA6EhC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,eAAe;IAyBvB;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,mBAAmB;IAmE3B,OAAO,CAAC,mBAAmB;IAuH3B;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAsEtB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA4CjC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,WAAW;IA0CnB;;;OAGG;IACH,OAAO,CAAC,aAAa;CAOtB"}
|
package/dist/DemoParser.js
CHANGED
|
@@ -270,6 +270,7 @@ export class DemoParser {
|
|
|
270
270
|
dataBlockDataMap,
|
|
271
271
|
connectionProtocolState: initialBlock.connectionState,
|
|
272
272
|
nextRecvEventSeq: initialBlock.nextRecvEventSeq,
|
|
273
|
+
compressionPoint: initialBlock.initialCompressionPoint,
|
|
273
274
|
});
|
|
274
275
|
this.ghostTracker = gt;
|
|
275
276
|
this.packetParser = pp;
|
|
@@ -406,13 +407,15 @@ export class DemoParser {
|
|
|
406
407
|
let initialGhosts = [];
|
|
407
408
|
let controlObjectGhostIndex = -1;
|
|
408
409
|
let controlObjectData;
|
|
410
|
+
let initialCompressionPoint;
|
|
409
411
|
let missionName = "";
|
|
410
412
|
let missionCRC = 0;
|
|
411
413
|
let phase2Error;
|
|
412
414
|
try {
|
|
413
415
|
debugInitial("phase2 start bit=%d remaining=%d", bs.getCurPos(), totalBits - bs.getCurPos());
|
|
414
416
|
// B.10f Events
|
|
415
|
-
({ nextRecvEventSeq, events: initialEvents } =
|
|
417
|
+
({ nextRecvEventSeq, events: initialEvents } =
|
|
418
|
+
this.readEventStartBlock(bs));
|
|
416
419
|
debugInitial("after initial events bit=%d count=%d", bs.getCurPos(), initialEvents.length);
|
|
417
420
|
// B.10g Ghosts
|
|
418
421
|
const ghostResult = this.readGhostStartBlock(bs, dataBlocks);
|
|
@@ -428,11 +431,20 @@ export class DemoParser {
|
|
|
428
431
|
if (ghost) {
|
|
429
432
|
const parser = this.registry.getGhostParser(ghost.classId);
|
|
430
433
|
if (parser?.readPacketData) {
|
|
434
|
+
// getGhostParser enables the nested vehicle readPacketData
|
|
435
|
+
// when the recorder was piloting at recording start — without
|
|
436
|
+
// it those bytes go unread and every later read in the
|
|
437
|
+
// initial block (mission name, CRC) is misaligned.
|
|
431
438
|
const conn = {
|
|
432
439
|
compressionPoint: { x: 0, y: 0, z: 0 },
|
|
433
440
|
ghostTracker: ibGhostTracker,
|
|
441
|
+
getGhostParser: (classId) => this.registry.getGhostParser(classId),
|
|
434
442
|
};
|
|
435
443
|
controlObjectData = parser.readPacketData(bs, conn);
|
|
444
|
+
// The control object's readPacketData establishes the
|
|
445
|
+
// connection's compression point (its position) — carry it
|
|
446
|
+
// into the packet parser seed.
|
|
447
|
+
initialCompressionPoint = conn.compressionPoint;
|
|
436
448
|
debugInitial("after control readPacketData bit=%d parser=%s", bs.getCurPos(), parser.name);
|
|
437
449
|
}
|
|
438
450
|
}
|
|
@@ -456,9 +468,7 @@ export class DemoParser {
|
|
|
456
468
|
}
|
|
457
469
|
const remaining = totalBits - bs.getCurPos();
|
|
458
470
|
const missionPrintableRatio = missionName.length > 0
|
|
459
|
-
? missionName
|
|
460
|
-
.split("")
|
|
461
|
-
.filter((c) => {
|
|
471
|
+
? missionName.split("").filter((c) => {
|
|
462
472
|
const code = c.charCodeAt(0);
|
|
463
473
|
return code >= 0x20 && code <= 0x7e;
|
|
464
474
|
}).length / missionName.length
|
|
@@ -490,6 +500,7 @@ export class DemoParser {
|
|
|
490
500
|
initialEvents,
|
|
491
501
|
controlObjectGhostIndex,
|
|
492
502
|
controlObjectData,
|
|
503
|
+
initialCompressionPoint,
|
|
493
504
|
missionName,
|
|
494
505
|
missionCRC,
|
|
495
506
|
phase2TrailingBits: remaining,
|
|
@@ -513,7 +524,16 @@ export class DemoParser {
|
|
|
513
524
|
const triggerFlags = [];
|
|
514
525
|
for (let i = 0; i < 6; i++)
|
|
515
526
|
triggerFlags.push(bs.readFlag());
|
|
516
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
clientId,
|
|
529
|
+
teamId,
|
|
530
|
+
score,
|
|
531
|
+
field0,
|
|
532
|
+
field1,
|
|
533
|
+
field2,
|
|
534
|
+
isBot,
|
|
535
|
+
triggerFlags,
|
|
536
|
+
};
|
|
517
537
|
}
|
|
518
538
|
/**
|
|
519
539
|
* Read DemoValues from FUN_005fb5c0 lines 388966-388980.
|
|
@@ -696,6 +716,7 @@ export class DemoParser {
|
|
|
696
716
|
guaranteed: true,
|
|
697
717
|
dataBitsStart,
|
|
698
718
|
dataBitsEnd: dataBitsStart,
|
|
719
|
+
failed: true,
|
|
699
720
|
});
|
|
700
721
|
break;
|
|
701
722
|
}
|
|
@@ -707,6 +728,7 @@ export class DemoParser {
|
|
|
707
728
|
guaranteed: true,
|
|
708
729
|
dataBitsStart,
|
|
709
730
|
dataBitsEnd: dataBitsStart,
|
|
731
|
+
failed: true,
|
|
710
732
|
});
|
|
711
733
|
break;
|
|
712
734
|
}
|
|
@@ -923,9 +945,22 @@ export class DemoParser {
|
|
|
923
945
|
trigger.push(data[57 + i] !== 0);
|
|
924
946
|
}
|
|
925
947
|
return {
|
|
926
|
-
px,
|
|
927
|
-
|
|
928
|
-
|
|
948
|
+
px,
|
|
949
|
+
py,
|
|
950
|
+
pz,
|
|
951
|
+
pyaw,
|
|
952
|
+
ppitch,
|
|
953
|
+
proll,
|
|
954
|
+
x,
|
|
955
|
+
y,
|
|
956
|
+
z,
|
|
957
|
+
yaw,
|
|
958
|
+
pitch,
|
|
959
|
+
roll,
|
|
960
|
+
id,
|
|
961
|
+
sendCount,
|
|
962
|
+
freeLook,
|
|
963
|
+
trigger,
|
|
929
964
|
};
|
|
930
965
|
}
|
|
931
966
|
/**
|