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,133 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user