import test, { afterEach } from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { TerrainController } from "../terrain-controller.js"; import { TerrainState } from "../terrain-state.js"; import { SectionBlockMap } from "../section-block-map.js"; const properties = JSON.parse( await readFile(new URL("../light-properties.json", import.meta.url)), ); const controllers = []; const bounds = { min: [-1, -1, -1], max: [18, 3, 3] }; const materials = new Map([ [1, { state: "minecraft:stone", color: [128, 128, 128] }], [2, { state: "minecraft:torch", color: [255, 200, 100], light: 14 }], ]); class FakeWorker { packets = []; transfers = []; postMessage(packet, transfers = []) { this.transfers.push(transfers.length); this.packets.push(structuredClone(packet, { transfer: transfers })); } terminate() { this.terminated = true; } reply(message) { this.onmessage({ data: message }); } light(packet, dirty = [], value = 0) { this.reply({ type: "light", epoch: packet.epoch, version: packet.version, dirty, milliseconds: 1, field: { min: [0, 0, 0], size: [1, 1, 1], sourceCount: value ? 1 : 0, data: new Uint8Array(4), blockLevels: new Uint8Array([value]), skyLevels: new Uint8Array([15]), }, }); } mesh(packet) { this.reply({ ...packet, vertices: new Float32Array(20), transparent: new Float32Array(), milliseconds: 1, }); } } function fixture() { const worker = new FakeWorker(), published = [], dirty = new Set(); const controller = new TerrainController({ workerFactory: () => worker, onLight: (field) => published.push(field), onDirty: (ids) => { for (const id of ids) dirty.add(id); }, }); controllers.push(controller); return { controller, worker, published, dirty }; } afterEach(() => { for (const controller of controllers.splice(0)) controller.dispose(); }); function queuedClock(t) { const callbacks = new Map(); let id = 0, now = 0; t.mock.method(globalThis, "setTimeout", (callback) => { callbacks.set(++id, callback); return id; }); t.mock.method(globalThis, "clearTimeout", (id) => callbacks.delete(id)); t.mock.method(performance, "now", () => (now += 3)); return () => { for (let count = 0; callbacks.size; count++) { assert.ok(count < 100, "Preparation failed to finish"); const [id, callback] = callbacks.entries().next().value; callbacks.delete(id); callback(); } }; } test("edits during incremental reset packing replay over the captured entries coherently", (t) => { const drain = queuedClock(t), { controller, worker } = fixture(); const blocks = new SectionBlockMap( Array.from({ length: 140 }, (_, x) => [`${x},0,0`, 1]), ); controller.reset(blocks, materials, new Map(), bounds); controller.flush(); assert.equal(controller.preparing, true); blocks.delete("0,0,0"); blocks.set("1,0,0", 2); blocks.set("200,0,0", 2); controller.update({ changes: [ { pos: [0, 0, 0], block: 0 }, { pos: [1, 0, 0], block: 2 }, { pos: [200, 0, 0], block: 2 }, ], }); drain(); assert.equal(worker.packets.length, 1); const state = new TerrainState(properties), packet = worker.packets[0]; assert.equal(packet.reset, true); assert.equal(packet.version, controller.version); assert.ok(packet.initial instanceof Int32Array); assert.ok(packet.changes instanceof Int32Array); assert.equal(state.sync(packet), true); assert.deepEqual([...state.blocks], [...blocks]); }); test("a replacement world cancels old reset preparation and rejects old light and mesh replies", (t) => { const drain = queuedClock(t), { controller, worker, published, dirty } = fixture(); controller.reset( new SectionBlockMap(Array.from({ length: 140 }, (_, x) => [`${x},0,0`, 1])), materials, new Map(), bounds, ); controller.flush(); assert.equal(controller.preparing, true); const oldEpoch = controller.epoch; controller.reset( new SectionBlockMap([["16,0,0", 2]]), materials, new Map(), bounds, ); controller.flush(); drain(); assert.equal(worker.packets.length, 1); const packet = worker.packets[0]; assert.deepEqual([...packet.initial], [16, 0, 0, 2]); worker.light(packet, ["1,0,0"], 14); assert.equal(controller.requestMesh("1,0,0"), true); const job = controller.busy; worker.light({ epoch: oldEpoch, version: packet.version }, ["old-world"], 15); worker.mesh({ ...job, epoch: oldEpoch }); assert.equal(controller.busy, job); assert.deepEqual([...dirty], ["1,0,0"]); assert.equal(published.length, 1); assert.equal(published[0].sample([0, 0, 0]).blockLevel, 14); worker.mesh(job); assert.equal(controller.ready.get("1,0,0").epoch, controller.epoch); }); test("subsequent syncs send deduplicated numeric deltas and only changed materials or textures", () => { const { controller, worker } = fixture(); const blocks = new SectionBlockMap([["0,0,0", 1]]), textures = new Map([["stone", 0]]); controller.reset(blocks, materials, textures, bounds); controller.flush(); blocks.entries = () => { throw Error("Incremental sync scanned the world"); }; blocks[Symbol.iterator] = blocks.entries; controller.update({ changes: [{ pos: [0, 0, 0], block: 2 }] }); controller.update({ changes: [ { pos: [0, 0, 0], block: 0 }, { pos: [16, 0, 0], block: 1 }, ], }); controller.flush(); const delta = worker.packets[1]; assert.equal(delta.reset, false); assert.equal(delta.initial, null); assert.equal(delta.blocks, undefined); assert.deepEqual([...delta.changes], [0, 0, 0, 0, 16, 0, 0, 1]); assert.deepEqual(delta.materials, []); assert.equal(delta.textures, undefined); assert.equal(worker.transfers[1], 1); const updated = new Map(materials); updated.set(2, { ...materials.get(2), light: 15, ignoredMetadata: "not copied", }); controller.update({ materials: updated, textures: new Map([["stone", 7]]) }); controller.flush(); assert.deepEqual( worker.packets[2].materials.map(([id]) => id), [2], ); assert.equal(worker.packets[2].materials[0][1].light, 15); assert.equal(worker.packets[2].materials[0][1].ignoredMetadata, undefined); assert.deepEqual(worker.packets[2].textures, [["stone", 7]]); }); test("skipped intermediate light fields contribute their dirty union without being published", () => { const { controller, worker, dirty, published } = fixture(); controller.reset( new SectionBlockMap([["0,0,0", 1]]), materials, new Map(), bounds, ); controller.flush(); const old = worker.packets[0]; controller.update({ changes: [{ pos: [0, 0, 0], block: 2 }] }); controller.flush(); const latest = worker.packets[1]; worker.light(old, ["0,0,0", "1,0,0"], 0); assert.equal(published.length, 0); assert.equal(controller.requestMesh("0,0,0"), false); worker.light(latest, ["1,0,0", "2,0,0"], 14); assert.equal(published.length, 1); assert.deepEqual([...dirty], ["0,0,0", "1,0,0", "2,0,0"]); assert.equal(controller.requestMesh("0,0,0"), true); }); test("mesh requests allow one in flight and two ready, and stale replies cannot release a newer job", () => { const { controller, worker } = fixture(); controller.reset( new SectionBlockMap([["0,0,0", 1]]), materials, new Map(), bounds, ); controller.flush(); worker.light(worker.packets[0]); assert.equal(controller.requestMesh("0,0,0"), true); assert.equal(controller.requestMesh("1,0,0"), false); worker.mesh(controller.busy); assert.equal(controller.requestMesh("0,0,0"), false); assert.equal(controller.requestMesh("1,0,0"), true); worker.mesh(controller.busy); assert.equal(controller.ready.size, 2); assert.equal(controller.requestMesh("2,0,0"), false); controller.ready.delete("0,0,0"); assert.equal(controller.requestMesh("2,0,0"), true); const stale = controller.busy; controller.update({ unload: [[2, 0, 0]] }); assert.equal(controller.ready.size, 0); controller.flush(); worker.light(worker.packets.at(-1)); assert.equal(controller.requestMesh("0,0,0"), false); worker.mesh(stale); assert.equal(controller.ready.has("2,0,0"), false); assert.equal(controller.requestMesh("0,0,0"), true); const current = controller.busy; worker.mesh(stale); assert.equal(controller.busy, current); worker.mesh(current); assert.deepEqual([...controller.ready.keys()], ["0,0,0"]); }); test("a section unloaded then re-entered within one flush keeps only its newer blocks", () => { const { controller, worker } = fixture(), state = new TerrainState(properties); controller.reset( new SectionBlockMap([ ["16,0,0", 1], ["17,0,0", 1], ]), materials, new Map(), bounds, ); controller.flush(); state.sync(worker.packets[0]); controller.update({ changes: [{ pos: [17, 0, 0], block: 2 }] }); controller.update({ unload: [[1, 0, 0]] }); controller.update({ changes: [{ pos: [16, 0, 0], block: 2 }] }); controller.flush(); state.sync(worker.packets[1]); assert.deepEqual([...state.blocks], [["16,0,0", 2]]); }); test("complete sections replace queued edits and replay newer edits after their numeric records", () => { const { controller, worker } = fixture(), state = new TerrainState(properties); controller.reset(new SectionBlockMap([["16,0,0", 1], ["17,0,0", 1]]), materials, new Map(), bounds); controller.flush(); state.sync(worker.packets[0]); controller.update({ changes: [{ pos: [17,0,0], block: 2 }] }); controller.update({ sections: [{ section: [1,0,0], blocks: [{ pos: [18,0,0], block: 1 }] }] }); controller.update({ changes: [{ pos: [18,0,0], block: 2 }] }); controller.flush(); state.sync(worker.packets[1]); assert.deepEqual([...state.blocks], [["18,0,0", 2]]); assert.deepEqual([...worker.packets[1].changes], [18,0,0,1,18,0,0,2]); controller.update({ sections: [{ section: [1,0,0], blocks: [{ pos: [19,0,0], block: 1 }] }] }); controller.update({ unload: [[1,0,0]] }); controller.update({ sections: [{ section: [1,0,0], blocks: [{ pos: [20,0,0], block: 2 }] }] }); controller.flush(); state.sync(worker.packets[2]); assert.deepEqual([...state.blocks], [["20,0,0", 2]]); controller.update({ sections: [{ section: [1,0,0], blocks: [] }] }); controller.flush(); state.sync(worker.packets[3]); assert.equal(state.blocks.size, 0, "An empty complete section clears old contents"); }); test("streamed section records are packed at flush without indexing each voxel on arrival", () => { const { controller, worker } = fixture(); controller.reset(new SectionBlockMap(), materials, new Map(), bounds); controller.flush(); const records = [{ pos: [16,0,0], block: 1 }]; controller.changes.set = () => { throw Error("Section records must not pass through the per-edit index"); }; controller.update({ sections: [{ section: [1,0,0], blocks: records }] }); assert.equal(controller.sectionChanges.get("1,0,0"), records); controller.flush(); assert.deepEqual([...worker.packets[1].changes], [16,0,0,1]); assert.equal(controller.sectionChanges.size, 0); }); test("runtime worker errors notify the renderer asynchronously and stop after disposal", async t => { t.mock.method(console, "error", () => {}); const worker = new FakeWorker(), errors = []; const controller = new TerrainController({ workerFactory: () => worker, onError: message => errors.push(message) }); controllers.push(controller); let prevented = false; worker.onerror({ message: "Worker stopped", preventDefault() { prevented = true; } }); assert.equal(prevented, true); assert.deepEqual(errors, []); await Promise.resolve(); assert.deepEqual(errors, ["Worker stopped"]); worker.onerror({ message: "Obsolete error", preventDefault() {} }); controller.dispose(); await Promise.resolve(); assert.deepEqual(errors, ["Worker stopped"]); });