t2-demo-parser 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -7
- package/dist/BitStream.d.ts +14 -3
- package/dist/BitStream.d.ts.map +1 -1
- package/dist/BitStream.js +28 -6
- package/dist/BitStream.js.map +1 -1
- package/dist/BitWriter.d.ts +29 -0
- package/dist/BitWriter.d.ts.map +1 -0
- package/dist/BitWriter.js +76 -0
- package/dist/BitWriter.js.map +1 -0
- package/dist/ClassRegistry.d.ts +23 -28
- package/dist/ClassRegistry.d.ts.map +1 -1
- package/dist/ClassRegistry.js +75 -84
- package/dist/ClassRegistry.js.map +1 -1
- package/dist/DataBlockParsers.d.ts.map +1 -1
- package/dist/DataBlockParsers.js +51 -76
- package/dist/DataBlockParsers.js.map +1 -1
- package/dist/DemoParser.d.ts +23 -22
- package/dist/DemoParser.d.ts.map +1 -1
- package/dist/DemoParser.js +218 -266
- package/dist/DemoParser.js.map +1 -1
- package/dist/EventParsers.d.ts.map +1 -1
- package/dist/EventParsers.js +87 -58
- package/dist/EventParsers.js.map +1 -1
- package/dist/GhostManager.d.ts +7 -1
- package/dist/GhostManager.d.ts.map +1 -1
- package/dist/GhostManager.js +231 -146
- package/dist/GhostManager.js.map +1 -1
- package/dist/HuffmanProcessor.d.ts +15 -0
- package/dist/HuffmanProcessor.d.ts.map +1 -1
- package/dist/HuffmanProcessor.js +45 -0
- package/dist/HuffmanProcessor.js.map +1 -1
- package/dist/LiveParser.d.ts +20 -1
- package/dist/LiveParser.d.ts.map +1 -1
- package/dist/LiveParser.js +31 -12
- package/dist/LiveParser.js.map +1 -1
- package/dist/PacketParser.d.ts +22 -2
- package/dist/PacketParser.d.ts.map +1 -1
- package/dist/PacketParser.js +111 -48
- package/dist/PacketParser.js.map +1 -1
- package/dist/Timeline.d.ts +5 -2
- package/dist/Timeline.d.ts.map +1 -1
- package/dist/Timeline.js +25 -12
- package/dist/Timeline.js.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/dataBlockDataTypes.d.ts +111 -112
- package/dist/dataBlockDataTypes.d.ts.map +1 -1
- package/dist/dataBlockDataTypes.js.map +1 -1
- package/dist/defaultRegistry.d.ts +9 -0
- package/dist/defaultRegistry.d.ts.map +1 -0
- package/dist/defaultRegistry.js +43 -0
- package/dist/defaultRegistry.js.map +1 -0
- package/dist/eventDataTypes.d.ts +74 -72
- package/dist/eventDataTypes.d.ts.map +1 -1
- package/dist/eventDataTypes.js.map +1 -1
- package/dist/ghostDataTypes.d.ts +125 -131
- package/dist/ghostDataTypes.d.ts.map +1 -1
- package/dist/ghostDataTypes.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +51 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/package.json +4 -1
- package/dist/BitStream.test.d.ts +0 -2
- package/dist/BitStream.test.d.ts.map +0 -1
- package/dist/BitStream.test.js +0 -419
- package/dist/BitStream.test.js.map +0 -1
- package/dist/ClassRegistry.test.d.ts +0 -2
- package/dist/ClassRegistry.test.d.ts.map +0 -1
- package/dist/ClassRegistry.test.js +0 -83
- package/dist/ClassRegistry.test.js.map +0 -1
- package/dist/DataBlockParsers.test.d.ts +0 -2
- package/dist/DataBlockParsers.test.d.ts.map +0 -1
- package/dist/DataBlockParsers.test.js +0 -294
- package/dist/DataBlockParsers.test.js.map +0 -1
- package/dist/DemoParser.test.d.ts +0 -2
- package/dist/DemoParser.test.d.ts.map +0 -1
- package/dist/DemoParser.test.js +0 -472
- package/dist/DemoParser.test.js.map +0 -1
- package/dist/GhostStateAccumulator.test.d.ts +0 -2
- package/dist/GhostStateAccumulator.test.d.ts.map +0 -1
- package/dist/GhostStateAccumulator.test.js +0 -201
- package/dist/GhostStateAccumulator.test.js.map +0 -1
- package/dist/GhostTracker.test.d.ts +0 -2
- package/dist/GhostTracker.test.d.ts.map +0 -1
- package/dist/GhostTracker.test.js +0 -64
- package/dist/GhostTracker.test.js.map +0 -1
- package/dist/IncrementalParser.test.d.ts +0 -2
- package/dist/IncrementalParser.test.d.ts.map +0 -1
- package/dist/IncrementalParser.test.js +0 -226
- package/dist/IncrementalParser.test.js.map +0 -1
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { DemoParser } from "./DemoParser.js";
|
|
5
|
-
import { createLiveParser } from "./LiveParser.js";
|
|
6
|
-
import { GhostStateAccumulator, mergeGhostParsedData, } from "./GhostStateAccumulator.js";
|
|
7
|
-
import { BlockTypePacket, BlockTypeSendPacket } from "./types.js";
|
|
8
|
-
const DEMO_DIR = path.resolve(import.meta.dirname, "..", "data");
|
|
9
|
-
function demoExists(file) {
|
|
10
|
-
return fs.existsSync(path.join(DEMO_DIR, file));
|
|
11
|
-
}
|
|
12
|
-
describe("mergeGhostParsedData", () => {
|
|
13
|
-
it("overwrites scalars and preserves create-only fields", () => {
|
|
14
|
-
const merged = mergeGhostParsedData({
|
|
15
|
-
type: "Player",
|
|
16
|
-
isStatic: true,
|
|
17
|
-
health: 1,
|
|
18
|
-
position: { x: 0, y: 0, z: 0 },
|
|
19
|
-
}, { health: 0.5, position: { x: 1, y: 2, z: 3 } });
|
|
20
|
-
expect(merged).toEqual({
|
|
21
|
-
type: "Player",
|
|
22
|
-
isStatic: true,
|
|
23
|
-
health: 0.5,
|
|
24
|
-
position: { x: 1, y: 2, z: 3 },
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
it("merges indexed entry arrays by index with entry replacement", () => {
|
|
28
|
-
const merged = mergeGhostParsedData({
|
|
29
|
-
threads: [
|
|
30
|
-
{ index: 0, sequence: 5, forward: true, atEnd: false },
|
|
31
|
-
{ index: 1, sequence: 9, forward: true, atEnd: false },
|
|
32
|
-
],
|
|
33
|
-
}, { threads: [{ index: 1, sequence: 12, forward: false, atEnd: true }] });
|
|
34
|
-
expect(merged.threads).toEqual([
|
|
35
|
-
{ index: 0, sequence: 5, forward: true, atEnd: false },
|
|
36
|
-
{ index: 1, sequence: 12, forward: false, atEnd: true },
|
|
37
|
-
]);
|
|
38
|
-
});
|
|
39
|
-
it("retains images entries with dataBlockId 0 (slot clear)", () => {
|
|
40
|
-
const merged = mergeGhostParsedData({ images: [{ index: 2, dataBlockId: 40, firing: true }] }, { images: [{ index: 2, dataBlockId: 0 }] });
|
|
41
|
-
expect(merged.images).toEqual([{ index: 2, dataBlockId: 0 }]);
|
|
42
|
-
});
|
|
43
|
-
it("adds new indexed entries alongside existing ones", () => {
|
|
44
|
-
const merged = mergeGhostParsedData({ sounds: [{ index: 0, dataBlockId: 7 }] }, { sounds: [{ index: 3, dataBlockId: 8 }] });
|
|
45
|
-
expect(merged.sounds).toEqual([
|
|
46
|
-
{ index: 0, dataBlockId: 7 },
|
|
47
|
-
{ index: 3, dataBlockId: 8 },
|
|
48
|
-
]);
|
|
49
|
-
});
|
|
50
|
-
it("replaces non-indexed arrays wholesale", () => {
|
|
51
|
-
const merged = mergeGhostParsedData({ wheels: [{ spring: 1 }, { spring: 2 }] }, { wheels: [{ spring: 3 }, { spring: 4 }] });
|
|
52
|
-
expect(merged.wheels).toEqual([{ spring: 3 }, { spring: 4 }]);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
describe("EndGhosting side effects", () => {
|
|
56
|
-
it("clears ghosts but retains datablocks (connection-lifetime state)", () => {
|
|
57
|
-
const kit = createLiveParser({
|
|
58
|
-
dataBlocks: [[161, { shapeName: "armor.dts" }]],
|
|
59
|
-
ghosts: [{ index: 5, classId: 10 }],
|
|
60
|
-
});
|
|
61
|
-
// White-box: apply the EndGhosting side effect directly. The real
|
|
62
|
-
// client (netGhost.cc:706) deletes only local ghosts; datablocks must
|
|
63
|
-
// survive mission changes because the server's transmitDataBlocks
|
|
64
|
-
// skips datablocks already sent on the connection.
|
|
65
|
-
kit.packetParser.applyEventSideEffects({ type: "GhostingMessageEvent", message: 2 });
|
|
66
|
-
expect(kit.ghostTracker.size()).toBe(0);
|
|
67
|
-
expect(kit.packetParser.getDataBlockDataMap()?.get(161)).toEqual({
|
|
68
|
-
shapeName: "armor.dts",
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
it("GhostStateAccumulator clears accumulated ghosts on EndGhosting", () => {
|
|
72
|
-
const accumulator = new GhostStateAccumulator();
|
|
73
|
-
accumulator.applyPacket({
|
|
74
|
-
events: [],
|
|
75
|
-
ghosts: [
|
|
76
|
-
{
|
|
77
|
-
index: 1,
|
|
78
|
-
type: "create",
|
|
79
|
-
classId: 10,
|
|
80
|
-
updateBitsStart: 0,
|
|
81
|
-
updateBitsEnd: 0,
|
|
82
|
-
parsedData: { type: "Player" },
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
});
|
|
86
|
-
expect(accumulator.size()).toBe(1);
|
|
87
|
-
accumulator.applyPacket({
|
|
88
|
-
events: [
|
|
89
|
-
{
|
|
90
|
-
classId: 20,
|
|
91
|
-
guaranteed: true,
|
|
92
|
-
dataBitsStart: 0,
|
|
93
|
-
dataBitsEnd: 0,
|
|
94
|
-
parsedData: { type: "GhostingMessageEvent", message: 2 },
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
ghosts: [],
|
|
98
|
-
});
|
|
99
|
-
expect(accumulator.size()).toBe(0);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
/**
|
|
103
|
-
* The load-bearing regression tests: export all cross-packet parser state
|
|
104
|
-
* at block K, seed a fresh parser stack with it, and verify both parsers
|
|
105
|
-
* produce deep-equal output for the remainder of the stream. This is
|
|
106
|
-
* exactly the watch-mode late-joiner scenario (relay exports, browser
|
|
107
|
-
* seeds) using demo files as a deterministic packet source.
|
|
108
|
-
*/
|
|
109
|
-
const ROUND_TRIP_DEMOS = [
|
|
110
|
-
// Vehicle-heavy demo (wheels, mounts).
|
|
111
|
-
{ file: "exogen_Harvester.rec", cutovers: [2_000, 12_000] },
|
|
112
|
-
{ file: "uploads_6_SterIO_2025_LT_Pub_SH.rec", cutovers: [3_000, 20_000] },
|
|
113
|
-
// MPB (WheeledVehicle) created ~block 86k, wheel updates continue past
|
|
114
|
-
// the cutover — guards seeded parsing of the wheel section, whose length
|
|
115
|
-
// must not depend on state accumulated before the seed point.
|
|
116
|
-
{
|
|
117
|
-
file: "uploads_7_2025-04-26_21-30_Tacocat_CTFGame_MisadventureV2.rec",
|
|
118
|
-
cutovers: [90_000],
|
|
119
|
-
},
|
|
120
|
-
];
|
|
121
|
-
/** Bound runtime: compare at most this many blocks after the cutover. */
|
|
122
|
-
const MAX_CONTINUE_BLOCKS = 10_000;
|
|
123
|
-
describe("seeded parser round-trip", () => {
|
|
124
|
-
for (const demo of ROUND_TRIP_DEMOS) {
|
|
125
|
-
for (const cutover of demo.cutovers) {
|
|
126
|
-
it(`${demo.file} @ block ${cutover}: seeded parser stays in lockstep`, { timeout: 120_000, skip: !demoExists(demo.file) }, async () => {
|
|
127
|
-
const parser = new DemoParser(fs.readFileSync(path.join(DEMO_DIR, demo.file)));
|
|
128
|
-
const { initialBlock } = await parser.load();
|
|
129
|
-
// Seed from the demo's initial ghosts, mirroring how the demo
|
|
130
|
-
// pipeline seeds its tracker. (A live relay connection has no
|
|
131
|
-
// initial block — it sees every create from connect onward.)
|
|
132
|
-
const accumulator = new GhostStateAccumulator();
|
|
133
|
-
accumulator.applyPacket({
|
|
134
|
-
events: [],
|
|
135
|
-
ghosts: initialBlock.initialGhosts,
|
|
136
|
-
});
|
|
137
|
-
for (let i = 0; i < cutover; i++) {
|
|
138
|
-
const block = parser.nextBlock();
|
|
139
|
-
if (!block)
|
|
140
|
-
break;
|
|
141
|
-
if (block.type === BlockTypePacket && block.parsed) {
|
|
142
|
-
accumulator.applyPacket(block.parsed);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const source = parser.getPacketParser();
|
|
146
|
-
const tracker = parser.getGhostTracker();
|
|
147
|
-
// The accumulator must track exactly the ghosts the parser knows.
|
|
148
|
-
const trackerEntries = [...tracker.getAllGhosts().entries()];
|
|
149
|
-
expect(accumulator.size()).toBe(trackerEntries.length);
|
|
150
|
-
const seedByIndex = new Map(accumulator
|
|
151
|
-
.getGhostSeeds()
|
|
152
|
-
.map((seed) => [seed.index, seed.classId]));
|
|
153
|
-
for (const [index, entry] of trackerEntries) {
|
|
154
|
-
expect(seedByIndex.get(index)).toBe(entry.classId);
|
|
155
|
-
}
|
|
156
|
-
const rejectedAtCutover = source.protocolRejected;
|
|
157
|
-
const seeded = createLiveParser({
|
|
158
|
-
dataBlocks: source.getDataBlockDataMap() ?? [],
|
|
159
|
-
ghosts: trackerEntries.map(([index, entry]) => ({
|
|
160
|
-
index,
|
|
161
|
-
classId: entry.classId,
|
|
162
|
-
})),
|
|
163
|
-
connectionProtocolState: source.getConnectionProtocolState(),
|
|
164
|
-
nextRecvEventSeq: source.getNextRecvEventSeq(),
|
|
165
|
-
compressionPoint: source.getCompressionPoint(),
|
|
166
|
-
pendingGuaranteedEvents: source.getPendingGuaranteedEvents(),
|
|
167
|
-
});
|
|
168
|
-
let compared = 0;
|
|
169
|
-
for (let i = 0; i < MAX_CONTINUE_BLOCKS; i++) {
|
|
170
|
-
const block = parser.nextBlock();
|
|
171
|
-
if (!block)
|
|
172
|
-
break;
|
|
173
|
-
if (block.type === BlockTypeSendPacket) {
|
|
174
|
-
seeded.packetParser.onSendPacketTrigger();
|
|
175
|
-
}
|
|
176
|
-
else if (block.type === BlockTypePacket) {
|
|
177
|
-
let seededParsed;
|
|
178
|
-
try {
|
|
179
|
-
seededParsed = seeded.packetParser.parsePacket(block.data);
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
seededParsed = undefined;
|
|
183
|
-
}
|
|
184
|
-
// Fast path: string compare; fall back to toEqual for a diff.
|
|
185
|
-
if (JSON.stringify(seededParsed) !== JSON.stringify(block.parsed)) {
|
|
186
|
-
expect(seededParsed).toEqual(block.parsed);
|
|
187
|
-
}
|
|
188
|
-
compared++;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
expect(compared).toBeGreaterThan(1_000);
|
|
192
|
-
expect(seeded.packetParser.ghostsTrackerDiverged).toBe(0);
|
|
193
|
-
expect(seeded.packetParser.ghostsFailed).toBe(0);
|
|
194
|
-
// Both parsers see identical headers from identical state, so
|
|
195
|
-
// rejects (if any) must match the source parser's count delta.
|
|
196
|
-
expect(seeded.packetParser.protocolRejected).toBe(source.protocolRejected - rejectedAtCutover);
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
//# sourceMappingURL=GhostStateAccumulator.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GhostStateAccumulator.test.js","sourceRoot":"","sources":["../src/GhostStateAccumulator.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGlE,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEjE,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;AAClD,CAAC;AAED,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GAAG,oBAAoB,CACjC;YACE,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,CAAC;YACT,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;SAC/B,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAChD,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACrB,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,GAAG;YACX,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;SAC/B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,MAAM,GAAG,oBAAoB,CACjC;YACE,OAAO,EAAE;gBACP,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;gBACtD,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;aACvD;SACF,EACD,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CACvE,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC;YAC7B,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;YACtD,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE;SACxD,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,MAAM,GAAG,oBAAoB,CACjC,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,EACzD,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,EAAE,CAC3C,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,MAAM,GAAG,oBAAoB,CACjC,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,EAAE,EAC1C,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,EAAE,CAC3C,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YAC5B,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE;YAC5B,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,MAAM,GAAG,oBAAoB,CACjC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,EAC1C,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAC3C,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,GAAG,GAAG,gBAAgB,CAAC;YAC3B,UAAU,EAAE,CAAC,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;YAC/C,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;SACpC,CAAC,CAAC;QACH,kEAAkE;QAClE,sEAAsE;QACtE,kEAAkE;QAClE,mDAAmD;QAEjD,GAAG,CAAC,YAGL,CAAC,qBAAqB,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;QACtE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,mBAAmB,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;YAC/D,SAAS,EAAE,WAAW;SACvB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,WAAW,GAAG,IAAI,qBAAqB,EAAE,CAAC;QAChD,WAAW,CAAC,WAAW,CAAC;YACtB,MAAM,EAAE,EAAE;YACV,MAAM,EAAE;gBACN;oBACE,KAAK,EAAE,CAAC;oBACR,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,EAAE;oBACX,eAAe,EAAE,CAAC;oBAClB,aAAa,EAAE,CAAC;oBAChB,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;iBAC/B;aACF;SACuB,CAAC,CAAC;QAC5B,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnC,WAAW,CAAC,WAAW,CAAC;YACtB,MAAM,EAAE;gBACN;oBACE,OAAO,EAAE,EAAE;oBACX,UAAU,EAAE,IAAI;oBAChB,aAAa,EAAE,CAAC;oBAChB,WAAW,EAAE,CAAC;oBACd,UAAU,EAAE,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,CAAC,EAAE;iBACzD;aACF;YACD,MAAM,EAAE,EAAE;SACc,CAAC,CAAC;QAC5B,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG;IACvB,uCAAuC;IACvC,EAAE,IAAI,EAAE,sBAAsB,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE;IAC3D,EAAE,IAAI,EAAE,qCAAqC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE;IAC1E,uEAAuE;IACvE,yEAAyE;IACzE,8DAA8D;IAC9D;QACE,IAAI,EAAE,+DAA+D;QACrE,QAAQ,EAAE,CAAC,MAAM,CAAC;KACnB;CACF,CAAC;AAEF,yEAAyE;AACzE,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,KAAK,MAAM,IAAI,IAAI,gBAAgB,EAAE,CAAC;QACpC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpC,EAAE,CACA,GAAG,IAAI,CAAC,IAAI,YAAY,OAAO,mCAAmC,EAClE,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAClD,KAAK,IAAI,EAAE;gBACT,MAAM,MAAM,GAAG,IAAI,UAAU,CAC3B,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAChD,CAAC;gBACF,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;gBAE7C,8DAA8D;gBAC9D,8DAA8D;gBAC9D,6DAA6D;gBAC7D,MAAM,WAAW,GAAG,IAAI,qBAAqB,EAAE,CAAC;gBAChD,WAAW,CAAC,WAAW,CAAC;oBACtB,MAAM,EAAE,EAAE;oBACV,MAAM,EAAE,YAAY,CAAC,aAAa;iBACV,CAAC,CAAC;gBAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC;oBACjC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;oBACjC,IAAI,CAAC,KAAK;wBAAE,MAAM;oBAClB,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;wBACnD,WAAW,CAAC,WAAW,CAAC,KAAK,CAAC,MAAoB,CAAC,CAAC;oBACtD,CAAC;gBACH,CAAC;gBAED,MAAM,MAAM,GAAG,MAAM,CAAC,eAAe,EAAE,CAAC;gBACxC,MAAM,OAAO,GAAG,MAAM,CAAC,eAAe,EAAE,CAAC;gBAEzC,kEAAkE;gBAClE,MAAM,cAAc,GAAG,CAAC,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC7D,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;gBACvD,MAAM,WAAW,GAAG,IAAI,GAAG,CACzB,WAAW;qBACR,aAAa,EAAE;qBACf,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAC7C,CAAC;gBACF,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,cAAc,EAAE,CAAC;oBAC5C,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBACrD,CAAC;gBAED,MAAM,iBAAiB,GAAG,MAAM,CAAC,gBAAgB,CAAC;gBAElD,MAAM,MAAM,GAAG,gBAAgB,CAAC;oBAC9B,UAAU,EAAE,MAAM,CAAC,mBAAmB,EAAE,IAAI,EAAE;oBAC9C,MAAM,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;wBAC9C,KAAK;wBACL,OAAO,EAAE,KAAK,CAAC,OAAO;qBACvB,CAAC,CAAC;oBACH,uBAAuB,EAAE,MAAM,CAAC,0BAA0B,EAAE;oBAC5D,gBAAgB,EAAE,MAAM,CAAC,mBAAmB,EAAE;oBAC9C,gBAAgB,EAAE,MAAM,CAAC,mBAAmB,EAAE;oBAC9C,uBAAuB,EAAE,MAAM,CAAC,0BAA0B,EAAE;iBAC7D,CAAC,CAAC;gBAEH,IAAI,QAAQ,GAAG,CAAC,CAAC;gBACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,mBAAmB,EAAE,CAAC,EAAE,EAAE,CAAC;oBAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;oBACjC,IAAI,CAAC,KAAK;wBAAE,MAAM;oBAClB,IAAI,KAAK,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;wBACvC,MAAM,CAAC,YAAY,CAAC,mBAAmB,EAAE,CAAC;oBAC5C,CAAC;yBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;wBAC1C,IAAI,YAAoC,CAAC;wBACzC,IAAI,CAAC;4BACH,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;wBAC7D,CAAC;wBAAC,MAAM,CAAC;4BACP,YAAY,GAAG,SAAS,CAAC;wBAC3B,CAAC;wBACD,8DAA8D;wBAC9D,IACE,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,EAC7D,CAAC;4BACD,MAAM,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;wBAC7C,CAAC;wBACD,QAAQ,EAAE,CAAC;oBACb,CAAC;gBACH,CAAC;gBAED,MAAM,CAAC,QAAQ,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;gBACxC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC1D,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACjD,8DAA8D;gBAC9D,+DAA+D;gBAC/D,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAC/C,MAAM,CAAC,gBAAgB,GAAG,iBAAiB,CAC5C,CAAC;YACJ,CAAC,CACF,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { DemoParser } from \"./DemoParser.js\";\nimport { createLiveParser } from \"./LiveParser.js\";\nimport {\n GhostStateAccumulator,\n mergeGhostParsedData,\n} from \"./GhostStateAccumulator.js\";\nimport { BlockTypePacket, BlockTypeSendPacket } from \"./types.js\";\nimport type { PacketData } from \"./types.js\";\n\nconst DEMO_DIR = path.resolve(import.meta.dirname, \"..\", \"data\");\n\nfunction demoExists(file: string): boolean {\n return fs.existsSync(path.join(DEMO_DIR, file));\n}\n\ndescribe(\"mergeGhostParsedData\", () => {\n it(\"overwrites scalars and preserves create-only fields\", () => {\n const merged = mergeGhostParsedData(\n {\n type: \"Player\",\n isStatic: true,\n health: 1,\n position: { x: 0, y: 0, z: 0 },\n },\n { health: 0.5, position: { x: 1, y: 2, z: 3 } },\n );\n expect(merged).toEqual({\n type: \"Player\",\n isStatic: true,\n health: 0.5,\n position: { x: 1, y: 2, z: 3 },\n });\n });\n\n it(\"merges indexed entry arrays by index with entry replacement\", () => {\n const merged = mergeGhostParsedData(\n {\n threads: [\n { index: 0, sequence: 5, forward: true, atEnd: false },\n { index: 1, sequence: 9, forward: true, atEnd: false },\n ],\n },\n { threads: [{ index: 1, sequence: 12, forward: false, atEnd: true }] },\n );\n expect(merged.threads).toEqual([\n { index: 0, sequence: 5, forward: true, atEnd: false },\n { index: 1, sequence: 12, forward: false, atEnd: true },\n ]);\n });\n\n it(\"retains images entries with dataBlockId 0 (slot clear)\", () => {\n const merged = mergeGhostParsedData(\n { images: [{ index: 2, dataBlockId: 40, firing: true }] },\n { images: [{ index: 2, dataBlockId: 0 }] },\n );\n expect(merged.images).toEqual([{ index: 2, dataBlockId: 0 }]);\n });\n\n it(\"adds new indexed entries alongside existing ones\", () => {\n const merged = mergeGhostParsedData(\n { sounds: [{ index: 0, dataBlockId: 7 }] },\n { sounds: [{ index: 3, dataBlockId: 8 }] },\n );\n expect(merged.sounds).toEqual([\n { index: 0, dataBlockId: 7 },\n { index: 3, dataBlockId: 8 },\n ]);\n });\n\n it(\"replaces non-indexed arrays wholesale\", () => {\n const merged = mergeGhostParsedData(\n { wheels: [{ spring: 1 }, { spring: 2 }] },\n { wheels: [{ spring: 3 }, { spring: 4 }] },\n );\n expect(merged.wheels).toEqual([{ spring: 3 }, { spring: 4 }]);\n });\n});\n\ndescribe(\"EndGhosting side effects\", () => {\n it(\"clears ghosts but retains datablocks (connection-lifetime state)\", () => {\n const kit = createLiveParser({\n dataBlocks: [[161, { shapeName: \"armor.dts\" }]],\n ghosts: [{ index: 5, classId: 10 }],\n });\n // White-box: apply the EndGhosting side effect directly. The real\n // client (netGhost.cc:706) deletes only local ghosts; datablocks must\n // survive mission changes because the server's transmitDataBlocks\n // skips datablocks already sent on the connection.\n (\n kit.packetParser as unknown as {\n applyEventSideEffects(data: Record<string, unknown>): void;\n }\n ).applyEventSideEffects({ type: \"GhostingMessageEvent\", message: 2 });\n expect(kit.ghostTracker.size()).toBe(0);\n expect(kit.packetParser.getDataBlockDataMap()?.get(161)).toEqual({\n shapeName: \"armor.dts\",\n });\n });\n\n it(\"GhostStateAccumulator clears accumulated ghosts on EndGhosting\", () => {\n const accumulator = new GhostStateAccumulator();\n accumulator.applyPacket({\n events: [],\n ghosts: [\n {\n index: 1,\n type: \"create\",\n classId: 10,\n updateBitsStart: 0,\n updateBitsEnd: 0,\n parsedData: { type: \"Player\" },\n },\n ],\n } as unknown as PacketData);\n expect(accumulator.size()).toBe(1);\n accumulator.applyPacket({\n events: [\n {\n classId: 20,\n guaranteed: true,\n dataBitsStart: 0,\n dataBitsEnd: 0,\n parsedData: { type: \"GhostingMessageEvent\", message: 2 },\n },\n ],\n ghosts: [],\n } as unknown as PacketData);\n expect(accumulator.size()).toBe(0);\n });\n});\n\n/**\n * The load-bearing regression tests: export all cross-packet parser state\n * at block K, seed a fresh parser stack with it, and verify both parsers\n * produce deep-equal output for the remainder of the stream. This is\n * exactly the watch-mode late-joiner scenario (relay exports, browser\n * seeds) using demo files as a deterministic packet source.\n */\nconst ROUND_TRIP_DEMOS = [\n // Vehicle-heavy demo (wheels, mounts).\n { file: \"exogen_Harvester.rec\", cutovers: [2_000, 12_000] },\n { file: \"uploads_6_SterIO_2025_LT_Pub_SH.rec\", cutovers: [3_000, 20_000] },\n // MPB (WheeledVehicle) created ~block 86k, wheel updates continue past\n // the cutover — guards seeded parsing of the wheel section, whose length\n // must not depend on state accumulated before the seed point.\n {\n file: \"uploads_7_2025-04-26_21-30_Tacocat_CTFGame_MisadventureV2.rec\",\n cutovers: [90_000],\n },\n];\n\n/** Bound runtime: compare at most this many blocks after the cutover. */\nconst MAX_CONTINUE_BLOCKS = 10_000;\n\ndescribe(\"seeded parser round-trip\", () => {\n for (const demo of ROUND_TRIP_DEMOS) {\n for (const cutover of demo.cutovers) {\n it(\n `${demo.file} @ block ${cutover}: seeded parser stays in lockstep`,\n { timeout: 120_000, skip: !demoExists(demo.file) },\n async () => {\n const parser = new DemoParser(\n fs.readFileSync(path.join(DEMO_DIR, demo.file)),\n );\n const { initialBlock } = await parser.load();\n\n // Seed from the demo's initial ghosts, mirroring how the demo\n // pipeline seeds its tracker. (A live relay connection has no\n // initial block — it sees every create from connect onward.)\n const accumulator = new GhostStateAccumulator();\n accumulator.applyPacket({\n events: [],\n ghosts: initialBlock.initialGhosts,\n } as unknown as PacketData);\n for (let i = 0; i < cutover; i++) {\n const block = parser.nextBlock();\n if (!block) break;\n if (block.type === BlockTypePacket && block.parsed) {\n accumulator.applyPacket(block.parsed as PacketData);\n }\n }\n\n const source = parser.getPacketParser();\n const tracker = parser.getGhostTracker();\n\n // The accumulator must track exactly the ghosts the parser knows.\n const trackerEntries = [...tracker.getAllGhosts().entries()];\n expect(accumulator.size()).toBe(trackerEntries.length);\n const seedByIndex = new Map(\n accumulator\n .getGhostSeeds()\n .map((seed) => [seed.index, seed.classId]),\n );\n for (const [index, entry] of trackerEntries) {\n expect(seedByIndex.get(index)).toBe(entry.classId);\n }\n\n const rejectedAtCutover = source.protocolRejected;\n\n const seeded = createLiveParser({\n dataBlocks: source.getDataBlockDataMap() ?? [],\n ghosts: trackerEntries.map(([index, entry]) => ({\n index,\n classId: entry.classId,\n })),\n connectionProtocolState: source.getConnectionProtocolState(),\n nextRecvEventSeq: source.getNextRecvEventSeq(),\n compressionPoint: source.getCompressionPoint(),\n pendingGuaranteedEvents: source.getPendingGuaranteedEvents(),\n });\n\n let compared = 0;\n for (let i = 0; i < MAX_CONTINUE_BLOCKS; i++) {\n const block = parser.nextBlock();\n if (!block) break;\n if (block.type === BlockTypeSendPacket) {\n seeded.packetParser.onSendPacketTrigger();\n } else if (block.type === BlockTypePacket) {\n let seededParsed: PacketData | undefined;\n try {\n seededParsed = seeded.packetParser.parsePacket(block.data);\n } catch {\n seededParsed = undefined;\n }\n // Fast path: string compare; fall back to toEqual for a diff.\n if (\n JSON.stringify(seededParsed) !== JSON.stringify(block.parsed)\n ) {\n expect(seededParsed).toEqual(block.parsed);\n }\n compared++;\n }\n }\n\n expect(compared).toBeGreaterThan(1_000);\n expect(seeded.packetParser.ghostsTrackerDiverged).toBe(0);\n expect(seeded.packetParser.ghostsFailed).toBe(0);\n // Both parsers see identical headers from identical state, so\n // rejects (if any) must match the source parser's count delta.\n expect(seeded.packetParser.protocolRejected).toBe(\n source.protocolRejected - rejectedAtCutover,\n );\n },\n );\n }\n }\n});\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GhostTracker.test.d.ts","sourceRoot":"","sources":["../src/GhostTracker.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { GhostTracker } from "./GhostManager.js";
|
|
3
|
-
describe("GhostTracker", () => {
|
|
4
|
-
it("creates and retrieves ghosts", () => {
|
|
5
|
-
const tracker = new GhostTracker();
|
|
6
|
-
tracker.createGhost(42, 25, "Player");
|
|
7
|
-
expect(tracker.hasGhost(42)).toBe(true);
|
|
8
|
-
const ghost = tracker.getGhost(42);
|
|
9
|
-
expect(ghost).toBeDefined();
|
|
10
|
-
expect(ghost.classId).toBe(25);
|
|
11
|
-
expect(ghost.className).toBe("Player");
|
|
12
|
-
expect(ghost.state).toEqual({});
|
|
13
|
-
});
|
|
14
|
-
it("returns undefined for non-existent ghosts", () => {
|
|
15
|
-
const tracker = new GhostTracker();
|
|
16
|
-
expect(tracker.getGhost(0)).toBeUndefined();
|
|
17
|
-
expect(tracker.hasGhost(0)).toBe(false);
|
|
18
|
-
});
|
|
19
|
-
it("deletes ghosts", () => {
|
|
20
|
-
const tracker = new GhostTracker();
|
|
21
|
-
tracker.createGhost(10, 5, "Debris");
|
|
22
|
-
expect(tracker.hasGhost(10)).toBe(true);
|
|
23
|
-
tracker.deleteGhost(10);
|
|
24
|
-
expect(tracker.hasGhost(10)).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
it("overwrites ghost at same index (index reuse)", () => {
|
|
27
|
-
const tracker = new GhostTracker();
|
|
28
|
-
tracker.createGhost(7, 25, "Player");
|
|
29
|
-
tracker.createGhost(7, 4, "Camera");
|
|
30
|
-
const ghost = tracker.getGhost(7);
|
|
31
|
-
expect(ghost.classId).toBe(4);
|
|
32
|
-
expect(ghost.className).toBe("Camera");
|
|
33
|
-
});
|
|
34
|
-
it("tracks size correctly", () => {
|
|
35
|
-
const tracker = new GhostTracker();
|
|
36
|
-
expect(tracker.size()).toBe(0);
|
|
37
|
-
tracker.createGhost(0, 25, "Player");
|
|
38
|
-
tracker.createGhost(1, 4, "Camera");
|
|
39
|
-
expect(tracker.size()).toBe(2);
|
|
40
|
-
tracker.deleteGhost(0);
|
|
41
|
-
expect(tracker.size()).toBe(1);
|
|
42
|
-
});
|
|
43
|
-
it("clears all ghosts", () => {
|
|
44
|
-
const tracker = new GhostTracker();
|
|
45
|
-
tracker.createGhost(0, 25, "Player");
|
|
46
|
-
tracker.createGhost(1, 4, "Camera");
|
|
47
|
-
tracker.createGhost(2, 5, "Debris");
|
|
48
|
-
tracker.clear();
|
|
49
|
-
expect(tracker.size()).toBe(0);
|
|
50
|
-
expect(tracker.hasGhost(0)).toBe(false);
|
|
51
|
-
expect(tracker.hasGhost(1)).toBe(false);
|
|
52
|
-
expect(tracker.hasGhost(2)).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
it("getAllGhosts returns the internal map", () => {
|
|
55
|
-
const tracker = new GhostTracker();
|
|
56
|
-
tracker.createGhost(0, 25, "Player");
|
|
57
|
-
tracker.createGhost(1, 4, "Camera");
|
|
58
|
-
const all = tracker.getAllGhosts();
|
|
59
|
-
expect(all.size).toBe(2);
|
|
60
|
-
expect(all.get(0)?.className).toBe("Player");
|
|
61
|
-
expect(all.get(1)?.className).toBe("Camera");
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
//# sourceMappingURL=GhostTracker.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GhostTracker.test.js","sourceRoot":"","sources":["../src/GhostTracker.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { GhostTracker } from \"./GhostManager.js\";\n\ndescribe(\"GhostTracker\", () => {\n it(\"creates and retrieves ghosts\", () => {\n const tracker = new GhostTracker();\n tracker.createGhost(42, 25, \"Player\");\n\n expect(tracker.hasGhost(42)).toBe(true);\n const ghost = tracker.getGhost(42);\n expect(ghost).toBeDefined();\n expect(ghost!.classId).toBe(25);\n expect(ghost!.className).toBe(\"Player\");\n expect(ghost!.state).toEqual({});\n });\n\n it(\"returns undefined for non-existent ghosts\", () => {\n const tracker = new GhostTracker();\n expect(tracker.getGhost(0)).toBeUndefined();\n expect(tracker.hasGhost(0)).toBe(false);\n });\n\n it(\"deletes ghosts\", () => {\n const tracker = new GhostTracker();\n tracker.createGhost(10, 5, \"Debris\");\n expect(tracker.hasGhost(10)).toBe(true);\n\n tracker.deleteGhost(10);\n expect(tracker.hasGhost(10)).toBe(false);\n });\n\n it(\"overwrites ghost at same index (index reuse)\", () => {\n const tracker = new GhostTracker();\n tracker.createGhost(7, 25, \"Player\");\n tracker.createGhost(7, 4, \"Camera\");\n\n const ghost = tracker.getGhost(7);\n expect(ghost!.classId).toBe(4);\n expect(ghost!.className).toBe(\"Camera\");\n });\n\n it(\"tracks size correctly\", () => {\n const tracker = new GhostTracker();\n expect(tracker.size()).toBe(0);\n\n tracker.createGhost(0, 25, \"Player\");\n tracker.createGhost(1, 4, \"Camera\");\n expect(tracker.size()).toBe(2);\n\n tracker.deleteGhost(0);\n expect(tracker.size()).toBe(1);\n });\n\n it(\"clears all ghosts\", () => {\n const tracker = new GhostTracker();\n tracker.createGhost(0, 25, \"Player\");\n tracker.createGhost(1, 4, \"Camera\");\n tracker.createGhost(2, 5, \"Debris\");\n\n tracker.clear();\n expect(tracker.size()).toBe(0);\n expect(tracker.hasGhost(0)).toBe(false);\n expect(tracker.hasGhost(1)).toBe(false);\n expect(tracker.hasGhost(2)).toBe(false);\n });\n\n it(\"getAllGhosts returns the internal map\", () => {\n const tracker = new GhostTracker();\n tracker.createGhost(0, 25, \"Player\");\n tracker.createGhost(1, 4, \"Camera\");\n\n const all = tracker.getAllGhosts();\n expect(all.size).toBe(2);\n expect(all.get(0)?.className).toBe(\"Player\");\n expect(all.get(1)?.className).toBe(\"Camera\");\n });\n});\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"IncrementalParser.test.d.ts","sourceRoot":"","sources":["../src/IncrementalParser.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import * as fs from "fs";
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
import * as crypto from "crypto";
|
|
5
|
-
import { DemoParser } from "./DemoParser.js";
|
|
6
|
-
import { BlockTypeMove } from "./types.js";
|
|
7
|
-
/**
|
|
8
|
-
* Incremental (progressive download) mode must be byte-for-byte
|
|
9
|
-
* equivalent to the one-shot parse: same header, same initial block,
|
|
10
|
-
* same block sequence, regardless of how the compressed stream is
|
|
11
|
-
* chunked — and the frontier (undefined from nextBlock() before
|
|
12
|
-
* finish()) must be a pause, never an error or a skipped block.
|
|
13
|
-
*/
|
|
14
|
-
const DEMO_DIR = path.resolve(import.meta.dirname, "..", "data");
|
|
15
|
-
const DEMO_FILE = "exogen_Katabatic_vpad.rec";
|
|
16
|
-
const LARGER_DEMO_FILE = "auto-capture_2025-05-16_05-04_DemoBot-Mia_CTFGame_Firestorm.rec";
|
|
17
|
-
function readDemo(file) {
|
|
18
|
-
const buf = fs.readFileSync(path.join(DEMO_DIR, file));
|
|
19
|
-
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
20
|
-
}
|
|
21
|
-
function blockDigest(block, hash) {
|
|
22
|
-
hash.update(Uint8Array.of(block.type, block.size & 0xff, block.size >> 8));
|
|
23
|
-
hash.update(block.data);
|
|
24
|
-
}
|
|
25
|
-
/** Drain all blocks, returning a digest + count of the full sequence. */
|
|
26
|
-
function drainAll(parser) {
|
|
27
|
-
const hash = crypto.createHash("sha1");
|
|
28
|
-
let count = 0;
|
|
29
|
-
let block;
|
|
30
|
-
while ((block = parser.nextBlock())) {
|
|
31
|
-
blockDigest(block, hash);
|
|
32
|
-
count++;
|
|
33
|
-
}
|
|
34
|
-
return { count, digest: hash.digest("hex") };
|
|
35
|
-
}
|
|
36
|
-
async function fullParse(bytes) {
|
|
37
|
-
const parser = new DemoParser(bytes);
|
|
38
|
-
const { header, initialBlock } = await parser.load();
|
|
39
|
-
return { parser, header, initialBlock, ...drainAll(parser) };
|
|
40
|
-
}
|
|
41
|
-
async function incrementalParse(bytes, chunkSize) {
|
|
42
|
-
const { byteLength, header } = DemoParser.peekHeader(bytes);
|
|
43
|
-
const prefixEnd = byteLength + header.initialBlockSize;
|
|
44
|
-
// Prefix must contain the whole initial block; deliberately include a
|
|
45
|
-
// ragged extra sliver so the constructor-tail push path is exercised.
|
|
46
|
-
const sliver = 7;
|
|
47
|
-
const parser = new DemoParser(bytes.subarray(0, prefixEnd + sliver), {
|
|
48
|
-
incremental: true,
|
|
49
|
-
});
|
|
50
|
-
const loaded = await parser.load();
|
|
51
|
-
const hash = crypto.createHash("sha1");
|
|
52
|
-
let count = 0;
|
|
53
|
-
for (let at = prefixEnd + sliver; at < bytes.length; at += chunkSize) {
|
|
54
|
-
parser.push(bytes.subarray(at, Math.min(at + chunkSize, bytes.length)));
|
|
55
|
-
// Interleave draining with pushing — the production consumer steps
|
|
56
|
-
// between network chunks.
|
|
57
|
-
let block;
|
|
58
|
-
while ((block = parser.nextBlock())) {
|
|
59
|
-
blockDigest(block, hash);
|
|
60
|
-
count++;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
parser.finish();
|
|
64
|
-
let block;
|
|
65
|
-
while ((block = parser.nextBlock())) {
|
|
66
|
-
blockDigest(block, hash);
|
|
67
|
-
count++;
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
parser,
|
|
71
|
-
header: loaded.header,
|
|
72
|
-
initialBlock: loaded.initialBlock,
|
|
73
|
-
count,
|
|
74
|
-
digest: hash.digest("hex"),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
describe("incremental DemoParser", () => {
|
|
78
|
-
it("peekHeader matches load()'s header and rejects short prefixes", async () => {
|
|
79
|
-
const bytes = readDemo(DEMO_FILE);
|
|
80
|
-
const { header, byteLength } = DemoParser.peekHeader(bytes);
|
|
81
|
-
const full = new DemoParser(bytes);
|
|
82
|
-
const loaded = await full.load();
|
|
83
|
-
expect(header).toEqual(loaded.header);
|
|
84
|
-
expect(byteLength).toBeGreaterThan(12);
|
|
85
|
-
expect(() => DemoParser.peekHeader(bytes.subarray(0, 0))).toThrow(RangeError);
|
|
86
|
-
expect(() => DemoParser.peekHeader(bytes.subarray(0, byteLength - 1))).toThrow(RangeError);
|
|
87
|
-
});
|
|
88
|
-
for (const chunkSize of [4096, 65521, 1 << 20]) {
|
|
89
|
-
it(`chunked feed (${chunkSize}B) is block-for-block identical to one-shot`, async () => {
|
|
90
|
-
const bytes = readDemo(DEMO_FILE);
|
|
91
|
-
const oneShot = await fullParse(bytes);
|
|
92
|
-
const inc = await incrementalParse(bytes, chunkSize);
|
|
93
|
-
expect(inc.header).toEqual(oneShot.header);
|
|
94
|
-
expect(inc.initialBlock.missionName).toEqual(oneShot.initialBlock.missionName);
|
|
95
|
-
expect(inc.count).toBe(oneShot.count);
|
|
96
|
-
expect(inc.digest).toBe(oneShot.digest);
|
|
97
|
-
expect(inc.parser.isComplete).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
it("larger demo: chunked feed matches one-shot", async () => {
|
|
101
|
-
const bytes = readDemo(LARGER_DEMO_FILE);
|
|
102
|
-
const oneShot = await fullParse(bytes);
|
|
103
|
-
const inc = await incrementalParse(bytes, 128 * 1024);
|
|
104
|
-
expect(inc.count).toBe(oneShot.count);
|
|
105
|
-
expect(inc.digest).toBe(oneShot.digest);
|
|
106
|
-
});
|
|
107
|
-
it("treats the frontier as a pause: half-feed stalls, resumes, completes", async () => {
|
|
108
|
-
const bytes = readDemo(DEMO_FILE);
|
|
109
|
-
const oneShot = await fullParse(bytes);
|
|
110
|
-
const { byteLength, header } = DemoParser.peekHeader(bytes);
|
|
111
|
-
const prefixEnd = byteLength + header.initialBlockSize;
|
|
112
|
-
// Split the COMPRESSED REGION in half (the initial block may be a
|
|
113
|
-
// large share of a client-recorded demo, so file-halving could land
|
|
114
|
-
// inside it and feed zero compressed bytes).
|
|
115
|
-
const half = prefixEnd + Math.floor((bytes.length - prefixEnd) / 2);
|
|
116
|
-
const parser = new DemoParser(bytes.subarray(0, prefixEnd), {
|
|
117
|
-
incremental: true,
|
|
118
|
-
});
|
|
119
|
-
await parser.load();
|
|
120
|
-
parser.push(bytes.subarray(prefixEnd, half));
|
|
121
|
-
const hash = crypto.createHash("sha1");
|
|
122
|
-
let count = 0;
|
|
123
|
-
let block;
|
|
124
|
-
while ((block = parser.nextBlock())) {
|
|
125
|
-
blockDigest(block, hash);
|
|
126
|
-
count++;
|
|
127
|
-
}
|
|
128
|
-
// Stalled at the frontier, not ended.
|
|
129
|
-
expect(parser.isComplete).toBe(false);
|
|
130
|
-
expect(count).toBeGreaterThan(0);
|
|
131
|
-
expect(count).toBeLessThan(oneShot.count);
|
|
132
|
-
// Repeated probing at the frontier is safe and yields nothing.
|
|
133
|
-
expect(parser.nextBlock()).toBeUndefined();
|
|
134
|
-
expect(parser.nextBlock()).toBeUndefined();
|
|
135
|
-
parser.push(bytes.subarray(half));
|
|
136
|
-
parser.finish();
|
|
137
|
-
while ((block = parser.nextBlock())) {
|
|
138
|
-
blockDigest(block, hash);
|
|
139
|
-
count++;
|
|
140
|
-
}
|
|
141
|
-
expect(parser.isComplete).toBe(true);
|
|
142
|
-
expect(count).toBe(oneShot.count);
|
|
143
|
-
expect(hash.digest("hex")).toBe(oneShot.digest);
|
|
144
|
-
});
|
|
145
|
-
it("reset() replays the grown stream from the start (backward seek)", async () => {
|
|
146
|
-
const bytes = readDemo(DEMO_FILE);
|
|
147
|
-
const oneShot = await fullParse(bytes);
|
|
148
|
-
const { byteLength, header } = DemoParser.peekHeader(bytes);
|
|
149
|
-
const prefixEnd = byteLength + header.initialBlockSize;
|
|
150
|
-
const parser = new DemoParser(bytes.subarray(0, prefixEnd), {
|
|
151
|
-
incremental: true,
|
|
152
|
-
});
|
|
153
|
-
await parser.load();
|
|
154
|
-
for (let at = prefixEnd; at < bytes.length; at += 64 * 1024) {
|
|
155
|
-
parser.push(bytes.subarray(at, Math.min(at + 64 * 1024, bytes.length)));
|
|
156
|
-
}
|
|
157
|
-
parser.finish();
|
|
158
|
-
// Consume some, reset, then a full drain must equal the one-shot.
|
|
159
|
-
for (let i = 0; i < 100; i++)
|
|
160
|
-
parser.nextBlock();
|
|
161
|
-
parser.reset();
|
|
162
|
-
const replay = drainAll(parser);
|
|
163
|
-
expect(replay.count).toBe(oneShot.count);
|
|
164
|
-
expect(replay.digest).toBe(oneShot.digest);
|
|
165
|
-
});
|
|
166
|
-
it("rejects misuse: push on one-shot parsers, before load, after finish", async () => {
|
|
167
|
-
const bytes = readDemo(DEMO_FILE);
|
|
168
|
-
const oneShot = new DemoParser(bytes);
|
|
169
|
-
await oneShot.load();
|
|
170
|
-
expect(() => oneShot.push(new Uint8Array(1))).toThrow(/incremental/);
|
|
171
|
-
expect(oneShot.isComplete).toBe(true);
|
|
172
|
-
const { byteLength, header } = DemoParser.peekHeader(bytes);
|
|
173
|
-
const prefixEnd = byteLength + header.initialBlockSize;
|
|
174
|
-
const unloaded = new DemoParser(bytes.subarray(0, prefixEnd), {
|
|
175
|
-
incremental: true,
|
|
176
|
-
});
|
|
177
|
-
expect(() => unloaded.push(new Uint8Array(1))).toThrow(/load/);
|
|
178
|
-
const parser = new DemoParser(bytes.subarray(0, prefixEnd), {
|
|
179
|
-
incremental: true,
|
|
180
|
-
});
|
|
181
|
-
await parser.load();
|
|
182
|
-
parser.push(bytes.subarray(prefixEnd));
|
|
183
|
-
parser.finish();
|
|
184
|
-
expect(() => parser.push(new Uint8Array(1))).toThrow(/finished/);
|
|
185
|
-
// finish() is idempotent.
|
|
186
|
-
parser.finish();
|
|
187
|
-
});
|
|
188
|
-
it("bufferedMoveTicks counts move ticks exactly, in both modes", async () => {
|
|
189
|
-
const bytes = readDemo(DEMO_FILE);
|
|
190
|
-
// Ground truth: count Move blocks by draining a one-shot parse.
|
|
191
|
-
const oneShot = new DemoParser(bytes);
|
|
192
|
-
await oneShot.load();
|
|
193
|
-
let moves = 0;
|
|
194
|
-
let block;
|
|
195
|
-
while ((block = oneShot.nextBlock())) {
|
|
196
|
-
if (block.type === BlockTypeMove)
|
|
197
|
-
moves++;
|
|
198
|
-
}
|
|
199
|
-
expect(oneShot.bufferedMoveTicks).toBe(moves);
|
|
200
|
-
const { byteLength, header } = DemoParser.peekHeader(bytes);
|
|
201
|
-
const prefixEnd = byteLength + header.initialBlockSize;
|
|
202
|
-
const parser = new DemoParser(bytes.subarray(0, prefixEnd), {
|
|
203
|
-
incremental: true,
|
|
204
|
-
});
|
|
205
|
-
await parser.load();
|
|
206
|
-
let last = 0;
|
|
207
|
-
for (let at = prefixEnd; at < bytes.length; at += 32 * 1024) {
|
|
208
|
-
parser.push(bytes.subarray(at, Math.min(at + 32 * 1024, bytes.length)));
|
|
209
|
-
const now = parser.bufferedMoveTicks;
|
|
210
|
-
expect(now).toBeGreaterThanOrEqual(last); // monotonic
|
|
211
|
-
last = now;
|
|
212
|
-
}
|
|
213
|
-
parser.finish();
|
|
214
|
-
expect(parser.bufferedMoveTicks).toBe(moves);
|
|
215
|
-
// Unaffected by the read cursor.
|
|
216
|
-
parser.reset();
|
|
217
|
-
expect(parser.bufferedMoveTicks).toBe(moves);
|
|
218
|
-
});
|
|
219
|
-
it("requires the full initial block up front", async () => {
|
|
220
|
-
const bytes = readDemo(DEMO_FILE);
|
|
221
|
-
const { byteLength, header } = DemoParser.peekHeader(bytes);
|
|
222
|
-
const short = new DemoParser(bytes.subarray(0, byteLength + header.initialBlockSize - 1), { incremental: true });
|
|
223
|
-
await expect(short.load()).rejects.toThrow(RangeError);
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
//# sourceMappingURL=IncrementalParser.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"IncrementalParser.test.js","sourceRoot":"","sources":["../src/IncrementalParser.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as crypto from \"crypto\";\nimport { DemoParser } from \"./DemoParser.js\";\nimport { BlockTypeMove } from \"./types.js\";\nimport type { DemoBlock } from \"./types.js\";\n\n/**\n * Incremental (progressive download) mode must be byte-for-byte\n * equivalent to the one-shot parse: same header, same initial block,\n * same block sequence, regardless of how the compressed stream is\n * chunked — and the frontier (undefined from nextBlock() before\n * finish()) must be a pause, never an error or a skipped block.\n */\n\nconst DEMO_DIR = path.resolve(import.meta.dirname, \"..\", \"data\");\nconst DEMO_FILE = \"exogen_Katabatic_vpad.rec\";\nconst LARGER_DEMO_FILE =\n \"auto-capture_2025-05-16_05-04_DemoBot-Mia_CTFGame_Firestorm.rec\";\n\nfunction readDemo(file: string): Uint8Array {\n const buf = fs.readFileSync(path.join(DEMO_DIR, file));\n return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);\n}\n\nfunction blockDigest(block: DemoBlock, hash: crypto.Hash): void {\n hash.update(Uint8Array.of(block.type, block.size & 0xff, block.size >> 8));\n hash.update(block.data);\n}\n\n/** Drain all blocks, returning a digest + count of the full sequence. */\nfunction drainAll(parser: DemoParser): { count: number; digest: string } {\n const hash = crypto.createHash(\"sha1\");\n let count = 0;\n let block: DemoBlock | undefined;\n while ((block = parser.nextBlock())) {\n blockDigest(block, hash);\n count++;\n }\n return { count, digest: hash.digest(\"hex\") };\n}\n\nasync function fullParse(bytes: Uint8Array) {\n const parser = new DemoParser(bytes);\n const { header, initialBlock } = await parser.load();\n return { parser, header, initialBlock, ...drainAll(parser) };\n}\n\nasync function incrementalParse(bytes: Uint8Array, chunkSize: number) {\n const { byteLength, header } = DemoParser.peekHeader(bytes);\n const prefixEnd = byteLength + header.initialBlockSize;\n // Prefix must contain the whole initial block; deliberately include a\n // ragged extra sliver so the constructor-tail push path is exercised.\n const sliver = 7;\n const parser = new DemoParser(bytes.subarray(0, prefixEnd + sliver), {\n incremental: true,\n });\n const loaded = await parser.load();\n const hash = crypto.createHash(\"sha1\");\n let count = 0;\n for (let at = prefixEnd + sliver; at < bytes.length; at += chunkSize) {\n parser.push(bytes.subarray(at, Math.min(at + chunkSize, bytes.length)));\n // Interleave draining with pushing — the production consumer steps\n // between network chunks.\n let block: DemoBlock | undefined;\n while ((block = parser.nextBlock())) {\n blockDigest(block, hash);\n count++;\n }\n }\n parser.finish();\n let block: DemoBlock | undefined;\n while ((block = parser.nextBlock())) {\n blockDigest(block, hash);\n count++;\n }\n return {\n parser,\n header: loaded.header,\n initialBlock: loaded.initialBlock,\n count,\n digest: hash.digest(\"hex\"),\n };\n}\n\ndescribe(\"incremental DemoParser\", () => {\n it(\"peekHeader matches load()'s header and rejects short prefixes\", async () => {\n const bytes = readDemo(DEMO_FILE);\n const { header, byteLength } = DemoParser.peekHeader(bytes);\n const full = new DemoParser(bytes);\n const loaded = await full.load();\n expect(header).toEqual(loaded.header);\n expect(byteLength).toBeGreaterThan(12);\n expect(() => DemoParser.peekHeader(bytes.subarray(0, 0))).toThrow(\n RangeError,\n );\n expect(() =>\n DemoParser.peekHeader(bytes.subarray(0, byteLength - 1)),\n ).toThrow(RangeError);\n });\n\n for (const chunkSize of [4096, 65521, 1 << 20]) {\n it(`chunked feed (${chunkSize}B) is block-for-block identical to one-shot`, async () => {\n const bytes = readDemo(DEMO_FILE);\n const oneShot = await fullParse(bytes);\n const inc = await incrementalParse(bytes, chunkSize);\n expect(inc.header).toEqual(oneShot.header);\n expect(inc.initialBlock.missionName).toEqual(\n oneShot.initialBlock.missionName,\n );\n expect(inc.count).toBe(oneShot.count);\n expect(inc.digest).toBe(oneShot.digest);\n expect(inc.parser.isComplete).toBe(true);\n });\n }\n\n it(\"larger demo: chunked feed matches one-shot\", async () => {\n const bytes = readDemo(LARGER_DEMO_FILE);\n const oneShot = await fullParse(bytes);\n const inc = await incrementalParse(bytes, 128 * 1024);\n expect(inc.count).toBe(oneShot.count);\n expect(inc.digest).toBe(oneShot.digest);\n });\n\n it(\"treats the frontier as a pause: half-feed stalls, resumes, completes\", async () => {\n const bytes = readDemo(DEMO_FILE);\n const oneShot = await fullParse(bytes);\n const { byteLength, header } = DemoParser.peekHeader(bytes);\n const prefixEnd = byteLength + header.initialBlockSize;\n // Split the COMPRESSED REGION in half (the initial block may be a\n // large share of a client-recorded demo, so file-halving could land\n // inside it and feed zero compressed bytes).\n const half = prefixEnd + Math.floor((bytes.length - prefixEnd) / 2);\n const parser = new DemoParser(bytes.subarray(0, prefixEnd), {\n incremental: true,\n });\n await parser.load();\n parser.push(bytes.subarray(prefixEnd, half));\n\n const hash = crypto.createHash(\"sha1\");\n let count = 0;\n let block: DemoBlock | undefined;\n while ((block = parser.nextBlock())) {\n blockDigest(block, hash);\n count++;\n }\n // Stalled at the frontier, not ended.\n expect(parser.isComplete).toBe(false);\n expect(count).toBeGreaterThan(0);\n expect(count).toBeLessThan(oneShot.count);\n // Repeated probing at the frontier is safe and yields nothing.\n expect(parser.nextBlock()).toBeUndefined();\n expect(parser.nextBlock()).toBeUndefined();\n\n parser.push(bytes.subarray(half));\n parser.finish();\n while ((block = parser.nextBlock())) {\n blockDigest(block, hash);\n count++;\n }\n expect(parser.isComplete).toBe(true);\n expect(count).toBe(oneShot.count);\n expect(hash.digest(\"hex\")).toBe(oneShot.digest);\n });\n\n it(\"reset() replays the grown stream from the start (backward seek)\", async () => {\n const bytes = readDemo(DEMO_FILE);\n const oneShot = await fullParse(bytes);\n const { byteLength, header } = DemoParser.peekHeader(bytes);\n const prefixEnd = byteLength + header.initialBlockSize;\n const parser = new DemoParser(bytes.subarray(0, prefixEnd), {\n incremental: true,\n });\n await parser.load();\n for (let at = prefixEnd; at < bytes.length; at += 64 * 1024) {\n parser.push(bytes.subarray(at, Math.min(at + 64 * 1024, bytes.length)));\n }\n parser.finish();\n // Consume some, reset, then a full drain must equal the one-shot.\n for (let i = 0; i < 100; i++) parser.nextBlock();\n parser.reset();\n const replay = drainAll(parser);\n expect(replay.count).toBe(oneShot.count);\n expect(replay.digest).toBe(oneShot.digest);\n });\n\n it(\"rejects misuse: push on one-shot parsers, before load, after finish\", async () => {\n const bytes = readDemo(DEMO_FILE);\n const oneShot = new DemoParser(bytes);\n await oneShot.load();\n expect(() => oneShot.push(new Uint8Array(1))).toThrow(/incremental/);\n expect(oneShot.isComplete).toBe(true);\n\n const { byteLength, header } = DemoParser.peekHeader(bytes);\n const prefixEnd = byteLength + header.initialBlockSize;\n const unloaded = new DemoParser(bytes.subarray(0, prefixEnd), {\n incremental: true,\n });\n expect(() => unloaded.push(new Uint8Array(1))).toThrow(/load/);\n\n const parser = new DemoParser(bytes.subarray(0, prefixEnd), {\n incremental: true,\n });\n await parser.load();\n parser.push(bytes.subarray(prefixEnd));\n parser.finish();\n expect(() => parser.push(new Uint8Array(1))).toThrow(/finished/);\n // finish() is idempotent.\n parser.finish();\n });\n\n it(\"bufferedMoveTicks counts move ticks exactly, in both modes\", async () => {\n const bytes = readDemo(DEMO_FILE);\n // Ground truth: count Move blocks by draining a one-shot parse.\n const oneShot = new DemoParser(bytes);\n await oneShot.load();\n let moves = 0;\n let block;\n while ((block = oneShot.nextBlock())) {\n if (block.type === BlockTypeMove) moves++;\n }\n expect(oneShot.bufferedMoveTicks).toBe(moves);\n\n const { byteLength, header } = DemoParser.peekHeader(bytes);\n const prefixEnd = byteLength + header.initialBlockSize;\n const parser = new DemoParser(bytes.subarray(0, prefixEnd), {\n incremental: true,\n });\n await parser.load();\n let last = 0;\n for (let at = prefixEnd; at < bytes.length; at += 32 * 1024) {\n parser.push(bytes.subarray(at, Math.min(at + 32 * 1024, bytes.length)));\n const now = parser.bufferedMoveTicks;\n expect(now).toBeGreaterThanOrEqual(last); // monotonic\n last = now;\n }\n parser.finish();\n expect(parser.bufferedMoveTicks).toBe(moves);\n // Unaffected by the read cursor.\n parser.reset();\n expect(parser.bufferedMoveTicks).toBe(moves);\n });\n\n it(\"requires the full initial block up front\", async () => {\n const bytes = readDemo(DEMO_FILE);\n const { byteLength, header } = DemoParser.peekHeader(bytes);\n const short = new DemoParser(\n bytes.subarray(0, byteLength + header.initialBlockSize - 1),\n { incremental: true },\n );\n await expect(short.load()).rejects.toThrow(RangeError);\n });\n});\n"]}
|