Files
shacraft-core/scripts/check_chunk_streaming.mjs
Emil c7e86663d8
MVP checks / mvp (push) Waiting to run
Expand voxel gameplay, lighting, full-height streaming and world imports
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.
2026-09-17 02:10:53 +03:00

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();
}