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.
@@ -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"]}
@@ -17,7 +17,22 @@ export declare class DemoParser {
17
17
  private _blockStreamOffset;
18
18
  private _blockCount?;
19
19
  private _blockCursor;
20
- constructor(buffer: Uint8Array);
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.
@@ -1 +1 @@
1
- {"version":3,"file":"DemoParser.d.ts","sourceRoot":"","sources":["../src/DemoParser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAwB,MAAM,mBAAmB,CAAC;AAuBvE,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,SAAS,EACT,gBAAgB,EAWhB,UAAU,EACX,MAAM,YAAY,CAAC;AAMpB,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,IAAI,CAAW;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IAEnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAC,CAAa;IAC7B,OAAO,CAAC,aAAa,CAAC,CAAmB;IACzC,OAAO,CAAC,iBAAiB,CAAC,CAAa;IACvC,OAAO,CAAC,iBAAiB,CAAC,CAAW;IACrC,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAK;gBAEb,MAAM,EAAE,UAAU;IAyE9B,WAAW,IAAI,aAAa;IAI5B,eAAe,IAAI,YAAY;IAI/B,eAAe,IAAI,YAAY;IAM/B,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,MAAM,IAAI,UAAU,CAGvB;IAED,IAAI,YAAY,IAAI,gBAAgB,CAGnC;IAED,IAAI,UAAU,IAAI,MAAM,CAkBvB;IAED,IAAI,WAAW,IAAI,MAAM,CAGxB;IAID;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC;IA4DjC;;;;;OAKG;IACH,SAAS,IAAI,SAAS,GAAG,SAAS;IA2DlC;;;;;OAKG;IACH,KAAK,IAAI,IAAI;IAWb;;;;;OAKG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAUpC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA8BzB;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC;IAQxC,OAAO,CAAC,UAAU;IAwBlB;4DACwD;IACxD,OAAO,CAAC,gBAAgB;IA4TxB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAuBtB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAQtB;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IA6EhC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,eAAe;IAyBvB;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,mBAAmB;IAmE3B,OAAO,CAAC,mBAAmB;IAuH3B;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IAsEtB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA4CjC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,WAAW;IA0CnB;;;OAGG;IACH,OAAO,CAAC,aAAa;CAOtB"}
1
+ {"version":3,"file":"DemoParser.d.ts","sourceRoot":"","sources":["../src/DemoParser.ts"],"names":[],"mappings":"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"}
@@ -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
- constructor(buffer) {
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
- // Phase 2: async decompress block stream
136
- const compressedData = this.buffer.subarray(this.offset);
137
- debug("compressed block stream: %d bytes", compressedData.length);
138
- const decompressedData = await new Promise((resolve, reject) => {
139
- inflate(compressedData, (err, data) => {
140
- if (err)
141
- reject(err);
142
- else
143
- resolve(data);
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
- debug("decompressed block stream: %d bytes", decompressedData.length);
147
- this._decompressedData = decompressedData;
148
- this._decompressedView = new DataView(decompressedData.buffer, decompressedData.byteOffset, decompressedData.byteLength);
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.