MVP checks / mvp (push) Canceled after 0s
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.
254 lines
9.7 KiB
JavaScript
254 lines
9.7 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import { TerrainState } from "../terrain-state.js";
|
|
import { MESH_VERTEX_STRIDE } from "../mesh-geometry.js";
|
|
|
|
const properties = JSON.parse(
|
|
await readFile(new URL("../light-properties.json", import.meta.url)),
|
|
);
|
|
const cube = { min: [0, 0, 0], max: [1, 1, 1] };
|
|
const stone = {
|
|
state: "minecraft:stone",
|
|
color: [128, 128, 128],
|
|
render: [cube],
|
|
};
|
|
const lamp = {
|
|
state: "minecraft:sea_lantern",
|
|
color: [240, 250, 250],
|
|
render: [cube],
|
|
light: 15,
|
|
};
|
|
const bounds = { min: [-2, -2, -2], max: [18, 3, 3] };
|
|
function packet(overrides = {}) {
|
|
return {
|
|
type: "sync",
|
|
epoch: 1,
|
|
version: 1,
|
|
reset: true,
|
|
initial: new Int32Array([0, 0, 0, 1]),
|
|
changes: new Int32Array(),
|
|
materials: [
|
|
[1, stone],
|
|
[2, lamp],
|
|
],
|
|
textures: [],
|
|
bounds,
|
|
unload: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
const request = (state, id = "0,0,0") => ({
|
|
epoch: state.epoch,
|
|
version: state.version,
|
|
id,
|
|
job: 1,
|
|
});
|
|
|
|
test("reset plus deltas preserves numeric coordinates and authoritative material lighting metadata", () => {
|
|
const state = new TerrainState(properties);
|
|
assert.equal(
|
|
state.sync(
|
|
packet({
|
|
initial: new Int32Array([-1, 0, 0, 1, 16, 0, 0, 1]),
|
|
changes: new Int32Array([-1, 0, 0, 0, 16, 0, 0, 2]),
|
|
}),
|
|
),
|
|
true,
|
|
);
|
|
assert.deepEqual([...state.blocks], [["16,0,0", 2]]);
|
|
assert.deepEqual([...state.blocks.loadedSectionKeys()], ["1,0,0"]);
|
|
assert.equal(state.materials.get(1).light_dampening, 15);
|
|
assert.equal(stone.light_dampening, undefined);
|
|
assert.equal(state.mesh(request(state, "1,0,0")).stale, true);
|
|
const result = state.light();
|
|
assert.equal(result.field.sourceCount, 1);
|
|
assert.equal(state.field.sample([16, 0, 0]).blockLevel, 15);
|
|
assert.notEqual(result.field.data.buffer, state.field.data.buffer);
|
|
assert.notEqual(
|
|
result.field.blockLevels.buffer,
|
|
state.field.blockLevels.buffer,
|
|
);
|
|
assert.notEqual(result.field.skyLevels.buffer, state.field.skyLevels.buffer);
|
|
assert.ok(state.mesh(request(state, "1,0,0")).vertices.length > 0);
|
|
});
|
|
|
|
test("outdated syncs and mesh requests cannot overwrite a new version or world", () => {
|
|
const state = new TerrainState(properties);
|
|
state.sync(packet());
|
|
state.light();
|
|
state.sync(
|
|
packet({ epoch: 2, version: 5, initial: new Int32Array([16, 0, 0, 2]) }),
|
|
);
|
|
assert.equal(state.sync(packet({ version: 100 })), false);
|
|
assert.equal(state.sync(packet({ epoch: 2, version: 4 })), false);
|
|
assert.equal(
|
|
state.sync(packet({ epoch: 3, version: 6, reset: false })),
|
|
false,
|
|
);
|
|
assert.deepEqual([...state.blocks], [["16,0,0", 2]]);
|
|
assert.equal(state.field, null);
|
|
assert.equal(
|
|
state.mesh({ epoch: 1, version: 1, id: "0,0,0", job: 1 }).stale,
|
|
true,
|
|
);
|
|
state.light();
|
|
assert.equal(
|
|
state.mesh({ epoch: 2, version: 4, id: "1,0,0", job: 2 }).stale,
|
|
true,
|
|
);
|
|
assert.ok(state.mesh(request(state, "1,0,0")).vertices.length > 0);
|
|
});
|
|
|
|
test("unloaded sections disappear from persistent storage and cannot be resurrected by old syncs", () => {
|
|
const state = new TerrainState(properties);
|
|
state.sync(packet({ initial: new Int32Array([0, 0, 0, 1, 16, 0, 0, 2]) }));
|
|
state.light();
|
|
state.mesh(request(state, "1,0,0"));
|
|
assert.equal(state.meshed.has("1,0,0"), true);
|
|
state.sync(
|
|
packet({
|
|
reset: false,
|
|
version: 2,
|
|
initial: null,
|
|
materials: [],
|
|
unload: [[1, 0, 0]],
|
|
}),
|
|
);
|
|
assert.equal(state.blocks.has("16,0,0"), false);
|
|
assert.deepEqual([...state.blocks.keysInSection("1,0,0")], []);
|
|
assert.equal(state.meshed.has("1,0,0"), false);
|
|
state.light();
|
|
assert.equal(state.mesh(request(state, "1,0,0")).vertices.length, 0);
|
|
assert.equal(
|
|
state.sync(packet({ initial: new Int32Array([16, 0, 0, 2]) })),
|
|
false,
|
|
);
|
|
assert.equal(state.blocks.has("16,0,0"), false);
|
|
});
|
|
|
|
test("material and texture replacements affect the next mesh and invalidate face texture cache", () => {
|
|
const state = new TerrainState(properties);
|
|
state.sync(packet({ textures: [["stone", 3]] }));
|
|
state.light();
|
|
const first = state.mesh(request(state));
|
|
assert.equal(first.vertices.length, 36 * MESH_VERTEX_STRIDE);
|
|
assert.equal(first.vertices[13], 3);
|
|
assert.equal(state.faceTextureCache.get("minecraft:stone")[0].layer, 3);
|
|
state.sync(
|
|
packet({
|
|
reset: false,
|
|
version: 2,
|
|
initial: null,
|
|
materials: [
|
|
[
|
|
1,
|
|
{
|
|
...stone,
|
|
render: [{ min: [0, 0, 0], max: [1, 0.5, 1] }],
|
|
light: 7,
|
|
},
|
|
],
|
|
],
|
|
textures: [["stone", 9]],
|
|
}),
|
|
);
|
|
assert.equal(state.faceTextureCache.size, 0);
|
|
assert.equal(state.mesh(request(state)).stale, true);
|
|
state.light();
|
|
const next = state.mesh(request(state));
|
|
for (let i = 0; i < next.vertices.length; i += MESH_VERTEX_STRIDE) {
|
|
assert.ok(next.vertices[i + 1] <= 0.5);
|
|
assert.equal(next.vertices[i + 13], 9);
|
|
assert.ok(Math.abs(next.vertices[i + 15] - 7 / 15) < 1e-6);
|
|
}
|
|
assert.equal(state.faceTextureCache.get("minecraft:stone")[0].layer, 9);
|
|
});
|
|
|
|
test("lighting changes keep emptied meshed sections dirty until their stale geometry is replaced", () => {
|
|
const state = new TerrainState(properties);
|
|
state.sync(packet({ initial: new Int32Array([0, 0, 0, 2]) }));
|
|
state.light();
|
|
state.mesh(request(state));
|
|
state.sync(
|
|
packet({
|
|
reset: false,
|
|
version: 2,
|
|
initial: null,
|
|
materials: [],
|
|
changes: new Int32Array([0, 0, 0, 0]),
|
|
}),
|
|
);
|
|
assert.equal(state.blocks.size, 0);
|
|
assert.ok(state.light().dirty.includes("0,0,0"));
|
|
assert.equal(state.mesh(request(state)).vertices.length, 0);
|
|
});
|
|
|
|
test("a retained unknown cube remeshes when its same-light definition arrives", () => {
|
|
const state = new TerrainState(properties);
|
|
state.sync(packet({ initial: new Int32Array([15, 0, 0, 99, 16, 0, 0, 1]) }));
|
|
state.light();
|
|
const before = state.mesh(request(state));
|
|
state.mesh(request(state, "1,0,0"));
|
|
const oldLight = state.field.data.slice(), oldSky = state.field.skyLevels.slice();
|
|
const defined = { ...stone, color: [60, 70, 80] };
|
|
state.sync(packet({ reset: false, version: 2, initial: null, materials: [[99, defined]] }));
|
|
const result = state.light();
|
|
assert.deepEqual(state.field.data, oldLight);
|
|
assert.deepEqual(state.field.skyLevels, oldSky);
|
|
assert.ok(result.dirty.includes("0,0,0"), "the placeholder's owner must be rebuilt even with identical lighting");
|
|
assert.ok(result.dirty.includes("1,0,0"), "neighbor culling/AO must also be refreshed");
|
|
const after = state.mesh(request(state));
|
|
assert.notDeepEqual(after.vertices.slice(3, 6), before.vertices.slice(3, 6));
|
|
assert.deepEqual([...after.vertices.slice(3, 6)], defined.color.map(value => Math.fround(value / 255)));
|
|
});
|
|
|
|
test("repeated equivalent material definitions do not spuriously dirty retained sections", () => {
|
|
const state = new TerrainState(properties);
|
|
state.sync(packet());
|
|
state.light(); state.mesh(request(state));
|
|
const current = state.materials.get(1);
|
|
const repeated = { render: [{ max: [1, 1, 1], min: [0, 0, 0] }], color: [128, 128, 128],
|
|
state: "minecraft:stone", ignoredMetadata: "does not affect the transmitted material" };
|
|
state.sync(packet({ reset: false, version: 2, initial: null, materials: [[1, repeated]] }));
|
|
assert.equal(state.materials.get(1), current);
|
|
assert.deepEqual(state.light().dirty, []);
|
|
});
|
|
|
|
test("a color-only definition replacement invalidates its mesh without a light delta", () => {
|
|
const state = new TerrainState(properties);
|
|
state.sync(packet());
|
|
state.light(); state.mesh(request(state));
|
|
const oldLight = state.field.data.slice(), oldSky = state.field.skyLevels.slice();
|
|
state.sync(packet({ reset: false, version: 2, initial: null, materials: [[1, { ...stone, color: [220, 70, 30] }]] }));
|
|
assert.ok(state.light().dirty.includes("0,0,0"));
|
|
assert.deepEqual(state.field.data, oldLight);
|
|
assert.deepEqual(state.field.skyLevels, oldSky);
|
|
assert.deepEqual([...state.mesh(request(state)).vertices.slice(3, 6)], [220, 70, 30].map(value => Math.fround(value / 255)));
|
|
});
|
|
|
|
test("material invalidations from separate coalesced syncs retain their union", () => {
|
|
const state = new TerrainState(properties), wide = { min: [-2, -2, -2], max: [50, 3, 3] };
|
|
state.sync(packet({ initial: new Int32Array([0, 0, 0, 1, 48, 0, 0, 3]), materials: [[1, stone], [3, stone]], bounds: wide }));
|
|
state.light(); state.mesh(request(state)); state.mesh(request(state, "3,0,0"));
|
|
state.sync(packet({ reset: false, version: 2, initial: null, materials: [[1, { ...stone, color: [10, 20, 30] }]], bounds: wide }));
|
|
state.sync(packet({ reset: false, version: 3, initial: null, materials: [[3, { ...stone, color: [30, 20, 10] }]], bounds: wide }));
|
|
assert.deepEqual(new Set(state.light().dirty), new Set(["0,0,0", "3,0,0"]));
|
|
assert.deepEqual(state.light().dirty, [], "the material union is consumed after a successful field publication");
|
|
});
|
|
|
|
test("world reset clears pending material geometry invalidations", () => {
|
|
const state = new TerrainState(properties), wide = { min: [-2, -2, -2], max: [50, 3, 3] };
|
|
state.sync(packet()); state.light(); state.mesh(request(state));
|
|
state.sync(packet({ reset: false, version: 2, initial: null, materials: [[1, { ...stone, color: [10, 20, 30] }]] }));
|
|
state.sync(packet({ epoch: 2, version: 3, initial: new Int32Array([48, 0, 0, 1]), materials: [[1, stone]], bounds: wide }));
|
|
assert.deepEqual(state.light().dirty, ["3,0,0"]);
|
|
});
|
|
|
|
test("a new definition absent from the loaded blocks does not remesh terrain", () => {
|
|
const state = new TerrainState(properties);
|
|
state.sync(packet()); state.light(); state.mesh(request(state));
|
|
state.sync(packet({ reset: false, version: 2, initial: null, materials: [[99, lamp]] }));
|
|
assert.deepEqual(state.light().dirty, []);
|
|
});
|