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.
134 lines
4.1 KiB
JavaScript
134 lines
4.1 KiB
JavaScript
import test from "node:test";
|
||
import assert from "node:assert/strict";
|
||
import { readFile } from "node:fs/promises";
|
||
import {
|
||
loadPhysics,
|
||
LocalPrediction,
|
||
samplePhysicsWorld,
|
||
} from "../player-physics.js";
|
||
|
||
const binary = await readFile(new URL("../physics.wasm", import.meta.url));
|
||
const fixture = JSON.parse(
|
||
await readFile(
|
||
new URL(
|
||
"../../crates/shacraft-physics/tests/fixtures/java26.2.json",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
),
|
||
);
|
||
const originalFetch = globalThis.fetch;
|
||
let step;
|
||
try {
|
||
// Exercise the actual browser ABI and its non-streaming MIME fallback.
|
||
globalThis.fetch = async () =>
|
||
new Response(binary, {
|
||
headers: { "Content-Type": "application/octet-stream" },
|
||
});
|
||
step = await loadPhysics("http://local-test/physics.wasm");
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
|
||
const bounds = { min: [-64, -32, -64], max: [63, 63, 63] };
|
||
function floor(surface = "stone") {
|
||
const blocks = new Map();
|
||
for (let x = -4; x <= 4; x++)
|
||
for (let z = -55; z <= 5; z++) blocks.set(`${x},-1,${z}`, 1);
|
||
const materials = new Map([
|
||
[
|
||
1,
|
||
{
|
||
state: `minecraft:${surface}`,
|
||
collision: [{ min: [0, 0, 0], max: [1, 1, 1] }],
|
||
},
|
||
],
|
||
]);
|
||
return (body) => samplePhysicsWorld(body, blocks, materials, bounds);
|
||
}
|
||
const settings = { allow_flight: true };
|
||
|
||
test("the shipped browser WebAssembly follows independently measured Java 26.2 trajectories", () => {
|
||
let checked = 0;
|
||
for (const [name, trajectory] of Object.entries(
|
||
fixture.travel_kernel_trajectories,
|
||
)) {
|
||
if (trajectory.medium !== "air") continue;
|
||
const getWorld = floor(trajectory.surface);
|
||
let body = {
|
||
position: trajectory.initial_position,
|
||
velocity: trajectory.initial_velocity,
|
||
flying: trajectory.flying,
|
||
on_ground: !trajectory.flying,
|
||
};
|
||
for (const sample of trajectory.samples) {
|
||
const input = {
|
||
forward: sample.tick <= trajectory.input_ticks ? 1 : 0,
|
||
sprint: trajectory.sprint,
|
||
jump: sample.tick === 1 && trajectory.jump_first_tick,
|
||
};
|
||
body = step({ body, input, world: getWorld(body), settings });
|
||
for (const field of ["position", "velocity"])
|
||
for (let axis = 0; axis < 3; axis++) {
|
||
const expected = sample[field][axis] * (axis === 2 ? -1 : 1);
|
||
assert.ok(
|
||
Math.abs(body[field][axis] - expected) < 2e-6,
|
||
`${name} tick ${sample.tick} ${field}[${axis}]: ${body[field][axis]} != ${expected}`,
|
||
);
|
||
}
|
||
assert.equal(
|
||
body.on_ground,
|
||
sample.on_ground,
|
||
`${name} tick ${sample.tick} grounded`,
|
||
);
|
||
checked++;
|
||
}
|
||
}
|
||
assert.ok(checked >= 200);
|
||
});
|
||
|
||
test("real WebAssembly prediction replays delayed authority without introducing trajectory drift", () => {
|
||
const getWorld = floor();
|
||
const prediction = new LocalPrediction(step, getWorld);
|
||
let authority = {
|
||
position: [0, 0, 0],
|
||
velocity: [0, -0.0784000015258789, 0],
|
||
on_ground: true,
|
||
};
|
||
prediction.receive({ body: authority, ack: 0, tick: 0, epoch: 1, settings });
|
||
const packets = [];
|
||
for (let tick = 1; tick <= 80; tick++) {
|
||
const input = {
|
||
forward: tick < 55 ? 1 : 0,
|
||
sprint: tick > 20 && tick < 50,
|
||
jump: tick === 25,
|
||
sneak: tick >= 55,
|
||
};
|
||
prediction.push(tick, input);
|
||
authority = step({
|
||
body: authority,
|
||
input,
|
||
world: getWorld(authority),
|
||
settings,
|
||
});
|
||
packets.push({ body: authority, ack: tick, tick, epoch: 1, settings });
|
||
// Variable 150–250 ms inbound latency; authority still acknowledges order.
|
||
if (packets.length > (tick % 3) + 3) prediction.receive(packets.shift());
|
||
for (let axis = 0; axis < 3; axis++) {
|
||
assert.ok(
|
||
Math.abs(prediction.body.position[axis] - authority.position[axis]) <
|
||
1e-11,
|
||
`position drift at tick ${tick}`,
|
||
);
|
||
assert.ok(
|
||
Math.abs(prediction.body.velocity[axis] - authority.velocity[axis]) <
|
||
1e-11,
|
||
`velocity drift at tick ${tick}`,
|
||
);
|
||
}
|
||
}
|
||
for (const packet of packets) prediction.receive(packet);
|
||
assert.equal(prediction.pending.length, 0);
|
||
assert.deepEqual(prediction.body, authority);
|
||
});
|