Expand voxel gameplay, lighting, full-height streaming and world imports
MVP checks / mvp (push) Canceled after 0s
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.
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user