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.
294 lines
9.8 KiB
JavaScript
294 lines
9.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { harness, Client, options } from "./server-harness.mjs";
|
|
import {
|
|
getViewBounds,
|
|
mergeChunkUpdate,
|
|
mergeSnapshot,
|
|
positionInView,
|
|
} from "../client/world-view.js";
|
|
|
|
// Isolated data and port: this check never edits the running demo world.
|
|
const opts = { port: "4012", ...options() },
|
|
h = await harness(opts),
|
|
clients = [],
|
|
checks = [],
|
|
transitions = [],
|
|
world = "test_chunk_stream";
|
|
const key = (pos) => pos.join(","),
|
|
sorted = (map) => [...map].sort(([a], [b]) => a.localeCompare(b)),
|
|
view = {
|
|
world,
|
|
revision: 0,
|
|
viewCenter: null,
|
|
viewBounds: null,
|
|
blocks: new Map(),
|
|
};
|
|
let receiveError,
|
|
explicitResync = false,
|
|
snapshots = 0;
|
|
|
|
function observe(message) {
|
|
if (receiveError) return;
|
|
try {
|
|
if (message.type === "error") throw Error(message.message);
|
|
if (["welcome", "snapshot"].includes(message.type)) {
|
|
if (message.type === "snapshot") {
|
|
assert.ok(explicitResync, "Walking must not send a full snapshot");
|
|
snapshots++;
|
|
}
|
|
assert.equal(message.world, world);
|
|
assert.ok(message.features.includes("chunk_stream_v1"));
|
|
view.revision = message.revision;
|
|
view.viewCenter = message.view_center;
|
|
view.viewBounds = getViewBounds(message);
|
|
mergeSnapshot(view.blocks, message.blocks);
|
|
} else if (message.type === "blocks") {
|
|
assert.equal(message.revision, view.revision + 1);
|
|
for (const change of message.changes) {
|
|
if (!positionInView(change.pos, view.viewBounds)) continue;
|
|
if (change.block) view.blocks.set(key(change.pos), change.block);
|
|
else view.blocks.delete(key(change.pos));
|
|
}
|
|
view.revision = message.revision;
|
|
} else if (message.type === "chunks") {
|
|
assert.ok(!("spawn" in message), "Movement must not reset the camera");
|
|
assert.ok(!("blocks" in message), "Movement must send section deltas");
|
|
assert.equal(message.sections.length, 12);
|
|
assert.equal(message.unload.length, 12);
|
|
assert.equal(
|
|
Math.abs(message.view_center[0] - message.from_center[0]),
|
|
16,
|
|
);
|
|
assert.deepEqual(message.view_center.slice(1), [0, 0]);
|
|
assert.ok(message.sections.some((section) => !section.blocks.length));
|
|
const enteringIds = new Set(
|
|
message.sections.flatMap((section) =>
|
|
section.blocks.map((record) => record.block),
|
|
),
|
|
),
|
|
materialIds = new Set(message.materials.map((material) => material.id));
|
|
assert.deepEqual(materialIds, enteringIds, "Only entering materials sent");
|
|
const departing = new Set(message.unload.map(key)),
|
|
retained = [...view.blocks].filter(
|
|
([id]) =>
|
|
!departing.has(
|
|
key(id.split(",").map((value) => Math.floor(Number(value) / 16))),
|
|
),
|
|
),
|
|
result = mergeChunkUpdate(view, message);
|
|
assert.equal(result.status, "applied", "Real client accepts server delta");
|
|
for (const [id, block] of retained)
|
|
assert.equal(view.blocks.get(id), block, "Retained geometry is unchanged");
|
|
transitions.push({
|
|
from: message.from_center,
|
|
to: message.view_center,
|
|
entering_sections: message.sections.length,
|
|
departing_sections: message.unload.length,
|
|
retained_sections: 48 - message.unload.length,
|
|
entering_blocks: message.sections.reduce(
|
|
(count, section) => count + section.blocks.length,
|
|
0,
|
|
),
|
|
retained_blocks: retained.length,
|
|
bytes: Buffer.byteLength(JSON.stringify(message)),
|
|
});
|
|
}
|
|
} catch (error) {
|
|
receiveError = error;
|
|
}
|
|
}
|
|
|
|
function assertReceived() {
|
|
if (receiveError) throw receiveError;
|
|
}
|
|
|
|
async function assertCurrentRegion() {
|
|
assertReceived();
|
|
const region = await h.control("world.read", {
|
|
world,
|
|
...view.viewBounds,
|
|
});
|
|
assert.equal(view.revision, region.revision);
|
|
assert.deepEqual(
|
|
sorted(view.blocks),
|
|
sorted(new Map(region.blocks.map((record) => [key(record.pos), record.block]))),
|
|
"Merged client view must exactly match the authoritative region",
|
|
);
|
|
}
|
|
|
|
async function edit(client, changes, operation) {
|
|
const result = await h.control("world.edit", {
|
|
world,
|
|
expected_revision: view.revision,
|
|
operation_id: operation,
|
|
changes,
|
|
});
|
|
await client.next("blocks", (message) => message.revision === result.revision);
|
|
await assertCurrentRegion();
|
|
}
|
|
|
|
async function walkTo(client, x, center, expectType = "chunks") {
|
|
const initial = (await client.state()).players.find(
|
|
(player) => player.id === client.id,
|
|
),
|
|
strafe = x > initial.position[0] ? 1 : -1,
|
|
seq = client.input({ strafe }),
|
|
pulse = setInterval(() => client.input({ strafe }), 100);
|
|
try {
|
|
await client.state(
|
|
(player, message) =>
|
|
message.ack >= seq &&
|
|
(strafe > 0 ? player.position[0] >= x : player.position[0] <= x),
|
|
10000,
|
|
);
|
|
} finally {
|
|
clearInterval(pulse);
|
|
const stopped = client.input();
|
|
await client.state((_player, message) => message.ack >= stopped);
|
|
}
|
|
const update = await client.next(
|
|
expectType,
|
|
(message) => message.view_center[0] === center,
|
|
);
|
|
assertReceived();
|
|
return update;
|
|
}
|
|
|
|
try {
|
|
const manifest = await h.get("/api/manifest");
|
|
async function material(state) {
|
|
const found = await h.control("catalog.search", { query: state, limit: 64 });
|
|
const item = found.items.find((item) => item.state === state);
|
|
assert.ok(item, `Catalog contains ${state}`);
|
|
return item.id;
|
|
}
|
|
const stone = await material("minecraft:stone"),
|
|
gold = await material("minecraft:gold_block"),
|
|
diamond = await material("minecraft:diamond_block"),
|
|
iron = await material("minecraft:iron_block");
|
|
await h.control("world.create", { world });
|
|
const plan = await h.control("build.plan", {
|
|
world,
|
|
expected_revision: 0,
|
|
operations: [
|
|
{ type: "box", min: [-64, 0, -32], max: [79, 0, 31], block: stone },
|
|
],
|
|
});
|
|
await h.control("build.commit", {
|
|
plan_id: plan.plan_id,
|
|
operation_id: "chunk-stream-floor",
|
|
});
|
|
await h.control("arena.configure", {
|
|
world,
|
|
mode: "creative",
|
|
spawn: [15, 2, 8],
|
|
});
|
|
const client = new Client(h.url);
|
|
clients.push(client);
|
|
client.ws.addEventListener("message", (event) => observe(JSON.parse(event.data)));
|
|
await client.open;
|
|
client.send({
|
|
type: "join",
|
|
protocol: 1,
|
|
manifest_hash: manifest.hash,
|
|
world,
|
|
name: "ChunkStreamProbe",
|
|
features: ["chunk_stream_v1"],
|
|
});
|
|
const welcome = await client.next("welcome");
|
|
assertReceived();
|
|
assert.deepEqual(view.viewCenter, [0, 0, 0]);
|
|
assert.deepEqual(view.viewBounds, { min: [-32, -16, -32], max: [31, 31, 31] });
|
|
await client.state((player) => player.position[1] < 1.15);
|
|
await assertCurrentRegion();
|
|
checks.push("Negotiated welcome supplies the complete 48-section view");
|
|
|
|
await edit(
|
|
client,
|
|
[
|
|
{ pos: [5, 1, 10], block: gold },
|
|
{ pos: [-24, 1, 10], block: diamond },
|
|
{ pos: [40, 1, 10], block: iron },
|
|
],
|
|
"chunk-stream-before-east",
|
|
);
|
|
assert.equal(view.blocks.has("40,1,10"), false);
|
|
await walkTo(client, 18, 16);
|
|
await assertCurrentRegion();
|
|
assert.equal(view.blocks.get("5,1,10"), gold);
|
|
assert.equal(view.blocks.has("-24,1,10"), false);
|
|
assert.equal(view.blocks.get("40,1,10"), iron);
|
|
checks.push("East crossing loads 12 sections, unloads 12, retains 36 without snapshot");
|
|
|
|
await edit(
|
|
client,
|
|
[
|
|
{ pos: [5, 1, 10], block: 0 },
|
|
{ pos: [-24, 1, 10], block: gold },
|
|
{ pos: [40, 1, 10], block: 0 },
|
|
],
|
|
"chunk-stream-while-away",
|
|
);
|
|
assert.equal(view.blocks.has("-24,1,10"), false);
|
|
await walkTo(client, 14, 0);
|
|
await assertCurrentRegion();
|
|
assert.equal(view.blocks.get("-24,1,10"), gold);
|
|
assert.equal(view.blocks.has("5,1,10"), false);
|
|
checks.push("Retained deletions persist and returning sections include edits made while unloaded");
|
|
|
|
await edit(
|
|
client,
|
|
[
|
|
{ pos: [24, 1, 10], block: gold },
|
|
{ pos: [-8, 1, 10], block: diamond },
|
|
{ pos: [-40, 1, 10], block: iron },
|
|
],
|
|
"chunk-stream-before-negative",
|
|
);
|
|
await walkTo(client, -2, -16);
|
|
await assertCurrentRegion();
|
|
assert.equal(view.blocks.has("24,1,10"), false);
|
|
assert.equal(view.blocks.get("-8,1,10"), diamond);
|
|
assert.equal(view.blocks.get("-40,1,10"), iron);
|
|
assert.equal(snapshots, 0);
|
|
assert.equal(transitions.length, 3);
|
|
assert.ok(transitions.every((transition) => transition.entering_blocks < welcome.blocks.length));
|
|
checks.push("Negative-coordinate crossing matches authoritative region and sends only entering blocks");
|
|
|
|
const beforeResync = sorted(view.blocks);
|
|
explicitResync = true;
|
|
client.send({ type: "resync" });
|
|
await client.next("snapshot");
|
|
assertReceived();
|
|
assert.equal(snapshots, 1);
|
|
assert.deepEqual(sorted(view.blocks), beforeResync);
|
|
await assertCurrentRegion();
|
|
checks.push("Explicit full resync remains available and preserves the current world contents");
|
|
|
|
const legacy = new Client(h.url);
|
|
clients.push(legacy);
|
|
const legacyWelcome = await legacy.join(manifest, world, "LegacyChunkProbe");
|
|
assert.ok(!legacyWelcome.features?.includes("chunk_stream_v1"));
|
|
await walkTo(legacy, 18, 16, "snapshot");
|
|
assert.equal(legacy.queue.some((message) => message.type === "chunks"), false);
|
|
checks.push("Clients without feature negotiation retain protocol-1 snapshot fallback");
|
|
|
|
await h.report({
|
|
passed: true,
|
|
checks,
|
|
transitions,
|
|
movement_snapshots: 0,
|
|
explicit_resync_snapshots: snapshots,
|
|
initial_blocks: welcome.blocks.length,
|
|
initial_snapshot_bytes: Buffer.byteLength(JSON.stringify(welcome)),
|
|
client_merge: "client/world-view.js",
|
|
binary: h.binary,
|
|
});
|
|
} catch (error) {
|
|
await h.report({ passed: false, checks, transitions, error: error.stack, logs: h.logs().slice(-8000) });
|
|
throw error;
|
|
} finally {
|
|
await Promise.all(clients.map((client) => client.close()));
|
|
await h.stop();
|
|
}
|