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.
359 lines
11 KiB
JavaScript
359 lines
11 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { SectionBlockMap } from "../section-block-map.js";
|
|
import {
|
|
getViewBounds,
|
|
mergeChunkUpdate,
|
|
mergeSnapshot,
|
|
positionInView,
|
|
} from "../world-view.js";
|
|
|
|
function view() {
|
|
return {
|
|
world: "lobby",
|
|
revision: 7,
|
|
viewCenter: [0, 0, 0],
|
|
viewBounds: { min: [-32, -16, -32], max: [31, 31, 31] },
|
|
blocks: new Map([
|
|
["0,0,0", 1],
|
|
["31,0,0", 2],
|
|
["-32,0,0", 3],
|
|
]),
|
|
};
|
|
}
|
|
|
|
function moveEast() {
|
|
const message = {
|
|
type: "chunks",
|
|
world: "lobby",
|
|
revision: 7,
|
|
from_center: [0, 0, 0],
|
|
view_center: [16, 0, 0],
|
|
view_min: [-16, -16, -32],
|
|
view_max: [47, 31, 31],
|
|
unload: [],
|
|
sections: [],
|
|
};
|
|
for (const y of [-1, 0, 1])
|
|
for (const z of [-2, -1, 0, 1]) {
|
|
message.unload.push([-2, y, z]);
|
|
message.sections.push({ section: [2, y, z], blocks: [] });
|
|
}
|
|
message.sections.find((s) => s.section.join(",") === "2,0,0").blocks = [
|
|
{ pos: [32, 0, 0], block: 4 },
|
|
];
|
|
return message;
|
|
}
|
|
|
|
test("crossing a section boundary retains overlapping blocks and map identity", () => {
|
|
const current = view(),
|
|
blocks = current.blocks,
|
|
result = mergeChunkUpdate(current, moveEast());
|
|
assert.equal(result.status, "applied");
|
|
assert.equal(current.blocks, blocks);
|
|
assert.equal(current.revision, 7);
|
|
assert.deepEqual(current.viewCenter, [16, 0, 0]);
|
|
assert.deepEqual(
|
|
[...current.blocks],
|
|
[
|
|
["0,0,0", 1],
|
|
["31,0,0", 2],
|
|
["32,0,0", 4],
|
|
],
|
|
);
|
|
assert.deepEqual(result.changes, [
|
|
{ pos: [-32, 0, 0], block: 0 },
|
|
{ pos: [32, 0, 0], block: 4 },
|
|
]);
|
|
});
|
|
|
|
test("an empty incoming section clears any stale block without touching overlap", () => {
|
|
const current = view();
|
|
current.blocks.set("32,0,16", 99);
|
|
const result = mergeChunkUpdate(current, moveEast());
|
|
assert.equal(result.status, "applied");
|
|
assert.equal(current.blocks.has("32,0,16"), false);
|
|
assert.ok(
|
|
result.changes.some(
|
|
(change) => change.pos.join(",") === "32,0,16" && !change.block,
|
|
),
|
|
);
|
|
assert.equal(current.blocks.get("0,0,0"), 1);
|
|
});
|
|
|
|
test("negative coordinates use floor division when loading and unloading sections", () => {
|
|
const current = view(),
|
|
message = moveEast();
|
|
current.viewCenter = [-16, 0, 0];
|
|
current.blocks = new Map([
|
|
["-1,0,0", 1],
|
|
["0,0,0", 2],
|
|
["-48,0,0", 3],
|
|
]);
|
|
message.from_center = [-16, 0, 0];
|
|
message.view_center = [-32, 0, 0];
|
|
message.view_min = [-64, -16, -32];
|
|
message.view_max = [-1, 31, 31];
|
|
message.unload = message.unload.map(([, y, z]) => [0, y, z]);
|
|
message.sections = message.sections.map(({ section: [, y, z] }) => ({
|
|
section: [-4, y, z],
|
|
blocks: y === 0 && z === 0 ? [{ pos: [-49, 0, 0], block: 4 }] : [],
|
|
}));
|
|
const result = mergeChunkUpdate(current, message);
|
|
assert.equal(result.status, "applied");
|
|
assert.deepEqual(
|
|
[...current.blocks],
|
|
[
|
|
["-1,0,0", 1],
|
|
["-48,0,0", 3],
|
|
["-49,0,0", 4],
|
|
],
|
|
);
|
|
});
|
|
|
|
test("same-world full snapshots only report actual changes and retain the map", () => {
|
|
const current = view(),
|
|
blocks = current.blocks,
|
|
records = [...blocks].map(([id, block]) => ({
|
|
pos: id.split(",").map(Number),
|
|
block,
|
|
}));
|
|
assert.deepEqual(mergeSnapshot(blocks, records), []);
|
|
assert.equal(current.blocks, blocks);
|
|
records.pop();
|
|
records.push({ pos: [32, 0, 0], block: 4 });
|
|
assert.deepEqual(mergeSnapshot(blocks, records), [
|
|
{ pos: [-32, 0, 0], block: 0 },
|
|
{ pos: [32, 0, 0], block: 4 },
|
|
]);
|
|
assert.equal(blocks.get("0,0,0"), 1);
|
|
});
|
|
|
|
test("newer or older chunk revisions request resync without advancing revision", () => {
|
|
for (const revision of [6, 8]) {
|
|
const current = view(),
|
|
before = structuredClone(current),
|
|
message = { ...moveEast(), revision };
|
|
assert.equal(mergeChunkUpdate(current, message).status, "resync");
|
|
assert.deepEqual(current, before);
|
|
}
|
|
});
|
|
|
|
test("an out-of-order or duplicate transition cannot overwrite the current window", () => {
|
|
const current = view(),
|
|
message = moveEast();
|
|
assert.equal(mergeChunkUpdate(current, message).status, "applied");
|
|
const before = structuredClone(current);
|
|
assert.equal(mergeChunkUpdate(current, message).status, "resync");
|
|
assert.deepEqual(current, before);
|
|
assert.equal(
|
|
mergeChunkUpdate(current, { ...message, world: "other" }).status,
|
|
"ignored",
|
|
);
|
|
assert.deepEqual(current, before);
|
|
});
|
|
|
|
test("incomplete or malformed transitions do not partially unload a live world", () => {
|
|
const invalidMessages = [
|
|
(m) => m.sections.pop(),
|
|
(m) => m.unload.pop(),
|
|
(m) => m.unload.push([0, 0, 0]),
|
|
(m) => (m.view_min[1] = -8),
|
|
(m) => m.sections[0].blocks.push({ pos: [0, 0, 0], block: 5 }),
|
|
(m) => m.sections[0].blocks.push({ pos: [32, -16, -32], block: -1 }),
|
|
];
|
|
for (const invalidate of invalidMessages) {
|
|
const current = view(),
|
|
before = structuredClone(current),
|
|
message = moveEast();
|
|
invalidate(message);
|
|
assert.equal(mergeChunkUpdate(current, message).status, "resync");
|
|
assert.deepEqual(current, before);
|
|
}
|
|
});
|
|
|
|
test("explicit view bounds include the entire bottom section with legacy fallback", () => {
|
|
const streamed = getViewBounds(moveEast()),
|
|
legacy = getViewBounds({ view_center: [16, 0, 0] });
|
|
assert.equal(positionInView([0, -16, 0], streamed), true);
|
|
assert.equal(positionInView([0, -17, 0], streamed), false);
|
|
assert.equal(positionInView([0, -16, 0], legacy), false);
|
|
assert.equal(positionInView([0, -8, 0], legacy), true);
|
|
assert.equal(positionInView([48, 0, 0], streamed), false);
|
|
});
|
|
|
|
test("indexed transitions never iterate or read retained blocks", () => {
|
|
class IndexedBlocks extends SectionBlockMap {
|
|
[Symbol.iterator]() {
|
|
throw Error("A chunk transition must not scan the complete map");
|
|
}
|
|
get(key) {
|
|
assert.ok(!["0,0,0", "31,0,0"].includes(key), "Retained block was read");
|
|
return super.get(key);
|
|
}
|
|
}
|
|
const current = view();
|
|
current.blocks = new IndexedBlocks(current.blocks);
|
|
current.blocks.set("-31,0,0", 8);
|
|
current.blocks.set("32,0,16", 99);
|
|
current.blocks.set("32,0,0", 3);
|
|
const result = mergeChunkUpdate(current, moveEast());
|
|
assert.equal(result.status, "applied");
|
|
assert.deepEqual(
|
|
[...current.blocks.entries()],
|
|
[
|
|
["0,0,0", 1],
|
|
["31,0,0", 2],
|
|
["32,0,0", 4],
|
|
],
|
|
);
|
|
assert.deepEqual([...current.blocks.loadedSectionKeys()].sort(), [
|
|
"0,0,0",
|
|
"1,0,0",
|
|
"2,0,0",
|
|
]);
|
|
assert.deepEqual(
|
|
result.changes.map(({ pos, block }) => [pos.join(","), block]).sort(),
|
|
[
|
|
["-31,0,0", 0],
|
|
["-32,0,0", 0],
|
|
["32,0,0", 4],
|
|
["32,0,16", 0],
|
|
],
|
|
);
|
|
});
|
|
|
|
test("malformed transitions preserve the section index before any indexed access", () => {
|
|
class IndexedBlocks extends SectionBlockMap {
|
|
keysInSection() {
|
|
throw Error("Incomplete transition accessed the index");
|
|
}
|
|
}
|
|
const current = view();
|
|
current.blocks = new IndexedBlocks(current.blocks);
|
|
const beforeBlocks = [...current.blocks],
|
|
beforeSections = [...current.blocks.loadedSectionKeys()],
|
|
beforeCenter = current.viewCenter,
|
|
beforeBounds = current.viewBounds,
|
|
message = moveEast();
|
|
message.sections.at(-1).blocks.push({ pos: [32, 16, 16], block: -1 });
|
|
assert.equal(mergeChunkUpdate(current, message).status, "resync");
|
|
assert.deepEqual([...current.blocks], beforeBlocks);
|
|
assert.deepEqual([...current.blocks.loadedSectionKeys()], beforeSections);
|
|
assert.equal(current.viewCenter, beforeCenter);
|
|
assert.equal(current.viewBounds, beforeBounds);
|
|
});
|
|
|
|
test("full snapshot merges maintain the section index alongside the block map", () => {
|
|
const blocks = new SectionBlockMap(view().blocks);
|
|
mergeSnapshot(blocks, [
|
|
{ pos: [0, 0, 0], block: 1 },
|
|
{ pos: [32, -1, 0], block: 4 },
|
|
]);
|
|
assert.deepEqual([...blocks.loadedSectionKeys()], ["0,0,0", "2,-1,0"]);
|
|
assert.deepEqual([...blocks.keysInSection("-2,0,0")], []);
|
|
assert.deepEqual([...blocks.keysInSection("2,-1,0")], ["32,-1,0"]);
|
|
});
|
|
|
|
test("optional section unloads omit only departing changes for indexed and ordinary maps", () => {
|
|
for (const BlockMap of [Map, SectionBlockMap]) {
|
|
const current = view();
|
|
current.blocks = new BlockMap(current.blocks);
|
|
current.blocks.set("32,0,16", 99);
|
|
const result = mergeChunkUpdate(current, moveEast(), {
|
|
includeUnloadedChanges: false,
|
|
});
|
|
assert.equal(result.status, "applied");
|
|
assert.equal(current.blocks.has("-32,0,0"), false);
|
|
assert.deepEqual(result.changes, [
|
|
{ pos: [32, 0, 16], block: 0 },
|
|
{ pos: [32, 0, 0], block: 4 },
|
|
]);
|
|
assert.deepEqual(
|
|
[...current.blocks],
|
|
[
|
|
["0,0,0", 1],
|
|
["31,0,0", 2],
|
|
["32,0,0", 4],
|
|
],
|
|
);
|
|
}
|
|
});
|
|
|
|
test("bulk streamed merges avoid departing key access and use validated sections for insertion", () => {
|
|
class BulkBlocks extends SectionBlockMap {
|
|
set(key, value) {
|
|
assert.equal(
|
|
this.locked,
|
|
undefined,
|
|
"Incoming records reparsed through ordinary set",
|
|
);
|
|
return super.set(key, value);
|
|
}
|
|
delete() {
|
|
throw Error("Departing blocks were deleted individually");
|
|
}
|
|
keysInSection(section) {
|
|
assert.ok(
|
|
!section.startsWith("-2,"),
|
|
"Departing block keys were requested",
|
|
);
|
|
return super.keysInSection(section);
|
|
}
|
|
[Symbol.iterator]() {
|
|
throw Error("Streamed merge scanned the full map");
|
|
}
|
|
}
|
|
const current = view(),
|
|
message = moveEast();
|
|
current.blocks = new BulkBlocks(current.blocks);
|
|
current.blocks.locked = true;
|
|
const result = mergeChunkUpdate(current, message, {
|
|
includeUnloadedChanges: false,
|
|
});
|
|
assert.equal(result.status, "applied");
|
|
assert.equal(current.blocks.has("-32,0,0"), false);
|
|
assert.equal(current.blocks.get("32,0,0"), 4);
|
|
assert.deepEqual(result.changes, [{ pos: [32, 0, 0], block: 4 }]);
|
|
assert.deepEqual(
|
|
[...current.blocks.loadedSectionKeys()],
|
|
["0,0,0", "1,0,0", "2,0,0"],
|
|
);
|
|
});
|
|
|
|
test("bulk merges still validate all incoming records before deleting any section", () => {
|
|
for (const invalidRecord of [
|
|
{ pos: [32, 16, 16], block: -1 },
|
|
{ pos: [31, 16, 16], block: 4 },
|
|
{ pos: [32, 16.5, 16], block: 4 },
|
|
]) {
|
|
const current = view(),
|
|
message = moveEast();
|
|
current.blocks = new SectionBlockMap(current.blocks);
|
|
const before = [...current.blocks],
|
|
sectionsBefore = [...current.blocks.loadedSectionKeys()],
|
|
centerBefore = current.viewCenter;
|
|
message.sections.at(-1).blocks.push(invalidRecord);
|
|
assert.equal(
|
|
mergeChunkUpdate(current, message, { includeUnloadedChanges: false })
|
|
.status,
|
|
"resync",
|
|
);
|
|
assert.deepEqual([...current.blocks], before);
|
|
assert.deepEqual([...current.blocks.loadedSectionKeys()], sectionsBefore);
|
|
assert.equal(current.viewCenter, centerBefore);
|
|
}
|
|
const current = view(),
|
|
message = moveEast();
|
|
current.blocks = new SectionBlockMap(current.blocks);
|
|
message.sections
|
|
.find((s) => s.section.join(",") === "2,0,0")
|
|
.blocks.push({ pos: [32, 0, 0], block: 0 });
|
|
assert.equal(
|
|
mergeChunkUpdate(current, message, { includeUnloadedChanges: false })
|
|
.status,
|
|
"resync",
|
|
);
|
|
assert.equal(current.blocks.get("-32,0,0"), 3);
|
|
});
|