Improve netcode and simplify (#4)
All checks were successful
Build & deploy / Build & deploy website (push) Successful in 32s
Build & deploy / Build & publish server image (push) Successful in 1m2s

- Simplify the logic wherever possible to remove overengineering and simplify the netcode
- Actually improve the netcode
- The new lines are mainly the package locks and tests

Reviewed-on: https://home.schmelczer.dev/git/andras/decla-red/pulls/4
This commit is contained in:
Andras Schmelczer 2026-09-15 07:57:57 +01:00
commit 93a7fe01ab

View file

@ -1,11 +1,6 @@
// Determinism guard for the shared character simulation. The client predictor
// and the authoritative server run the EXACT same stepCharacterMovement; if it
// were non-deterministic (a stray Math.random / Date, or an order-dependent
// reduce) prediction would rubber-band and could never reconcile. This is also
// the regression net for the upcoming GC/scratch-pool perf rewrites: the pinned
// hash must not change unless physics behaviour is intentionally changed.
import { describe, it, expect } from 'vitest';
import { createRequire } from 'node:module';
import { execFileSync } from 'node:child_process';
const require = createRequire(import.meta.url);
const shared = require('../shared/lib/main.js');
@ -13,7 +8,10 @@ const { vec2 } = require('../shared/node_modules/gl-matrix');
const {
stepCharacterMovement,
tickPlanetDetachment,
resolveCircleMovement,
PlanetBase,
Random,
headRadius,
feetRadius,
headOffset,
@ -21,6 +19,8 @@ const {
rightFootOffset,
} = shared;
const stepSeconds = 1 / 200;
const makeBody = (center, radius) => ({
center: vec2.clone(center),
radius,
@ -29,55 +29,125 @@ const makeBody = (center, radius) => ({
restitution: 0,
});
// A free-space world (no planets): the body is driven purely by the movement
// input force, posture springs and momentum decay — enough to exercise the
// deterministic core without coupling the test to planet SDF geometry.
const emptyWorld = {
groundsNear: () => [],
const makeState = (start) => ({
head: makeBody(vec2.add(vec2.create(), start, headOffset), headRadius),
leftFoot: makeBody(vec2.add(vec2.create(), start, leftFootOffset), feetRadius),
rightFoot: makeBody(vec2.add(vec2.create(), start, rightFootOffset), feetRadius),
direction: 0,
currentPlanet: undefined,
secondsSinceOnSurface: 1,
bodyVelocity: vec2.create(),
});
const makeWorld = (grounds) => ({
groundsNear: () => grounds,
stepBody: (body, dt) => {
resolveCircleMovement(body, dt, []);
return undefined;
const hit = resolveCircleMovement(body, dt, grounds);
return hit && hit.isGround ? hit : undefined;
},
};
});
const stepSeconds = 1 / 200;
const round = (v) => Math.round(v * 1000) / 1000;
const roundVec = (v) => [round(v[0]), round(v[1])];
// Run a fixed, scripted input sequence through the shared sim and return a
// stable string snapshot of the final pose + carried momentum.
const runSimulation = () => {
const start = vec2.fromValues(100, 100);
const state = {
head: makeBody(vec2.add(vec2.create(), start, headOffset), headRadius),
leftFoot: makeBody(vec2.add(vec2.create(), start, leftFootOffset), feetRadius),
rightFoot: makeBody(vec2.add(vec2.create(), start, rightFootOffset), feetRadius),
direction: 0,
currentPlanet: undefined,
secondsSinceOnSurface: 1,
bodyVelocity: vec2.create(),
};
const state = makeState(vec2.fromValues(100, 100));
const world = makeWorld([]);
for (let i = 0; i < 300; i++) {
const angle = i * 0.1;
const input = vec2.fromValues(Math.cos(angle), Math.sin(angle));
stepCharacterMovement(state, emptyWorld, input, stepSeconds);
stepCharacterMovement(
state,
world,
vec2.fromValues(Math.cos(angle), Math.sin(angle)),
stepSeconds,
);
}
const round = (v) => Math.round(v * 1000) / 1000;
return JSON.stringify({
head: [round(state.head.center[0]), round(state.head.center[1])],
leftFoot: [round(state.leftFoot.center[0]), round(state.leftFoot.center[1])],
rightFoot: [round(state.rightFoot.center[0]), round(state.rightFoot.center[1])],
bodyVelocity: [round(state.bodyVelocity[0]), round(state.bodyVelocity[1])],
head: roundVec(state.head.center),
leftFoot: roundVec(state.leftFoot.center),
rightFoot: roundVec(state.rightFoot.center),
bodyVelocity: roundVec(state.bodyVelocity),
});
};
const runPlanetSimulation = () => {
Random.seed = 20;
const planet = new PlanetBase(
1,
PlanetBase.createPlanetVertices(vec2.fromValues(0, -900), 1700, 1900, 70),
);
planet.angularVelocity = 0.09;
const world = makeWorld([planet]);
const state = makeState(vec2.fromValues(0, 250));
for (let i = 0; i < 900; i++) {
const input = i < 200 ? vec2.create() : vec2.fromValues(i < 600 ? 1 : -1, 0);
tickPlanetDetachment(state, stepSeconds);
stepCharacterMovement(state, world, input, stepSeconds);
planet.advanceRotation(stepSeconds);
}
return JSON.stringify({
head: roundVec(state.head.center),
leftFoot: roundVec(state.leftFoot.center),
rightFoot: roundVec(state.rightFoot.center),
direction: round(state.direction),
onSurface: !!state.currentPlanet,
secondsSinceOnSurface: round(state.secondsSinceOnSurface),
});
};
describe('shared character simulation determinism', () => {
it('does not depend on configuring vector precision before imports', () => {
const run = (configureBeforeImport) =>
execFileSync(
process.execPath,
[
'-e',
`
const { glMatrix, vec2 } = require('./shared/node_modules/gl-matrix');
if (${configureBeforeImport}) glMatrix.setMatrixArrayType(Array);
const s = require('./shared/lib/main.js');
glMatrix.setMatrixArrayType(Array);
const body = (offset, radius) => ({
center: vec2.clone(offset), radius, velocity: vec2.create(),
lastNormal: [0, 1], restitution: 0,
});
const state = {
head: body(s.headOffset, s.headRadius),
leftFoot: body(s.leftFootOffset, s.feetRadius),
rightFoot: body(s.rightFootOffset, s.feetRadius),
direction: 0, currentPlanet: undefined,
secondsSinceOnSurface: 1, bodyVelocity: [0, 0],
};
const world = {
groundsNear: () => [],
stepBody: (body, dt) => s.resolveCircleMovement(body, dt, []),
};
for (let i = 0; i < 100; i++) s.stepCharacterMovement(state, world, [1, 0], 0.005);
process.stdout.write(JSON.stringify(state));
`,
],
{ encoding: 'utf8' },
);
expect(run(false)).toBe(run(true));
});
it('produces identical output across independent runs', () => {
expect(runSimulation()).toBe(runSimulation());
});
it('matches the pinned reference pose (changes only with intentional physics edits)', () => {
// Regression pin — update deliberately when physics behaviour changes.
expect(runSimulation()).toMatchSnapshot();
});
it('is deterministic on a spinning planet too', () => {
expect(runPlanetSimulation()).toBe(runPlanetSimulation());
});
it('matches the pinned reference pose on a spinning planet', () => {
expect(runPlanetSimulation()).toMatchSnapshot();
});
});