Files
shacraft-core/client/tests/block-light-controller.test.js
Emil c7e86663d8
MVP checks / mvp (push) Waiting to run
Expand voxel gameplay, lighting, full-height streaming and world imports
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
2026-09-17 02:10:53 +03:00

228 lines
7.4 KiB
JavaScript

import test, { after, afterEach } from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { BlockLightController } from "../block-light-controller.js";
import { buildBlockLight } from "../block-light.js";
import { loadLightProperties } from "../light-properties.js";
const properties = JSON.parse(
await readFile(new URL("../light-properties.json", import.meta.url)),
);
const savedWorker = globalThis.Worker,
savedFetch = globalThis.fetch;
const controllers = [];
class FakeWorker {
constructor() {
this.packets = [];
this.terminated = false;
}
postMessage(packet) {
this.packets.push(structuredClone(packet));
}
terminate() {
this.terminated = true;
}
reply(index) {
const packet = this.packets[index];
const { min, size, data, sourceCount, blockLevels, skyLevels } =
buildBlockLight(
new Map(packet.blocks),
new Map(packet.materials),
packet.bounds,
);
this.onmessage({
data: {
generation: packet.generation,
field: { min, size, data, sourceCount, blockLevels, skyLevels },
milliseconds: 1,
},
});
}
}
globalThis.Worker = FakeWorker;
globalThis.fetch = async () => ({ ok: true, json: async () => properties });
afterEach(() => {
for (const controller of controllers.splice(0)) {
clearTimeout(controller.timer);
controller.worker?.terminate();
}
});
after(() => {
if (savedWorker === undefined) delete globalThis.Worker;
else globalThis.Worker = savedWorker;
globalThis.fetch = savedFetch;
});
const bounds = { min: [-2, 0, -2], max: [2, 3, 2] };
const materials = new Map([
[1, { state: "minecraft:torch", light: 14, render: [] }],
[
2,
{
state: "minecraft:sea_lantern",
light: 15,
render: [{ min: [0, 0, 0], max: [1, 1, 1] }],
},
],
]);
function fixture() {
const published = [];
const controller = new BlockLightController((field) => published.push(field));
controllers.push(controller);
return { controller, worker: controller.worker, published };
}
test("requests awaiting metadata coalesce into the latest complete world", async () => {
const { controller, worker, published } = fixture();
controller.request(new Map([["0,1,0", 1]]), materials, bounds);
controller.request(new Map([["1,1,0", 2]]), materials, bounds);
await loadLightProperties();
controller.start();
assert.equal(worker.packets.length, 1);
assert.equal(worker.packets[0].generation, 2);
assert.deepEqual(worker.packets[0].blocks, [["1,1,0", 2]]);
worker.reply(0);
assert.equal(published.length, 1);
assert.equal(published[0].sample([1, 1, 0]).blockLevel, 15);
assert.equal(controller.status, "ready");
});
test("an old world's in-flight result is discarded before publishing the replacement world", async () => {
const { controller, worker, published } = fixture();
await loadLightProperties();
const first = new Map([["0,1,0", 1]]);
controller.request(first, materials, bounds);
controller.start();
controller.request(new Map([["1,1,0", 2]]), materials, bounds);
first.clear();
assert.equal(worker.packets.length, 1);
worker.reply(0);
assert.equal(published.length, 0);
assert.equal(worker.packets.length, 2);
worker.reply(1);
assert.equal(published.length, 1);
assert.equal(published[0].sample([1, 1, 0]).blockLevel, 15);
assert.equal(controller.pending, null);
assert.equal(controller.busy, false);
});
test("rapid edits during a build coalesce while the last valid field remains available", async () => {
const { controller, worker, published } = fixture();
await loadLightProperties();
const blocks = new Map([["0,1,0", 1]]);
controller.request(blocks, materials, bounds);
controller.start();
worker.reply(0);
const retained = published[0];
blocks.set("0,1,0", 2);
controller.request(blocks, materials, bounds);
controller.start();
blocks.set("1,1,0", 1);
controller.request(blocks, materials, bounds);
blocks.clear();
controller.request(blocks, materials, bounds);
assert.equal(published.length, 1);
assert.equal(retained.sample([0, 1, 0]).blockLevel, 14);
worker.reply(1);
assert.equal(worker.packets.length, 3);
assert.equal(worker.packets[2].generation, 4);
assert.deepEqual(worker.packets[2].blocks, []);
assert.equal(published.length, 1);
worker.reply(2);
assert.equal(published.length, 2);
assert.equal(published[1].sourceCount, 0);
assert.equal(published[1].sample([0, 1, 0]).blockLevel, 0);
});
test("worker failure recovers using the newest queued world and the same solver", async () => {
const { controller, worker, published } = fixture();
await loadLightProperties();
controller.request(new Map([["0,1,0", 1]]), materials, bounds);
controller.start();
controller.request(new Map([["1,1,0", 2]]), materials, bounds);
let prevented = false;
worker.onerror({
preventDefault() {
prevented = true;
},
});
assert.equal(prevented, true);
assert.equal(worker.terminated, true);
assert.equal(controller.worker, null);
// Resolve through publish rather than a timing assumption about the fallback.
await new Promise((resolve, reject) => {
const timeout = setTimeout(
() => reject(Error("Fallback lighting did not finish")),
1000,
);
const publish = controller.publish;
controller.publish = (field) => {
clearTimeout(timeout);
publish(field);
resolve();
};
});
assert.equal(published.length, 1);
assert.equal(published[0].sample([1, 1, 0]).blockLevel, 15);
assert.equal(controller.status, "ready");
});
test("a duplicate stale reply cannot mark a newer build idle or start a concurrent job", async () => {
const { controller, worker, published } = fixture();
await loadLightProperties();
controller.request(new Map([["0,1,0", 1]]), materials, bounds);
controller.start();
controller.request(new Map([["1,1,0", 2]]), materials, bounds);
worker.reply(0);
assert.equal(worker.packets.length, 2);
assert.equal(controller.busy, true);
worker.reply(0);
assert.equal(controller.busy, true);
controller.request(new Map([["-1,1,0", 1]]), materials, bounds);
controller.start();
assert.equal(worker.packets.length, 2);
assert.equal(published.length, 0);
worker.reply(1);
assert.equal(worker.packets.length, 3);
worker.reply(2);
assert.equal(published.length, 1);
assert.equal(published[0].sample([-1, 1, 0]).blockLevel, 14);
worker.reply(2);
assert.equal(published.length, 1);
});
test("synchronous Worker construction failure still publishes light from the fallback solver", async () => {
const previousWorker = globalThis.Worker;
globalThis.Worker = class {
constructor() {
throw new Error("Worker blocked by browser policy");
}
};
let context;
try {
context = fixture();
} finally {
globalThis.Worker = previousWorker;
}
const { controller, published } = context;
assert.equal(controller.worker, null);
const completed = new Promise((resolve, reject) => {
const timeout = setTimeout(
() => reject(Error("Fallback lighting did not finish")),
1000,
);
const publish = controller.publish;
controller.publish = (field) => {
clearTimeout(timeout);
publish(field);
resolve();
};
});
controller.request(new Map([["0,1,0", 1]]), materials, bounds);
await loadLightProperties();
controller.start();
await completed;
assert.equal(published.length, 1);
assert.equal(published[0].sample([0, 1, 0]).blockLevel, 14);
assert.equal(controller.status, "ready");
});