import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { harness, Client, options } from "./server-harness.mjs"; import { loadPhysics, samplePhysicsWorld } from "../client/player-physics.js"; import { getViewBounds, mergeChunkUpdate, mergeSnapshot } from "../client/world-view.js"; // This check starts a separate server and temporary database. It never connects // to the user's running game or modifies their worlds. const opts = { port: "4013", binary: "target/release/shacraft-server", output: "artifacts/physics/network-report.json", ...options(), }; const serverHash = createHash("sha256").update(await readFile(opts.binary)).digest("hex"); const h = await harness(opts), clients = [], checks = [], samples = []; const world = "test_player_physics", groundedWorld = "test_player_grounded"; const inputs = new Map([[0, {}]]); const view = { world, revision: 0, viewCenter: null, viewBounds: null, blocks: new Map() }; const materials = new Map(); let client, step, previous, heldInput = {}, receiveError, permitSnapshot = false; let snapshots = 0, chunks = 0, comparedTicks = 0, maximumError = 0, resetCount = 0; const zero = { yaw: 0, pitch: 0, forward: 0, strafe: 0, jump: false, sprint: false, sneak: false, fly_toggle: false }; const controls = (input) => ({ ...zero, ...input, // The wire protocol stores these fields as f32 before passing them to Rust. ...Object.fromEntries(["yaw", "pitch", "forward", "strafe"].map((key) => [key, Math.fround(input[key] || 0)])), }); function received() { if (receiveError) throw receiveError; } function observe(message) { if (receiveError) return; try { if (message.type === "error") throw Error(message.message); for (const material of message.materials || []) materials.set(material.id, material); if (message.type === "welcome" || message.type === "snapshot") { if (message.type === "snapshot") { assert.ok(permitSnapshot, "Movement must not send a full snapshot"); snapshots++; } assert.ok(message.features.includes("movement_prediction_v1")); assert.ok(message.features.includes("chunk_stream_v1")); view.world = message.world; view.revision = message.revision; view.viewCenter = message.view_center; view.viewBounds = getViewBounds(message); mergeSnapshot(view.blocks, message.blocks); previous = message.motion; heldInput = { ...zero }; } else if (message.type === "chunks") { assert.ok(!("spawn" in message) && !("blocks" in message)); assert.equal(mergeChunkUpdate(view, message).status, "applied"); chunks++; } else if (message.type === "state") { const motion = message.motion; assert.ok(motion, "Negotiated state carries authoritative motion"); assert.equal(message.ack, motion.ack); assert.equal(message.tick, motion.tick); if (previous && motion.epoch !== previous.epoch) { resetCount++; heldInput = { ...zero }; } else if (previous && motion.tick === previous.tick + 1) { assert.ok(motion.ack >= previous.ack, "Acknowledgements cannot regress"); assert.ok(motion.ack - previous.ack <= 1, "At most one queued command is processed per tick"); if (motion.ack !== previous.ack) { assert.ok(inputs.has(motion.ack), `Known consumed command ${motion.ack}`); heldInput = controls(inputs.get(motion.ack)); } else heldInput = { ...heldInput, fly_toggle: false }; const neighborhood = samplePhysicsWorld(previous.body, view.blocks, materials, view.viewBounds); assert.ok(neighborhood, "Collision neighborhood is loaded during the probe"); const predicted = step({ body: previous.body, input: heldInput, world: neighborhood, settings: motion.settings }); let error = 0; for (const field of ["position", "velocity"]) for (let axis = 0; axis < 3; axis++) error = Math.max(error, Math.abs(predicted[field][axis] - motion.body[field][axis])); maximumError = Math.max(maximumError, error); assert.ok(error < 1e-10, `Native/WASM drift at tick ${motion.tick}, ack ${motion.ack}: ${error}`); for (const field of ["pose", "on_ground", "sprinting", "flying", "jump_cooldown", "in_water", "in_lava"]) assert.equal(predicted[field], motion.body[field], `${field} matches at tick ${motion.tick}`); comparedTicks++; } samples.push({ tick: motion.tick, ack: motion.ack, epoch: motion.epoch, position: motion.body.position, pose: motion.body.pose, flying: motion.body.flying }); previous = motion; } } catch (error) { receiveError = error; } } function input(fields = {}) { const message = { type: "input", seq: ++client.seq, ...zero, ...fields }; inputs.set(message.seq, message); client.send(message); return message.seq; } async function state(predicate = () => true, timeout = 10000) { const message = await client.next("state", (m) => predicate(m.motion, m), timeout); received(); return message.motion; } async function acknowledged(fields) { const seq = input(fields); return state((motion) => motion.ack >= seq); } async function ticks(count) { const start = previous.tick; return state((motion) => motion.tick >= start + count); } try { const manifest = await h.get("/api/manifest"); step = await loadPhysics(`${h.url}/physics.wasm`); const binary = Buffer.from(await (await fetch(`${h.url}/physics.wasm`)).arrayBuffer()); const wasmHash = createHash("sha256").update(binary).digest("hex"); const catalog = await h.control("catalog.search", { query: "minecraft:stone", limit: 64 }); const stone = catalog.items.find((item) => item.state === "minecraft:stone"); assert.ok(stone); await h.control("world.create", { world }); const plan = await h.control("build.plan", { world, expected_revision: 0, operations: [{ type: "box", min: [-96, 0, -16], max: [160, 0, 16], block: stone.id }] }); await h.control("build.commit", { plan_id: plan.plan_id, operation_id: "physics-floor" }); await h.control("arena.configure", { world, mode: "creative", spawn: [15, 1, 8] }); await h.control("world.create", { world: groundedWorld, template: world }); await h.control("arena.configure", { world: groundedWorld, mode: "spleef", spawn: [0, 1, 8] }); 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: "PhysicsProbe", features: ["chunk_stream_v1", "movement_prediction_v1"] }); const welcome = await client.next("welcome"); received(); assert.ok(welcome.motion.settings.allow_flight); await state((motion) => motion.body.on_ground); checks.push("HTTP-served WebAssembly loads and both movement/chunk features negotiate"); const beforeBurst = previous; const burstStart = samples.length; for (let index = 0; index < 12; index++) input({ forward: 1, strafe: .25, yaw: .3, sprint: true, jump: index === 2 }); const burstEnd = client.seq; const burstMotion = await state((motion) => motion.ack === burstEnd); const acks = samples.slice(burstStart).filter((m) => m.ack > beforeBurst.ack).map((m) => m.ack); assert.deepEqual([...new Set(acks)], Array.from({ length: 12 }, (_, i) => beforeBurst.ack + i + 1)); assert.ok(burstMotion.tick - beforeBurst.tick >= 12); client.send({ type: "input", seq: burstEnd, ...zero, forward: -1 }); client.send({ type: "input", seq: burstEnd - 1, ...zero, forward: -1 }); const duplicate = await ticks(2); assert.equal(duplicate.ack, burstEnd); checks.push("A 12-command burst advances at most one acknowledgement per tick; duplicate/stale inputs are ignored"); await acknowledged({}); await ticks(14); const crouch = await acknowledged({ sneak: true }); assert.equal(crouch.body.pose, "crouching"); const jump = await acknowledged({ jump: true }); assert.equal(jump.body.pose, "standing"); assert.ok(jump.body.velocity[1] > 0 && !jump.body.on_ground); const flying = await acknowledged({ fly_toggle: true, jump: true }); assert.equal(flying.body.flying, true); const ascended = await ticks(4); assert.equal(ascended.body.flying, true, "Held input cannot re-toggle flight"); assert.ok(ascended.body.position[1] > flying.body.position[1]); const stoppedFlying = await acknowledged({ fly_toggle: true }); assert.equal(stoppedFlying.body.flying, false); await state((motion) => motion.body.on_ground, 10000); checks.push("Crouch dimensions, normal jump, authorized flight ascent and one-shot flight toggles are authoritative"); const epochBeforeReset = previous.epoch; for (let i = 0; i < 12; i++) input({ forward: 1 }); client.send({ type: "input_reset" }); const reset = await state((motion) => motion.epoch > epochBeforeReset); assert.equal(reset.ack, client.seq); await acknowledged({}); await ticks(3); const epochBeforeRespawn = previous.epoch; client.send({ type: "respawn" }); const respawn = await state((motion) => motion.epoch > epochBeforeRespawn); assert.ok(Math.abs(respawn.body.position[0] - 15) < 1e-9); assert.ok(Math.abs(respawn.body.position[2] - 8) < 1e-9); checks.push("Input reset discards queued commands with a new epoch; respawn clears motion and resets position"); const chunksBeforeWalk = chunks, snapshotsBeforeWalk = snapshots; let walk = await acknowledged({ forward: 1, sprint: true, yaw: Math.PI / 2 }); const startX = walk.body.position[0]; for (let i = 0; i < 145; i++) walk = await acknowledged({ forward: 1, sprint: true, yaw: Math.PI / 2 }); await acknowledged({}); await ticks(8); assert.ok(walk.body.position[0] - startX > 35); assert.ok(chunks - chunksBeforeWalk >= 2); assert.equal(snapshots, snapshotsBeforeWalk); checks.push("Sustained predicted sprint crosses multiple chunk boundaries using deltas without full snapshots"); const oldEpoch = previous.epoch; permitSnapshot = true; client.send({ type: "switch_world", world: groundedWorld }); const switched = await client.next("snapshot", (m) => m.world === groundedWorld); received(); permitSnapshot = false; assert.ok(switched.motion.epoch > oldEpoch); assert.equal(switched.motion.settings.allow_flight, false); const deniedFlight = await acknowledged({ fly_toggle: true, jump: true }); assert.equal(deniedFlight.body.flying, false); await acknowledged({}); checks.push("World switch resets the epoch and denies flight outside creative mode"); const legacy = new Client(h.url); clients.push(legacy); const legacyWelcome = await legacy.join(manifest, groundedWorld, "LegacyPhysicsProbe"); assert.equal(legacyWelcome.motion, undefined); const legacyStart = (await legacy.state()).players.find((p) => p.id === legacy.id).position; const legacySeq = legacy.input({ strafe: 1 }); const legacyMoved = await legacy.state((player, message) => message.ack >= legacySeq && player.position[0] > legacyStart[0] + .5); assert.equal(legacyMoved.motion, undefined); legacy.input(); checks.push("A client without prediction support still receives movement and acknowledgements without motion payloads"); received(); assert.ok(comparedTicks > 170); await h.report({ ok: true, checks, server: h.url, server_sha256: serverHash, wasm_sha256: wasmHash, native_wasm_tolerance: 1e-10, native_wasm_max_error: maximumError, native_wasm_compared_ticks: comparedTicks, chunk_updates: chunks, expected_full_snapshots: snapshots, observed_epoch_resets: resetCount, samples }); } catch (error) { await h.report({ ok: false, checks, error: String(error.stack || error), receive_error: receiveError?.stack, native_wasm_compared_ticks: comparedTicks, native_wasm_max_error: maximumError, samples, server_log: h.logs() }); throw error; } finally { await Promise.all(clients.map((client) => client.close())); await h.stop(); }