t2-demo-parser 1.0.2 → 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/DataBlockParsers.d.ts.map +1 -1
- package/dist/DataBlockParsers.js +51 -42
- package/dist/DataBlockParsers.js.map +1 -1
- package/dist/DataBlockParsers.test.js +32 -0
- package/dist/DataBlockParsers.test.js.map +1 -1
- 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/dataBlockDataTypes.d.ts +22 -21
- package/dist/dataBlockDataTypes.d.ts.map +1 -1
- package/dist/dataBlockDataTypes.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):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataBlockParsers.d.ts","sourceRoot":"","sources":["../src/DataBlockParsers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"DataBlockParsers.d.ts","sourceRoot":"","sources":["../src/DataBlockParsers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAkvExD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CA2NtE"}
|
package/dist/DataBlockParsers.js
CHANGED
|
@@ -440,23 +440,25 @@ function playerDataUnpack(bs) {
|
|
|
440
440
|
splashEmitters.push(readDataBlockRef(bs));
|
|
441
441
|
}
|
|
442
442
|
result.splashEmitters = splashEmitters;
|
|
443
|
-
// 17. 11× F32
|
|
444
|
-
//
|
|
445
|
-
|
|
443
|
+
// 17. 11× F32 (offsets 0x3fc-0x424). Names binary-verified against
|
|
444
|
+
// initPersistFields: the two leading fields are the Tribes 2 heat
|
|
445
|
+
// signature rates (retail player.cs: 1/4 and 1/3), then the ground
|
|
446
|
+
// impact shake block. Demo values confirm: 0.25, 0.333…
|
|
447
|
+
result.heatDecayPerSec = bs.readF32(); // 0x3fc
|
|
448
|
+
result.heatIncreasePerSec = bs.readF32(); // 0x400
|
|
449
|
+
result.groundImpactMinSpeed = bs.readF32(); // 0x404
|
|
446
450
|
result.groundImpactShakeFreq = {
|
|
447
|
-
x: bs.readF32(), //
|
|
448
|
-
y: bs.readF32(), //
|
|
449
|
-
z: bs.readF32(), //
|
|
451
|
+
x: bs.readF32(), // 0x408
|
|
452
|
+
y: bs.readF32(), // 0x40c
|
|
453
|
+
z: bs.readF32(), // 0x410
|
|
450
454
|
};
|
|
451
455
|
result.groundImpactShakeAmp = {
|
|
452
|
-
x: bs.readF32(), //
|
|
453
|
-
y: bs.readF32(), //
|
|
454
|
-
z: bs.readF32(), //
|
|
456
|
+
x: bs.readF32(), // 0x414
|
|
457
|
+
y: bs.readF32(), // 0x418
|
|
458
|
+
z: bs.readF32(), // 0x41c
|
|
455
459
|
};
|
|
456
|
-
result.groundImpactShakeDuration = bs.readF32(); //
|
|
457
|
-
result.groundImpactShakeFalloff = bs.readF32(); //
|
|
458
|
-
result.boundingRadius = bs.readF32(); // 0x420 (Tribes 2 extra)
|
|
459
|
-
result.moveBubbleSize = bs.readF32(); // 0x424 (Tribes 2 extra)
|
|
460
|
+
result.groundImpactShakeDuration = bs.readF32(); // 0x420
|
|
461
|
+
result.groundImpactShakeFalloff = bs.readF32(); // 0x424
|
|
460
462
|
return result;
|
|
461
463
|
}
|
|
462
464
|
// ============================================================
|
|
@@ -479,6 +481,8 @@ function vehicleDataUnpack(bs) {
|
|
|
479
481
|
result.softImpactSpeed = bs.readF32(); // 0x380
|
|
480
482
|
result.hardImpactSpeed = bs.readF32(); // 0x384 (900 dec)
|
|
481
483
|
result.minRollSpeed = bs.readF32(); // 0x388
|
|
484
|
+
// Registered in the retail binary as "maxSteerinAngle" (engine typo);
|
|
485
|
+
// we keep the corrected spelling.
|
|
482
486
|
result.maxSteeringAngle = bs.readF32(); // 0x38c
|
|
483
487
|
result.maxDrag = bs.readF32(); // 0x3a4
|
|
484
488
|
result.minDrag = bs.readF32(); // 0x3a0
|
|
@@ -1222,9 +1226,10 @@ function debrisDataUnpack(bs) {
|
|
|
1222
1226
|
result.gravModifier = bs.readF32(); // 0x78
|
|
1223
1227
|
result.terminalVelocity = bs.readF32(); // 0x7c
|
|
1224
1228
|
result.ignoreWater = readBool(bs); // 0x80 (8-bit bool)
|
|
1225
|
-
// 2 readString
|
|
1226
|
-
|
|
1227
|
-
result.
|
|
1229
|
+
// 2 readString — names binary-verified: 0x8c is registered as
|
|
1230
|
+
// `texture`, 0x84 as `shapeName` (read in that wire order)
|
|
1231
|
+
result.texture = bs.readString(); // 0x8c
|
|
1232
|
+
result.shapeName = bs.readString(); // 0x84
|
|
1228
1233
|
// 2 emitter refs (loop at 0xa4, 0xa8) + 1 explosion ref (0x94)
|
|
1229
1234
|
result.emitter0 = readDataBlockRef(bs);
|
|
1230
1235
|
result.emitter1 = readDataBlockRef(bs);
|
|
@@ -1502,15 +1507,17 @@ function audioEnvironmentUnpack(bs) {
|
|
|
1502
1507
|
result.reflectionsDelay = readRangedF32(bs, 0, 0.3, 9); // 0x5c
|
|
1503
1508
|
result.reverbDelay = readRangedF32(bs, 0, 0.1, 7); // 0x60
|
|
1504
1509
|
result.roomVolume = readRangedS32(bs, -10000, 0); // 0x64
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
result.
|
|
1508
|
-
result.
|
|
1509
|
-
|
|
1510
|
+
// Names binary-verified: initPersistFields maps 0x6c damping,
|
|
1511
|
+
// 0x70 environmentSize, 0x74 environmentDiffusion, 0x78 airAbsorption.
|
|
1512
|
+
result.damping = readRangedF32(bs, 0, 1, 9); // 0x6c (9 bits, confirmed in binary)
|
|
1513
|
+
result.environmentSize = readRangedF32(bs, 0, 2, 10); // 0x70 (10 bits, confirmed in binary)
|
|
1514
|
+
result.environmentDiffusion = readRangedF32(bs, 1, 100, 8); // 0x74 (8 bits, confirmed in binary)
|
|
1515
|
+
result.airAbsorption = readRangedF32(bs, 0, 1, 10); // 0x78 (10 bits, confirmed in binary)
|
|
1510
1516
|
result.flags = bs.readInt(6); // 0x7c
|
|
1511
1517
|
}
|
|
1512
|
-
// Trailing field: always present after both branches (
|
|
1513
|
-
|
|
1518
|
+
// Trailing field: always present after both branches (offset 0x68 =
|
|
1519
|
+
// effectVolume per initPersistFields)
|
|
1520
|
+
result.effectVolume = readRangedF32(bs, 0, 1, 8);
|
|
1514
1521
|
return result;
|
|
1515
1522
|
}
|
|
1516
1523
|
// ============================================================
|
|
@@ -1697,20 +1704,22 @@ function stationFXVehicleDataUnpack(bs) {
|
|
|
1697
1704
|
y: bs.readF32(), // 0x90
|
|
1698
1705
|
z: bs.readF32(), // 0x94
|
|
1699
1706
|
};
|
|
1700
|
-
// 11 strings
|
|
1701
|
-
|
|
1702
|
-
//
|
|
1703
|
-
result.
|
|
1704
|
-
|
|
1705
|
-
result.
|
|
1706
|
-
result.
|
|
1707
|
-
result.
|
|
1708
|
-
result.
|
|
1709
|
-
result.
|
|
1710
|
-
result.
|
|
1707
|
+
// 11 strings — names binary-verified (initPersistFields: 0x98
|
|
1708
|
+
// glowNodeName, 0x9c leftNodeName[4], 0xac rightNodeName[4], 0xbc
|
|
1709
|
+
// texture[2]) and demo-verified ("GLOWFX", LFX1/RFX1…, stationGlow).
|
|
1710
|
+
result.glowNodeName = bs.readString(); // 0x98
|
|
1711
|
+
// 4×(left, right) node name pairs, interleaved on the wire
|
|
1712
|
+
result.leftNodeName0 = bs.readString();
|
|
1713
|
+
result.rightNodeName0 = bs.readString();
|
|
1714
|
+
result.leftNodeName1 = bs.readString();
|
|
1715
|
+
result.rightNodeName1 = bs.readString();
|
|
1716
|
+
result.leftNodeName2 = bs.readString();
|
|
1717
|
+
result.rightNodeName2 = bs.readString();
|
|
1718
|
+
result.leftNodeName3 = bs.readString();
|
|
1719
|
+
result.rightNodeName3 = bs.readString();
|
|
1711
1720
|
// 2 readString
|
|
1712
|
-
result.
|
|
1713
|
-
result.
|
|
1721
|
+
result.texture0 = bs.readString();
|
|
1722
|
+
result.texture1 = bs.readString();
|
|
1714
1723
|
return result;
|
|
1715
1724
|
}
|
|
1716
1725
|
// ============================================================
|
|
@@ -1729,12 +1738,12 @@ function stationFXPersonalDataUnpack(bs) {
|
|
|
1729
1738
|
result.bottomAlpha = bs.readF32();
|
|
1730
1739
|
result.glowSpeed = bs.readF32();
|
|
1731
1740
|
result.scrollSpeed = bs.readF32();
|
|
1732
|
-
//
|
|
1733
|
-
|
|
1734
|
-
result.
|
|
1735
|
-
|
|
1736
|
-
result.
|
|
1737
|
-
result.
|
|
1741
|
+
// 4 strings — demo-verified ("FX1", "FX2", "special/stationLight"):
|
|
1742
|
+
// the two attach node names, then texture[2]
|
|
1743
|
+
result.leftNodeName = bs.readString();
|
|
1744
|
+
result.rightNodeName = bs.readString();
|
|
1745
|
+
result.texture0 = bs.readString();
|
|
1746
|
+
result.texture1 = bs.readString();
|
|
1738
1747
|
return result;
|
|
1739
1748
|
}
|
|
1740
1749
|
// ============================================================
|