MVP checks / mvp (push) Waiting to run
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.
228 lines
7.4 KiB
JavaScript
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");
|
|
});
|