t2-demo-parser 2.0.0 → 2.2.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/dist/DataBlockParsers.d.ts.map +1 -1
- package/dist/DataBlockParsers.js +89 -48
- package/dist/DataBlockParsers.js.map +1 -1
- package/dist/DataBlockParsers.test.js +128 -0
- package/dist/DataBlockParsers.test.js.map +1 -1
- package/dist/DemoParser.d.ts +63 -1
- package/dist/DemoParser.d.ts.map +1 -1
- package/dist/DemoParser.js +208 -15
- package/dist/DemoParser.js.map +1 -1
- package/dist/IncrementalParser.test.d.ts +2 -0
- package/dist/IncrementalParser.test.d.ts.map +1 -0
- package/dist/IncrementalParser.test.js +226 -0
- package/dist/IncrementalParser.test.js.map +1 -0
- package/dist/dataBlockDataTypes.d.ts +54 -29
- package/dist/dataBlockDataTypes.d.ts.map +1 -1
- package/dist/dataBlockDataTypes.js.map +1 -1
- package/package.json +1 -1
|
@@ -62,6 +62,134 @@ describe("projectile datablock field decoding", () => {
|
|
|
62
62
|
// disc.cs: reflectOnWaterImpactAngle = 15.0.
|
|
63
63
|
expect(disc.reflectOnWaterImpactAngle).toBe(15);
|
|
64
64
|
});
|
|
65
|
+
it("decodes retail EnergyBolt (blaster) fields exactly", async () => {
|
|
66
|
+
const dataBlocks = await loadDataBlocks("exogen_Katabatic_vpad.rec");
|
|
67
|
+
let bolt;
|
|
68
|
+
for (const [, db] of dataBlocks) {
|
|
69
|
+
if (db.className === "EnergyProjectileData") {
|
|
70
|
+
bolt = db.data;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
expect(bolt, "retail EnergyBolt datablock").toBeDefined();
|
|
75
|
+
// blaster.cs: binary-verified names (initPersistFields FUN_00694b40,
|
|
76
|
+
// unpackData FUN_00694d80) — these were previously mislabeled with
|
|
77
|
+
// ELF-style beam field names.
|
|
78
|
+
expect(bolt.crossViewAng).toBeCloseTo(0.99, 5);
|
|
79
|
+
expect(bolt.crossSize).toBeCloseTo(0.55, 5);
|
|
80
|
+
expect(bolt.blurLifetime).toBeCloseTo(0.2, 5);
|
|
81
|
+
expect(bolt.blurWidth).toBeCloseTo(0.25, 5);
|
|
82
|
+
expect(bolt.blurColor.r).toBeCloseTo(0.4, 5);
|
|
83
|
+
expect(bolt.blurColor.g).toBeCloseTo(0, 5);
|
|
84
|
+
expect(bolt.blurColor.b).toBeCloseTo(0, 5);
|
|
85
|
+
expect(bolt.texture0).toBe("special/blasterBolt");
|
|
86
|
+
expect(bolt.texture1).toBe("special/blasterBoltCross");
|
|
87
|
+
// blaster.cs: scale = "0.25 20.0 1.0" (bolt quad half-width / length).
|
|
88
|
+
expect(bolt.scale.x).toBeCloseTo(0.25, 5);
|
|
89
|
+
expect(bolt.scale.y).toBeCloseTo(20, 5);
|
|
90
|
+
});
|
|
91
|
+
it("decodes retail BasicSniperShot (laser rifle) fields exactly", async () => {
|
|
92
|
+
const dataBlocks = await loadDataBlocks("exogen_Katabatic_vpad.rec");
|
|
93
|
+
let shot;
|
|
94
|
+
for (const [, db] of dataBlocks) {
|
|
95
|
+
if (db.className === "SniperProjectileData") {
|
|
96
|
+
shot = db.data;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
expect(shot, "retail BasicSniperShot datablock").toBeDefined();
|
|
101
|
+
// sniperRifle.cs values.
|
|
102
|
+
expect(shot.maxRifleRange).toBeCloseTo(1000, 5);
|
|
103
|
+
expect(shot.beamColor.r).toBeCloseTo(1, 2);
|
|
104
|
+
expect(shot.beamColor.g).toBeCloseTo(0.1, 2);
|
|
105
|
+
expect(shot.beamColor.b).toBeCloseTo(0.1, 2);
|
|
106
|
+
expect(shot.fadeTime).toBeCloseTo(1.0, 5);
|
|
107
|
+
expect(shot.startBeamWidth).toBeCloseTo(0.145, 5);
|
|
108
|
+
expect(shot.endBeamWidth).toBeCloseTo(0.25, 5);
|
|
109
|
+
expect(shot.pulseSpeed).toBeCloseTo(6.0, 5);
|
|
110
|
+
expect(shot.pulseLength).toBeCloseTo(0.15, 5);
|
|
111
|
+
expect(shot.textures[0]).toBe("special/flare");
|
|
112
|
+
expect(shot.textures[1]).toBe("special/nonlingradient");
|
|
113
|
+
expect(shot.textures[2]).toBe("special/laserrip01");
|
|
114
|
+
expect(shot.textures[11]).toBe("special/sniper00");
|
|
115
|
+
});
|
|
116
|
+
it("decodes both ELF datablocks (gun and turret) exactly", async () => {
|
|
117
|
+
const dataBlocks = await loadDataBlocks("exogen_Katabatic_vpad.rec");
|
|
118
|
+
const elfs = [];
|
|
119
|
+
for (const [, db] of dataBlocks) {
|
|
120
|
+
if (db.className === "ELFProjectileData") {
|
|
121
|
+
elfs.push(db.data);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Binary-verified names (initPersistFields FUN_0064a860, unpackData
|
|
125
|
+
// FUN_0064ae00) — previously fabricated beam names. Two datablocks
|
|
126
|
+
// exist: the handheld BasicELF at beamRange 38 (the z0dd/ZOD server
|
|
127
|
+
// patch every surviving server lineage runs — "WHAT?? INCREASE ELF
|
|
128
|
+
// RANGE?!!? was 37"; the stock script says 30) and the stock
|
|
129
|
+
// ELFTurretBolt at 75 (ELFBarrelLarge.cs).
|
|
130
|
+
expect(elfs.map((e) => e.beamRange).sort((a, b) => a - b)).toEqual([
|
|
131
|
+
38, 75,
|
|
132
|
+
]);
|
|
133
|
+
for (const elf of elfs) {
|
|
134
|
+
expect(elf.mainBeamWidth).toBeCloseTo(0.1, 5);
|
|
135
|
+
expect(elf.mainBeamSpeed).toBeCloseTo(9.0, 5);
|
|
136
|
+
expect(elf.mainBeamRepeat).toBeCloseTo(0.25, 5);
|
|
137
|
+
expect(elf.lightningWidth).toBeCloseTo(0.1, 5);
|
|
138
|
+
expect(elf.lightningDist).toBeCloseTo(0.15, 5);
|
|
139
|
+
expect(elf.textures).toEqual([
|
|
140
|
+
"special/ELFBeam",
|
|
141
|
+
"special/ELFLightning",
|
|
142
|
+
"special/BlueImpact",
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
it("decodes retail DefaultRepairBeam fields exactly", async () => {
|
|
147
|
+
const dataBlocks = await loadDataBlocks("exogen_Katabatic_vpad.rec");
|
|
148
|
+
let repair;
|
|
149
|
+
for (const [, db] of dataBlocks) {
|
|
150
|
+
if (db.className === "RepairProjectileData") {
|
|
151
|
+
repair = db.data;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
expect(repair, "retail DefaultRepairBeam datablock").toBeDefined();
|
|
156
|
+
// repairpack.cs: binary-verified names (initPersistFields
|
|
157
|
+
// FUN_00644910, unpackData FUN_00644c40); numSegments is an S32
|
|
158
|
+
// whose raw bits the old decode read as a float.
|
|
159
|
+
expect(repair.beamRange).toBeCloseTo(10, 5);
|
|
160
|
+
expect(repair.beamWidth).toBeCloseTo(0.15, 5);
|
|
161
|
+
expect(repair.numSegments).toBe(20);
|
|
162
|
+
expect(repair.texRepeat).toBeCloseTo(0.2, 5);
|
|
163
|
+
expect(repair.blurFreq).toBeCloseTo(10.0, 5);
|
|
164
|
+
expect(repair.blurLifetime).toBeCloseTo(1.0, 5);
|
|
165
|
+
expect(repair.cutoffAngle).toBeCloseTo(25.0, 5);
|
|
166
|
+
expect(repair.textures).toEqual(["special/redbump2", "special/redflare"]);
|
|
167
|
+
});
|
|
168
|
+
it("decodes image-state transitions in the engine packing order", async () => {
|
|
169
|
+
// Every retail weapon image opens Activate → (timeout) →
|
|
170
|
+
// ActivateReady (e.g. blaster.cs stateTransitionOnTimeout[0]).
|
|
171
|
+
// With the old rotated names, the timeout transition surfaced under
|
|
172
|
+
// transitionGeneric0Out and consumers carried a remap table.
|
|
173
|
+
const dataBlocks = await loadDataBlocks("exogen_Katabatic_vpad.rec");
|
|
174
|
+
let checked = 0;
|
|
175
|
+
for (const [, db] of dataBlocks) {
|
|
176
|
+
if (db.className !== "ShapeBaseImageData")
|
|
177
|
+
continue;
|
|
178
|
+
const data = db.data;
|
|
179
|
+
const states = data.states;
|
|
180
|
+
if (!states || states.length < 2)
|
|
181
|
+
continue;
|
|
182
|
+
if (states[0].name.toLowerCase() !== "activate")
|
|
183
|
+
continue;
|
|
184
|
+
const readyIndex = states.findIndex((st) => st.name.toLowerCase() === "activateready");
|
|
185
|
+
if (readyIndex < 0)
|
|
186
|
+
continue;
|
|
187
|
+
// Transition values are 1-based state indices (0 = none).
|
|
188
|
+
expect(states[0].transitionOnTimeout).toBe(readyIndex + 1);
|
|
189
|
+
checked++;
|
|
190
|
+
}
|
|
191
|
+
expect(checked).toBeGreaterThan(3);
|
|
192
|
+
});
|
|
65
193
|
it("decodes retail ChaingunBullet tracer fields exactly", async () => {
|
|
66
194
|
const dataBlocks = await loadDataBlocks("exogen_Katabatic_vpad.rec");
|
|
67
195
|
let bullet;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataBlockParsers.test.js","sourceRoot":"","sources":["../src/DataBlockParsers.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 { DemoParser } from \"./DemoParser.js\";\nimport type {\n LinearProjectileDataBlock,\n PlayerDataBlock,\n TracerProjectileDataBlock,\n} from \"./dataBlockDataTypes.js\";\n\nconst DEMO_DIR = path.resolve(import.meta.dirname, \"..\", \"data\");\n\nasync function loadDataBlocks(file: string) {\n const buffer = fs.readFileSync(path.join(DEMO_DIR, file));\n const parser = new DemoParser(new Uint8Array(buffer));\n const { initialBlock } = await parser.load();\n return initialBlock.dataBlocks;\n}\n\n/**\n * Regression tests for projectile datablock field naming, which was\n * binary-verified against Tribes2.exe build 25034 (initPersistFields\n * FUN_0062b3c0 / FUN_0063fcb0 map names to the struct offsets that\n * unpackData reads in order). Prior to that verification several fields\n * were misassigned — most damagingly, the true lifetimeMS landed in a\n * field named `fizzleType`, breaking client-side projectile simulation.\n */\ndescribe(\"projectile datablock field decoding\", () => {\n it(\"decodes LinearProjectileData fields with engine semantics\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n const linears: LinearProjectileDataBlock[] = [];\n for (const [, db] of dataBlocks) {\n if (\n db.className === \"LinearProjectileData\" ||\n db.className === \"TracerProjectileData\" ||\n db.className === \"LinearFlareProjectileData\"\n ) {\n linears.push(db.data as LinearProjectileDataBlock);\n }\n }\n expect(linears.length).toBeGreaterThan(0);\n\n for (const block of linears) {\n // lifetimeMS/fizzleTimeMS are real milliseconds, rounded up to a\n // 32ms tick by the engine's onAdd, and clamped to 511 ticks.\n expect(block.lifetimeMS! % 32).toBe(0);\n expect(block.lifetimeMS).toBeGreaterThanOrEqual(32);\n expect(block.lifetimeMS).toBeLessThanOrEqual(511 * 32);\n expect(block.fizzleTimeMS! % 32).toBe(0);\n // Angles are ranged 0–90 degrees on the wire.\n expect(block.reflectOnWaterImpactAngle).toBeGreaterThanOrEqual(0);\n expect(block.reflectOnWaterImpactAngle).toBeLessThanOrEqual(90);\n expect(typeof block.explodeOnDeath).toBe(\"boolean\");\n expect(typeof block.doDynamicClientHits).toBe(\"boolean\");\n }\n });\n\n it(\"decodes retail DiscProjectile exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let disc: LinearProjectileDataBlock | undefined;\n for (const [, db] of dataBlocks) {\n const data = db.data as LinearProjectileDataBlock;\n if (\n db.className === \"LinearProjectileData\" &&\n data.dryVelocity === 90 &&\n data.explodeOnDeath === true\n ) {\n disc = data;\n break;\n }\n }\n expect(disc, \"retail DiscProjectile datablock\").toBeDefined();\n // disc.cs: lifetimeMS = fizzleTimeMS = 5000 → tick-rounded to 5024.\n expect(disc!.lifetimeMS).toBe(5024);\n expect(disc!.fizzleTimeMS).toBe(5024);\n // disc.cs: reflectOnWaterImpactAngle = 15.0.\n expect(disc!.reflectOnWaterImpactAngle).toBe(15);\n });\n\n it(\"decodes retail ChaingunBullet tracer fields exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let bullet: TracerProjectileDataBlock | undefined;\n for (const [, db] of dataBlocks) {\n const data = db.data as TracerProjectileDataBlock;\n if (\n db.className === \"TracerProjectileData\" &&\n data.tracerTex0 === \"special/tracer00\" &&\n data.lifetimeMS === 3008\n ) {\n bullet = data;\n break;\n }\n }\n expect(bullet, \"ChaingunBullet datablock\").toBeDefined();\n // Values as recorded in this demo (a lightly modded server — tracer\n // length/width are doubled from retail, everything else matches\n // chaingun.cs). Each assertion pins a decode position: a swap with\n // any neighboring field would produce a nonsensical value here.\n expect(bullet!.dryVelocity).toBe(750);\n expect(bullet!.wetVelocity).toBe(280);\n expect(bullet!.fizzleTimeMS).toBe(3008);\n expect(bullet!.explodeOnDeath).toBe(false);\n // Chainguns are the only retail weapons with doDynamicClientHits.\n expect(bullet!.doDynamicClientHits).toBe(true);\n // activateDelayMS = -1 (retail default) read as U32.\n expect(bullet!.activateDelayMS).toBe(0xffffffff);\n expect(bullet!.tracerLength).toBe(30);\n expect(bullet!.tracerMinPixels).toBe(6);\n expect(bullet!.tracerWidth).toBeCloseTo(0.2, 5);\n expect(bullet!.tracerAlpha).toBe(false);\n // Retail chaingun.cs tracerColor: 211/255, 215/255, 120/255, 0.75.\n expect(bullet!.tracerColor!.r).toBeCloseTo(211 / 255, 2);\n expect(bullet!.tracerColor!.g).toBeCloseTo(215 / 255, 2);\n expect(bullet!.tracerColor!.b).toBeCloseTo(120 / 255, 2);\n expect(bullet!.crossViewAng).toBeCloseTo(0.99, 5);\n expect(bullet!.crossSize).toBeCloseTo(0.2, 5);\n expect(bullet!.renderCross).toBe(true);\n expect(bullet!.tracerTex1).toBe(\"special/tracercross\");\n });\n\n it(\"decodes PlayerData movement/jet fields with engine semantics\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let armor: PlayerDataBlock | undefined;\n for (const [, db] of dataBlocks) {\n const data = db.data as PlayerDataBlock;\n // The armor the recorder wears: retail-value run/speed section.\n if (db.className === \"PlayerData\" && data.maxForwardSpeed === 15) {\n armor = data;\n break;\n }\n }\n expect(armor, \"light armor PlayerData datablock\").toBeDefined();\n // Retail player.cs values — each pins a decode position (previously\n // these landed in fields named jumpForce/runSurfaceAngle/etc.).\n expect(armor!.runForce).toBe(4968); // 55.2 * 90\n expect(armor!.maxBackwardSpeed).toBe(13);\n expect(armor!.maxSideSpeed).toBe(13);\n expect(armor!.underwaterVertJetFactor).toBeCloseTo(1.5, 5);\n expect(armor!.minLookAngle).toBeCloseTo(-1.5, 5);\n expect(armor!.maxLookAngle).toBeCloseTo(1.5, 5);\n // Jet energy fields must decode as plausible per-tick magnitudes\n // (this server mods them slightly, so no exact match): drain is a\n // fraction of maxEnergy(60), threshold is a small energy value.\n expect(armor!.jetEnergyDrain).toBeGreaterThan(0);\n expect(armor!.jetEnergyDrain).toBeLessThan(5);\n expect(armor!.underwaterJetEnergyDrain).toBeGreaterThan(0);\n expect(armor!.underwaterJetEnergyDrain).toBeLessThan(5);\n expect(armor!.minJetEnergy).toBeGreaterThanOrEqual(0);\n expect(armor!.minJetEnergy).toBeLessThan(10);\n // Heat signature rates — retail player.cs: 1/4 and 1/3. These are the\n // first two reads of the ground-impact tail; the old decode shifted\n // the whole section by two slots.\n expect(armor!.heatDecayPerSec).toBeCloseTo(0.25, 5);\n expect(armor!.heatIncreasePerSec).toBeCloseTo(1 / 3, 5);\n expect(armor!.groundImpactShakeDuration).toBeCloseTo(0.6, 5);\n expect(armor!.groundImpactShakeFalloff).toBeCloseTo(10, 5);\n });\n\n it(\"decodes StationFX node names and textures exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let vehicleFX: Record<string, unknown> | undefined;\n let personalFX: Record<string, unknown> | undefined;\n for (const [, db] of dataBlocks) {\n if (db.className === \"StationFXVehicleData\") vehicleFX ??= db.data;\n if (db.className === \"StationFXPersonalData\") personalFX ??= db.data;\n }\n // serverVehicleHud.cs VehicleInvFX values.\n expect(vehicleFX, \"StationFXVehicleData\").toBeDefined();\n expect(vehicleFX!.glowNodeName).toBe(\"GLOWFX\");\n expect(vehicleFX!.leftNodeName0).toBe(\"LFX1\");\n expect(vehicleFX!.rightNodeName0).toBe(\"RFX1\");\n expect(vehicleFX!.leftNodeName3).toBe(\"LFX4\");\n expect(vehicleFX!.rightNodeName3).toBe(\"RFX4\");\n expect(vehicleFX!.texture0).toBe(\"special/stationGlow\");\n expect(vehicleFX!.texture1).toBe(\"special/stationLight2\");\n // station.cs personal FX values.\n expect(personalFX, \"StationFXPersonalData\").toBeDefined();\n expect(personalFX!.leftNodeName).toBe(\"FX1\");\n expect(personalFX!.rightNodeName).toBe(\"FX2\");\n expect(personalFX!.texture0).toBe(\"special/stationLight\");\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"DataBlockParsers.test.js","sourceRoot":"","sources":["../src/DataBlockParsers.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 { DemoParser } from \"./DemoParser.js\";\nimport type {\n ELFProjectileDataBlock,\n EnergyProjectileDataBlock,\n LinearProjectileDataBlock,\n PlayerDataBlock,\n RepairProjectileDataBlock,\n ShapeBaseImageDataBlock,\n SniperProjectileDataBlock,\n TracerProjectileDataBlock,\n} from \"./dataBlockDataTypes.js\";\n\nconst DEMO_DIR = path.resolve(import.meta.dirname, \"..\", \"data\");\n\nasync function loadDataBlocks(file: string) {\n const buffer = fs.readFileSync(path.join(DEMO_DIR, file));\n const parser = new DemoParser(new Uint8Array(buffer));\n const { initialBlock } = await parser.load();\n return initialBlock.dataBlocks;\n}\n\n/**\n * Regression tests for projectile datablock field naming, which was\n * binary-verified against Tribes2.exe build 25034 (initPersistFields\n * FUN_0062b3c0 / FUN_0063fcb0 map names to the struct offsets that\n * unpackData reads in order). Prior to that verification several fields\n * were misassigned — most damagingly, the true lifetimeMS landed in a\n * field named `fizzleType`, breaking client-side projectile simulation.\n */\ndescribe(\"projectile datablock field decoding\", () => {\n it(\"decodes LinearProjectileData fields with engine semantics\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n const linears: LinearProjectileDataBlock[] = [];\n for (const [, db] of dataBlocks) {\n if (\n db.className === \"LinearProjectileData\" ||\n db.className === \"TracerProjectileData\" ||\n db.className === \"LinearFlareProjectileData\"\n ) {\n linears.push(db.data as LinearProjectileDataBlock);\n }\n }\n expect(linears.length).toBeGreaterThan(0);\n\n for (const block of linears) {\n // lifetimeMS/fizzleTimeMS are real milliseconds, rounded up to a\n // 32ms tick by the engine's onAdd, and clamped to 511 ticks.\n expect(block.lifetimeMS! % 32).toBe(0);\n expect(block.lifetimeMS).toBeGreaterThanOrEqual(32);\n expect(block.lifetimeMS).toBeLessThanOrEqual(511 * 32);\n expect(block.fizzleTimeMS! % 32).toBe(0);\n // Angles are ranged 0–90 degrees on the wire.\n expect(block.reflectOnWaterImpactAngle).toBeGreaterThanOrEqual(0);\n expect(block.reflectOnWaterImpactAngle).toBeLessThanOrEqual(90);\n expect(typeof block.explodeOnDeath).toBe(\"boolean\");\n expect(typeof block.doDynamicClientHits).toBe(\"boolean\");\n }\n });\n\n it(\"decodes retail DiscProjectile exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let disc: LinearProjectileDataBlock | undefined;\n for (const [, db] of dataBlocks) {\n const data = db.data as LinearProjectileDataBlock;\n if (\n db.className === \"LinearProjectileData\" &&\n data.dryVelocity === 90 &&\n data.explodeOnDeath === true\n ) {\n disc = data;\n break;\n }\n }\n expect(disc, \"retail DiscProjectile datablock\").toBeDefined();\n // disc.cs: lifetimeMS = fizzleTimeMS = 5000 → tick-rounded to 5024.\n expect(disc!.lifetimeMS).toBe(5024);\n expect(disc!.fizzleTimeMS).toBe(5024);\n // disc.cs: reflectOnWaterImpactAngle = 15.0.\n expect(disc!.reflectOnWaterImpactAngle).toBe(15);\n });\n\n it(\"decodes retail EnergyBolt (blaster) fields exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let bolt: EnergyProjectileDataBlock | undefined;\n for (const [, db] of dataBlocks) {\n if (db.className === \"EnergyProjectileData\") {\n bolt = db.data as EnergyProjectileDataBlock;\n break;\n }\n }\n expect(bolt, \"retail EnergyBolt datablock\").toBeDefined();\n // blaster.cs: binary-verified names (initPersistFields FUN_00694b40,\n // unpackData FUN_00694d80) — these were previously mislabeled with\n // ELF-style beam field names.\n expect(bolt!.crossViewAng).toBeCloseTo(0.99, 5);\n expect(bolt!.crossSize).toBeCloseTo(0.55, 5);\n expect(bolt!.blurLifetime).toBeCloseTo(0.2, 5);\n expect(bolt!.blurWidth).toBeCloseTo(0.25, 5);\n expect(bolt!.blurColor!.r).toBeCloseTo(0.4, 5);\n expect(bolt!.blurColor!.g).toBeCloseTo(0, 5);\n expect(bolt!.blurColor!.b).toBeCloseTo(0, 5);\n expect(bolt!.texture0).toBe(\"special/blasterBolt\");\n expect(bolt!.texture1).toBe(\"special/blasterBoltCross\");\n // blaster.cs: scale = \"0.25 20.0 1.0\" (bolt quad half-width / length).\n expect(bolt!.scale!.x).toBeCloseTo(0.25, 5);\n expect(bolt!.scale!.y).toBeCloseTo(20, 5);\n });\n\n it(\"decodes retail BasicSniperShot (laser rifle) fields exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let shot: SniperProjectileDataBlock | undefined;\n for (const [, db] of dataBlocks) {\n if (db.className === \"SniperProjectileData\") {\n shot = db.data as SniperProjectileDataBlock;\n break;\n }\n }\n expect(shot, \"retail BasicSniperShot datablock\").toBeDefined();\n // sniperRifle.cs values.\n expect(shot!.maxRifleRange).toBeCloseTo(1000, 5);\n expect(shot!.beamColor!.r).toBeCloseTo(1, 2);\n expect(shot!.beamColor!.g).toBeCloseTo(0.1, 2);\n expect(shot!.beamColor!.b).toBeCloseTo(0.1, 2);\n expect(shot!.fadeTime).toBeCloseTo(1.0, 5);\n expect(shot!.startBeamWidth).toBeCloseTo(0.145, 5);\n expect(shot!.endBeamWidth).toBeCloseTo(0.25, 5);\n expect(shot!.pulseSpeed).toBeCloseTo(6.0, 5);\n expect(shot!.pulseLength).toBeCloseTo(0.15, 5);\n expect(shot!.textures![0]).toBe(\"special/flare\");\n expect(shot!.textures![1]).toBe(\"special/nonlingradient\");\n expect(shot!.textures![2]).toBe(\"special/laserrip01\");\n expect(shot!.textures![11]).toBe(\"special/sniper00\");\n });\n\n it(\"decodes both ELF datablocks (gun and turret) exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n const elfs: ELFProjectileDataBlock[] = [];\n for (const [, db] of dataBlocks) {\n if (db.className === \"ELFProjectileData\") {\n elfs.push(db.data as ELFProjectileDataBlock);\n }\n }\n // Binary-verified names (initPersistFields FUN_0064a860, unpackData\n // FUN_0064ae00) — previously fabricated beam names. Two datablocks\n // exist: the handheld BasicELF at beamRange 38 (the z0dd/ZOD server\n // patch every surviving server lineage runs — \"WHAT?? INCREASE ELF\n // RANGE?!!? was 37\"; the stock script says 30) and the stock\n // ELFTurretBolt at 75 (ELFBarrelLarge.cs).\n expect(elfs.map((e) => e.beamRange).sort((a, b) => a! - b!)).toEqual([\n 38, 75,\n ]);\n for (const elf of elfs) {\n expect(elf.mainBeamWidth).toBeCloseTo(0.1, 5);\n expect(elf.mainBeamSpeed).toBeCloseTo(9.0, 5);\n expect(elf.mainBeamRepeat).toBeCloseTo(0.25, 5);\n expect(elf.lightningWidth).toBeCloseTo(0.1, 5);\n expect(elf.lightningDist).toBeCloseTo(0.15, 5);\n expect(elf.textures).toEqual([\n \"special/ELFBeam\",\n \"special/ELFLightning\",\n \"special/BlueImpact\",\n ]);\n }\n });\n\n it(\"decodes retail DefaultRepairBeam fields exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let repair: RepairProjectileDataBlock | undefined;\n for (const [, db] of dataBlocks) {\n if (db.className === \"RepairProjectileData\") {\n repair = db.data as RepairProjectileDataBlock;\n break;\n }\n }\n expect(repair, \"retail DefaultRepairBeam datablock\").toBeDefined();\n // repairpack.cs: binary-verified names (initPersistFields\n // FUN_00644910, unpackData FUN_00644c40); numSegments is an S32\n // whose raw bits the old decode read as a float.\n expect(repair!.beamRange).toBeCloseTo(10, 5);\n expect(repair!.beamWidth).toBeCloseTo(0.15, 5);\n expect(repair!.numSegments).toBe(20);\n expect(repair!.texRepeat).toBeCloseTo(0.2, 5);\n expect(repair!.blurFreq).toBeCloseTo(10.0, 5);\n expect(repair!.blurLifetime).toBeCloseTo(1.0, 5);\n expect(repair!.cutoffAngle).toBeCloseTo(25.0, 5);\n expect(repair!.textures).toEqual([\"special/redbump2\", \"special/redflare\"]);\n });\n\n it(\"decodes image-state transitions in the engine packing order\", async () => {\n // Every retail weapon image opens Activate → (timeout) →\n // ActivateReady (e.g. blaster.cs stateTransitionOnTimeout[0]).\n // With the old rotated names, the timeout transition surfaced under\n // transitionGeneric0Out and consumers carried a remap table.\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let checked = 0;\n for (const [, db] of dataBlocks) {\n if (db.className !== \"ShapeBaseImageData\") continue;\n const data = db.data as ShapeBaseImageDataBlock;\n const states = data.states;\n if (!states || states.length < 2) continue;\n if (states[0].name.toLowerCase() !== \"activate\") continue;\n const readyIndex = states.findIndex(\n (st) => st.name.toLowerCase() === \"activateready\",\n );\n if (readyIndex < 0) continue;\n // Transition values are 1-based state indices (0 = none).\n expect(states[0].transitionOnTimeout).toBe(readyIndex + 1);\n checked++;\n }\n expect(checked).toBeGreaterThan(3);\n });\n\n it(\"decodes retail ChaingunBullet tracer fields exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let bullet: TracerProjectileDataBlock | undefined;\n for (const [, db] of dataBlocks) {\n const data = db.data as TracerProjectileDataBlock;\n if (\n db.className === \"TracerProjectileData\" &&\n data.tracerTex0 === \"special/tracer00\" &&\n data.lifetimeMS === 3008\n ) {\n bullet = data;\n break;\n }\n }\n expect(bullet, \"ChaingunBullet datablock\").toBeDefined();\n // Values as recorded in this demo (a lightly modded server — tracer\n // length/width are doubled from retail, everything else matches\n // chaingun.cs). Each assertion pins a decode position: a swap with\n // any neighboring field would produce a nonsensical value here.\n expect(bullet!.dryVelocity).toBe(750);\n expect(bullet!.wetVelocity).toBe(280);\n expect(bullet!.fizzleTimeMS).toBe(3008);\n expect(bullet!.explodeOnDeath).toBe(false);\n // Chainguns are the only retail weapons with doDynamicClientHits.\n expect(bullet!.doDynamicClientHits).toBe(true);\n // activateDelayMS = -1 (retail default) read as U32.\n expect(bullet!.activateDelayMS).toBe(0xffffffff);\n expect(bullet!.tracerLength).toBe(30);\n expect(bullet!.tracerMinPixels).toBe(6);\n expect(bullet!.tracerWidth).toBeCloseTo(0.2, 5);\n expect(bullet!.tracerAlpha).toBe(false);\n // Retail chaingun.cs tracerColor: 211/255, 215/255, 120/255, 0.75.\n expect(bullet!.tracerColor!.r).toBeCloseTo(211 / 255, 2);\n expect(bullet!.tracerColor!.g).toBeCloseTo(215 / 255, 2);\n expect(bullet!.tracerColor!.b).toBeCloseTo(120 / 255, 2);\n expect(bullet!.crossViewAng).toBeCloseTo(0.99, 5);\n expect(bullet!.crossSize).toBeCloseTo(0.2, 5);\n expect(bullet!.renderCross).toBe(true);\n expect(bullet!.tracerTex1).toBe(\"special/tracercross\");\n });\n\n it(\"decodes PlayerData movement/jet fields with engine semantics\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let armor: PlayerDataBlock | undefined;\n for (const [, db] of dataBlocks) {\n const data = db.data as PlayerDataBlock;\n // The armor the recorder wears: retail-value run/speed section.\n if (db.className === \"PlayerData\" && data.maxForwardSpeed === 15) {\n armor = data;\n break;\n }\n }\n expect(armor, \"light armor PlayerData datablock\").toBeDefined();\n // Retail player.cs values — each pins a decode position (previously\n // these landed in fields named jumpForce/runSurfaceAngle/etc.).\n expect(armor!.runForce).toBe(4968); // 55.2 * 90\n expect(armor!.maxBackwardSpeed).toBe(13);\n expect(armor!.maxSideSpeed).toBe(13);\n expect(armor!.underwaterVertJetFactor).toBeCloseTo(1.5, 5);\n expect(armor!.minLookAngle).toBeCloseTo(-1.5, 5);\n expect(armor!.maxLookAngle).toBeCloseTo(1.5, 5);\n // Jet energy fields must decode as plausible per-tick magnitudes\n // (this server mods them slightly, so no exact match): drain is a\n // fraction of maxEnergy(60), threshold is a small energy value.\n expect(armor!.jetEnergyDrain).toBeGreaterThan(0);\n expect(armor!.jetEnergyDrain).toBeLessThan(5);\n expect(armor!.underwaterJetEnergyDrain).toBeGreaterThan(0);\n expect(armor!.underwaterJetEnergyDrain).toBeLessThan(5);\n expect(armor!.minJetEnergy).toBeGreaterThanOrEqual(0);\n expect(armor!.minJetEnergy).toBeLessThan(10);\n // Heat signature rates — retail player.cs: 1/4 and 1/3. These are the\n // first two reads of the ground-impact tail; the old decode shifted\n // the whole section by two slots.\n expect(armor!.heatDecayPerSec).toBeCloseTo(0.25, 5);\n expect(armor!.heatIncreasePerSec).toBeCloseTo(1 / 3, 5);\n expect(armor!.groundImpactShakeDuration).toBeCloseTo(0.6, 5);\n expect(armor!.groundImpactShakeFalloff).toBeCloseTo(10, 5);\n });\n\n it(\"decodes StationFX node names and textures exactly\", async () => {\n const dataBlocks = await loadDataBlocks(\"exogen_Katabatic_vpad.rec\");\n let vehicleFX: Record<string, unknown> | undefined;\n let personalFX: Record<string, unknown> | undefined;\n for (const [, db] of dataBlocks) {\n if (db.className === \"StationFXVehicleData\") vehicleFX ??= db.data;\n if (db.className === \"StationFXPersonalData\") personalFX ??= db.data;\n }\n // serverVehicleHud.cs VehicleInvFX values.\n expect(vehicleFX, \"StationFXVehicleData\").toBeDefined();\n expect(vehicleFX!.glowNodeName).toBe(\"GLOWFX\");\n expect(vehicleFX!.leftNodeName0).toBe(\"LFX1\");\n expect(vehicleFX!.rightNodeName0).toBe(\"RFX1\");\n expect(vehicleFX!.leftNodeName3).toBe(\"LFX4\");\n expect(vehicleFX!.rightNodeName3).toBe(\"RFX4\");\n expect(vehicleFX!.texture0).toBe(\"special/stationGlow\");\n expect(vehicleFX!.texture1).toBe(\"special/stationLight2\");\n // station.cs personal FX values.\n expect(personalFX, \"StationFXPersonalData\").toBeDefined();\n expect(personalFX!.leftNodeName).toBe(\"FX1\");\n expect(personalFX!.rightNodeName).toBe(\"FX2\");\n expect(personalFX!.texture0).toBe(\"special/stationLight\");\n });\n});\n"]}
|
package/dist/DemoParser.d.ts
CHANGED
|
@@ -17,7 +17,22 @@ export declare class DemoParser {
|
|
|
17
17
|
private _blockStreamOffset;
|
|
18
18
|
private _blockCount?;
|
|
19
19
|
private _blockCursor;
|
|
20
|
-
|
|
20
|
+
private readonly _incremental;
|
|
21
|
+
private _complete;
|
|
22
|
+
private _inflator?;
|
|
23
|
+
/** The zlib stream announced its own end (final deflate block seen) —
|
|
24
|
+
* later bytes are trailing garbage and further pushes would throw. */
|
|
25
|
+
private _streamEnded;
|
|
26
|
+
/** Backing store for the growing decompressed stream (incremental
|
|
27
|
+
* mode). `_decompressedData` is always a length-exact subarray of
|
|
28
|
+
* this, so every existing `.length`-based read stays correct. */
|
|
29
|
+
private _backing?;
|
|
30
|
+
private _decompressedLength;
|
|
31
|
+
private _scanOffset;
|
|
32
|
+
private _bufferedMoveTicks;
|
|
33
|
+
constructor(buffer: Uint8Array, options?: {
|
|
34
|
+
incremental?: boolean;
|
|
35
|
+
});
|
|
21
36
|
getRegistry(): ClassRegistry;
|
|
22
37
|
getGhostTracker(): GhostTracker;
|
|
23
38
|
getPacketParser(): PacketParser;
|
|
@@ -34,6 +49,53 @@ export declare class DemoParser {
|
|
|
34
49
|
* Idempotent — second call returns cached result.
|
|
35
50
|
*/
|
|
36
51
|
load(): Promise<LoadResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Parse just the fixed-size demo header from a byte prefix, without
|
|
54
|
+
* constructing a parser. Throws RangeError when `bytes` is too short —
|
|
55
|
+
* callers streaming a download retry as more data arrives. The
|
|
56
|
+
* returned `byteLength` is where the initial block begins; the block
|
|
57
|
+
* stream begins at `byteLength + header.initialBlockSize`.
|
|
58
|
+
*/
|
|
59
|
+
static peekHeader(bytes: Uint8Array): {
|
|
60
|
+
header: DemoHeader;
|
|
61
|
+
byteLength: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Incremental mode only: feed the next chunk of raw (compressed) block
|
|
65
|
+
* stream bytes as they arrive. Inflation happens synchronously; any
|
|
66
|
+
* blocks completed by this chunk become readable via nextBlock().
|
|
67
|
+
* The chunk is not retained.
|
|
68
|
+
*/
|
|
69
|
+
push(chunk: Uint8Array): void;
|
|
70
|
+
/**
|
|
71
|
+
* Incremental mode only: signal that the download is complete. Flushes
|
|
72
|
+
* the inflator; after this, running out of blocks means the true end
|
|
73
|
+
* of the demo rather than the frontier.
|
|
74
|
+
*/
|
|
75
|
+
finish(): void;
|
|
76
|
+
/**
|
|
77
|
+
* False only in incremental mode before finish(): nextBlock()
|
|
78
|
+
* returning undefined then means "frontier — more data may arrive",
|
|
79
|
+
* not the end of the demo.
|
|
80
|
+
*/
|
|
81
|
+
get isComplete(): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Bytes of decompressed block stream available so far. Grows during
|
|
84
|
+
* an incremental feed; consumers that latched an "out of blocks"
|
|
85
|
+
* state can compare against it to know new data has arrived.
|
|
86
|
+
*/
|
|
87
|
+
get decompressedByteLength(): number;
|
|
88
|
+
/**
|
|
89
|
+
* Move-tick blocks in the decompressed stream so far — each is one
|
|
90
|
+
* fixed 32ms simulation tick, so this measures buffered DEMO TIME
|
|
91
|
+
* exactly (compressed bytes don't: quiet stretches pack far denser).
|
|
92
|
+
* Independent of the read cursor; unaffected by reset(). Maintained
|
|
93
|
+
* incrementally as data arrives; a full lazy scan on first access
|
|
94
|
+
* covers the one-shot mode.
|
|
95
|
+
*/
|
|
96
|
+
get bufferedMoveTicks(): number;
|
|
97
|
+
private _scanBufferedTicks;
|
|
98
|
+
private _appendDecompressed;
|
|
37
99
|
/**
|
|
38
100
|
* Read and parse the next block from the decompressed buffer.
|
|
39
101
|
* Returns the parsed block, or undefined when all blocks are exhausted.
|
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;
|
|
1
|
+
{"version":3,"file":"DemoParser.d.ts","sourceRoot":"","sources":["../src/DemoParser.ts"],"names":[],"mappings":"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"}
|
package/dist/DemoParser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inflate } from "fflate";
|
|
1
|
+
import { inflate, Inflate } from "fflate";
|
|
2
2
|
import createDebug from "debug";
|
|
3
3
|
import { BitStream } from "./BitStream.js";
|
|
4
4
|
import { PacketParser } from "./PacketParser.js";
|
|
@@ -26,10 +26,28 @@ export class DemoParser {
|
|
|
26
26
|
_blockStreamOffset = 0;
|
|
27
27
|
_blockCount;
|
|
28
28
|
_blockCursor = 0;
|
|
29
|
-
|
|
29
|
+
// Incremental mode: the compressed block stream arrives in chunks via
|
|
30
|
+
// push() and is inflated as it goes; nextBlock() treats the end of the
|
|
31
|
+
// decompressed data as a frontier (more may arrive) until finish().
|
|
32
|
+
_incremental;
|
|
33
|
+
_complete = true;
|
|
34
|
+
_inflator;
|
|
35
|
+
/** The zlib stream announced its own end (final deflate block seen) —
|
|
36
|
+
* later bytes are trailing garbage and further pushes would throw. */
|
|
37
|
+
_streamEnded = false;
|
|
38
|
+
/** Backing store for the growing decompressed stream (incremental
|
|
39
|
+
* mode). `_decompressedData` is always a length-exact subarray of
|
|
40
|
+
* this, so every existing `.length`-based read stays correct. */
|
|
41
|
+
_backing;
|
|
42
|
+
_decompressedLength = 0;
|
|
43
|
+
// Buffered-time scan state (see bufferedMoveTicks).
|
|
44
|
+
_scanOffset = 0;
|
|
45
|
+
_bufferedMoveTicks = 0;
|
|
46
|
+
constructor(buffer, options) {
|
|
30
47
|
this.buffer = buffer;
|
|
31
48
|
this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
32
49
|
this.offset = 0;
|
|
50
|
+
this._incremental = options?.incremental === true;
|
|
33
51
|
this.registry = new ClassRegistry();
|
|
34
52
|
this.ghostTracker = new GhostTracker();
|
|
35
53
|
// Register parser catalogs
|
|
@@ -129,23 +147,50 @@ export class DemoParser {
|
|
|
129
147
|
// Phase 1: header + initial block
|
|
130
148
|
const header = this.readHeader();
|
|
131
149
|
debug('header: "%s" version=0x%s length=%dms (%smin) initialBlockSize=%d', header.identString, header.protocolVersion.toString(16), header.demoLengthMs, (header.demoLengthMs / 1000 / 60).toFixed(1), header.initialBlockSize);
|
|
150
|
+
if (this._incremental &&
|
|
151
|
+
this.buffer.length < this.offset + header.initialBlockSize) {
|
|
152
|
+
throw new RangeError(`incremental parser needs the full initial block up front: have ${this.buffer.length - this.offset} bytes, need ${header.initialBlockSize}`);
|
|
153
|
+
}
|
|
132
154
|
const initialBlockData = this.buffer.subarray(this.offset, this.offset + header.initialBlockSize);
|
|
133
155
|
const initialBlock = this.readInitialBlock(initialBlockData);
|
|
134
156
|
this.offset += header.initialBlockSize;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
if (this._incremental) {
|
|
158
|
+
// Phase 2 (incremental): streaming inflate. Compressed bytes
|
|
159
|
+
// arrive via push(); whatever tail the constructor buffer already
|
|
160
|
+
// holds past the initial block is the first chunk. fflate's
|
|
161
|
+
// Inflate emits decompressed output synchronously during push.
|
|
162
|
+
this._complete = false;
|
|
163
|
+
this._decompressedLength = 0;
|
|
164
|
+
this._backing = new Uint8Array(1 << 20);
|
|
165
|
+
this._decompressedData = this._backing.subarray(0, 0);
|
|
166
|
+
this._decompressedView = new DataView(this._backing.buffer, 0, 0);
|
|
167
|
+
this._inflator = new Inflate((chunk, final) => {
|
|
168
|
+
this._appendDecompressed(chunk);
|
|
169
|
+
if (final)
|
|
170
|
+
this._streamEnded = true;
|
|
144
171
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
const tail = this.buffer.subarray(this.offset);
|
|
173
|
+
// The prefix buffer has served its purpose (header + initial block
|
|
174
|
+
// are parsed); don't retain arbitrary compressed tail bytes twice.
|
|
175
|
+
if (tail.length > 0)
|
|
176
|
+
this._inflator.push(tail);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Phase 2: async decompress block stream
|
|
180
|
+
const compressedData = this.buffer.subarray(this.offset);
|
|
181
|
+
debug("compressed block stream: %d bytes", compressedData.length);
|
|
182
|
+
const decompressedData = await new Promise((resolve, reject) => {
|
|
183
|
+
inflate(compressedData, (err, data) => {
|
|
184
|
+
if (err)
|
|
185
|
+
reject(err);
|
|
186
|
+
else
|
|
187
|
+
resolve(data);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
debug("decompressed block stream: %d bytes", decompressedData.length);
|
|
191
|
+
this._decompressedData = decompressedData;
|
|
192
|
+
this._decompressedView = new DataView(decompressedData.buffer, decompressedData.byteOffset, decompressedData.byteLength);
|
|
193
|
+
}
|
|
149
194
|
// Phase 3: set up PacketParser with seeded ghost tracker
|
|
150
195
|
this.setupPacketParser(initialBlock);
|
|
151
196
|
// Cache results
|
|
@@ -156,6 +201,154 @@ export class DemoParser {
|
|
|
156
201
|
this._loaded = true;
|
|
157
202
|
return { header, initialBlock };
|
|
158
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Parse just the fixed-size demo header from a byte prefix, without
|
|
206
|
+
* constructing a parser. Throws RangeError when `bytes` is too short —
|
|
207
|
+
* callers streaming a download retry as more data arrives. The
|
|
208
|
+
* returned `byteLength` is where the initial block begins; the block
|
|
209
|
+
* stream begins at `byteLength + header.initialBlockSize`.
|
|
210
|
+
*/
|
|
211
|
+
static peekHeader(bytes) {
|
|
212
|
+
if (bytes.length < 1)
|
|
213
|
+
throw new RangeError("incomplete header");
|
|
214
|
+
const strLen = bytes[0];
|
|
215
|
+
const byteLength = 1 + strLen + 12;
|
|
216
|
+
if (bytes.length < byteLength)
|
|
217
|
+
throw new RangeError("incomplete header");
|
|
218
|
+
const identString = new TextDecoder("ascii").decode(bytes.subarray(1, 1 + strLen));
|
|
219
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
220
|
+
const protocolVersion = view.getUint32(1 + strLen, true);
|
|
221
|
+
const demoLengthMs = view.getUint32(1 + strLen + 4, true);
|
|
222
|
+
const initialBlockSize = view.getUint32(1 + strLen + 8, true);
|
|
223
|
+
return {
|
|
224
|
+
header: { identString, protocolVersion, demoLengthMs, initialBlockSize },
|
|
225
|
+
byteLength,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Incremental mode only: feed the next chunk of raw (compressed) block
|
|
230
|
+
* stream bytes as they arrive. Inflation happens synchronously; any
|
|
231
|
+
* blocks completed by this chunk become readable via nextBlock().
|
|
232
|
+
* The chunk is not retained.
|
|
233
|
+
*/
|
|
234
|
+
push(chunk) {
|
|
235
|
+
if (!this._incremental)
|
|
236
|
+
throw new Error("not an incremental parser");
|
|
237
|
+
if (!this._loaded)
|
|
238
|
+
throw new Error("must call load() first");
|
|
239
|
+
if (this._complete)
|
|
240
|
+
throw new Error("already finished");
|
|
241
|
+
if (chunk.length === 0 || this._streamEnded)
|
|
242
|
+
return;
|
|
243
|
+
this._inflator.push(chunk);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Incremental mode only: signal that the download is complete. Flushes
|
|
247
|
+
* the inflator; after this, running out of blocks means the true end
|
|
248
|
+
* of the demo rather than the frontier.
|
|
249
|
+
*/
|
|
250
|
+
finish() {
|
|
251
|
+
if (!this._incremental)
|
|
252
|
+
throw new Error("not an incremental parser");
|
|
253
|
+
if (!this._loaded)
|
|
254
|
+
throw new Error("must call load() first");
|
|
255
|
+
if (this._complete)
|
|
256
|
+
return;
|
|
257
|
+
if (!this._streamEnded) {
|
|
258
|
+
try {
|
|
259
|
+
this._inflator.push(new Uint8Array(0), true);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
// A truncated zlib stream: the blocks inflated so far remain
|
|
263
|
+
// valid (nextBlock bounds-checks the tail) — degrade to a
|
|
264
|
+
// shorter demo rather than failing the whole load.
|
|
265
|
+
debug("finish(): inflate flush failed: %o", err);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
this._inflator = undefined;
|
|
269
|
+
this._complete = true;
|
|
270
|
+
// Trim: the doubling backing store can overshoot by up to 2× —
|
|
271
|
+
// release the slack now that the final length is known.
|
|
272
|
+
if (this._backing && this._backing.length > this._decompressedLength) {
|
|
273
|
+
const exact = this._backing.slice(0, this._decompressedLength);
|
|
274
|
+
this._backing = exact;
|
|
275
|
+
this._decompressedData = exact;
|
|
276
|
+
this._decompressedView = new DataView(exact.buffer, exact.byteOffset, exact.byteLength);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* False only in incremental mode before finish(): nextBlock()
|
|
281
|
+
* returning undefined then means "frontier — more data may arrive",
|
|
282
|
+
* not the end of the demo.
|
|
283
|
+
*/
|
|
284
|
+
get isComplete() {
|
|
285
|
+
return this._complete;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Bytes of decompressed block stream available so far. Grows during
|
|
289
|
+
* an incremental feed; consumers that latched an "out of blocks"
|
|
290
|
+
* state can compare against it to know new data has arrived.
|
|
291
|
+
*/
|
|
292
|
+
get decompressedByteLength() {
|
|
293
|
+
return this._incremental
|
|
294
|
+
? this._decompressedLength
|
|
295
|
+
: (this._decompressedData?.length ?? 0);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Move-tick blocks in the decompressed stream so far — each is one
|
|
299
|
+
* fixed 32ms simulation tick, so this measures buffered DEMO TIME
|
|
300
|
+
* exactly (compressed bytes don't: quiet stretches pack far denser).
|
|
301
|
+
* Independent of the read cursor; unaffected by reset(). Maintained
|
|
302
|
+
* incrementally as data arrives; a full lazy scan on first access
|
|
303
|
+
* covers the one-shot mode.
|
|
304
|
+
*/
|
|
305
|
+
get bufferedMoveTicks() {
|
|
306
|
+
this._scanBufferedTicks();
|
|
307
|
+
return this._bufferedMoveTicks;
|
|
308
|
+
}
|
|
309
|
+
_scanBufferedTicks() {
|
|
310
|
+
const data = this._decompressedData;
|
|
311
|
+
const view = this._decompressedView;
|
|
312
|
+
if (!data || !view)
|
|
313
|
+
return;
|
|
314
|
+
let off = this._scanOffset;
|
|
315
|
+
let ticks = this._bufferedMoveTicks;
|
|
316
|
+
while (off + 2 <= data.length) {
|
|
317
|
+
const typeSize = view.getUint16(off, true);
|
|
318
|
+
const size = typeSize & 0xfff;
|
|
319
|
+
if (off + 2 + size > data.length)
|
|
320
|
+
break;
|
|
321
|
+
if (typeSize >> 12 === BlockTypeMove)
|
|
322
|
+
ticks++;
|
|
323
|
+
off += 2 + size;
|
|
324
|
+
}
|
|
325
|
+
this._scanOffset = off;
|
|
326
|
+
this._bufferedMoveTicks = ticks;
|
|
327
|
+
}
|
|
328
|
+
_appendDecompressed(chunk) {
|
|
329
|
+
if (chunk.length === 0)
|
|
330
|
+
return;
|
|
331
|
+
const needed = this._decompressedLength + chunk.length;
|
|
332
|
+
let backing = this._backing;
|
|
333
|
+
if (needed > backing.length) {
|
|
334
|
+
let capacity = backing.length;
|
|
335
|
+
while (capacity < needed)
|
|
336
|
+
capacity *= 2;
|
|
337
|
+
const grown = new Uint8Array(capacity);
|
|
338
|
+
grown.set(backing.subarray(0, this._decompressedLength));
|
|
339
|
+
backing = grown;
|
|
340
|
+
this._backing = grown;
|
|
341
|
+
}
|
|
342
|
+
backing.set(chunk, this._decompressedLength);
|
|
343
|
+
this._decompressedLength = needed;
|
|
344
|
+
// Refresh the exact-length views; subarray shares memory, so this is
|
|
345
|
+
// cheap and keeps every `.length`-based reader (nextBlock,
|
|
346
|
+
// blockCount) correct without touching them.
|
|
347
|
+
this._decompressedData = backing.subarray(0, needed);
|
|
348
|
+
this._decompressedView = new DataView(backing.buffer, 0, needed);
|
|
349
|
+
// The lazily-cached block count no longer covers the new bytes.
|
|
350
|
+
this._blockCount = undefined;
|
|
351
|
+
}
|
|
159
352
|
/**
|
|
160
353
|
* Read and parse the next block from the decompressed buffer.
|
|
161
354
|
* Returns the parsed block, or undefined when all blocks are exhausted.
|