rook/val Public Public: anyone can read this; nobody can change it.

forked from benjivers/val

workbenchthis machine

Record where a player is looking in every phase, alive or not

Ben: "the screen is still shaking on random rounds... when the round starts,
moving and standing still". Then the two facts that found it: the readout
said 30 Hz in, so the wire was fine, and pressing V stopped it dead.

That last one is the whole diagnosis. The two views draw with different
angles — `facing = mode === "fp" ? head.angle : eye.angle`. Top-down uses the
server's; first person uses the browser's. So a fight between those two is
invisible in one view and violent in the other.

And they were fighting. `player.angle = input.angle` lived inside
movePlayer, which:

  - the `end` and `over` phases return before ever reaching, and
  - a dead player never enters (it returns on `!player.alive`).

So between rounds, and for anyone lying dead, the server's copy of your angle
froze while your browser kept following your mouse. yaw.js takes the server's
angle only when it disagrees with what the client recently sent — and once
those sent angles aged out of SENT_MEMORY, the frozen one stopped looking
like an echo and started looking like a correction. The view snapped back to
where you were last recorded looking, over and over.

Measured, a client looking around for twenty seconds:

    alive, live phase                     0.1 snaps a second
    alive, END phase (round just decided) 0.8
    DEAD, waiting for the round           0.8
    alive, buy phase                      0.1

After: 0.1 everywhere, which is the baseline in a live round.

The fix is one rule, in one place, and it is the rule that was wrong rather
than a number that needed tuning: looking around is not moving. Movement is
what a phase and a death gate; where your head points is recorded every tick
regardless. The assignment moves out of movePlayer into tick, for every
player, in every phase.

Why the buy phase was fine and the end phase was not — movePlayer runs in
buy — is also why this only ever bit at a round turning over, which is
exactly where it was reported.

305 tests. The new one walks a seat through live, dead, end, over and buy and
asserts the angle follows; putting the assignment back inside movePlayer
fails it and nothing else.
Benjivers-bit committed d721124 Browse files
2 files changed +62 −1
src/match.test.ts +43
@@ -414,3 +414,46 @@ test("the deal is the seeded shuffle, so the same match replays the same", () =>
414414 expect(newMatch(77).players.map((p) => p.name)).toEqual(newMatch(77).players.map((p) => p.name));
415415 expect(newMatch(77).players.map((p) => p.name)).not.toEqual(newMatch(78).players.map((p) => p.name));
416416 });
417
418 const round1 = (n: number) => Math.round(n * 1000) / 1000;
419
420 test("where you are looking is recorded in every phase, alive or not", () => {
421 // The bug this pins: `player.angle = input.angle` lived inside movePlayer,
422 // which the end and over phases return before reaching and which a dead
423 // player never enters. The server's copy of your angle froze, the browser
424 // kept its own, and once the angles the client had recently sent aged out
425 // of its memory the frozen one read as a correction — so the view snapped
426 // back, about 0.8 times a second, in first person only. Looking around is
427 // not moving, and no phase and no death stops you doing it.
428 const state = newMatch(5);
429 const me = state.players[0]!;
430 me.bot = false;
431
432 const looksFollow = (label: string) => {
433 for (const want of [0.4, -1.2, 2.9]) {
434 setInput(me, { angle: want });
435 tick(state, 1 / 30);
436 expect(`${label}: ${round1(me.angle)}`).toBe(`${label}: ${round1(want)}`);
437 }
438 };
439
440 looksFollow("live");
441
442 me.alive = false;
443 me.hp = 0;
444 looksFollow("dead");
445 me.alive = true;
446 me.hp = 100;
447
448 state.phase = "end";
449 state.clock = 99;
450 looksFollow("between rounds");
451
452 state.phase = "over";
453 state.clock = 99;
454 looksFollow("match over");
455
456 state.phase = "buy";
457 state.clock = 99;
458 looksFollow("buy");
459 });
src/match.ts +19 −1
@@ -314,6 +314,24 @@ export function tick(state: MatchState, dt: number): void {
314314 state.shots = state.shots.filter((s) => s.age < 0.12);
315315 for (const event of state.events) event.age += dt;
316316
317 // Where you are LOOKING is recorded every tick, in every phase, alive or
318 // dead. Moving is what the phases and dying gate; turning your head is not.
319 //
320 // This used to live inside movePlayer, which the round-end and match-over
321 // phases return before reaching and which a dead player never enters. The
322 // server's copy of your angle therefore froze between rounds and while you
323 // were down — and the browser, which owns your view and only takes the
324 // server's angle when the server disagrees, kept looking where the mouse
325 // said. Once the angles a client had recently sent aged out of its memory
326 // (yaw.js SENT_MEMORY), the frozen one read as a genuine correction and the
327 // view snapped back to it. Measured: about 0.8 snaps a second while dead or
328 // between rounds, against 0.0 in a live round. It was invisible in the
329 // top-down view, which draws the server's angle, and violent in first
330 // person, which draws the browser's — and that asymmetry is what made it
331 // look like the renderer rather than the rules.
332 for (const player of state.players)
333 if (Number.isFinite(player.input.angle)) player.angle = player.input.angle;
334
317335 if (state.phase === "over") {
318336 state.clock -= dt;
319337 if (state.clock <= 0) resetMatch(state);
@@ -385,7 +403,7 @@ export function speedOf(state: MatchState, player: Player): number {
385403 function movePlayer(state: MatchState, player: Player, dt: number, penned: boolean): void {
386404 if (!player.alive) return;
387405 const input = player.input;
388 if (Number.isFinite(input.angle)) player.angle = input.angle;
406 // The angle is applied for every player in tick(), whatever the phase.
389407 let mx = input.mx;
390408 let my = input.my;
391409 const len = Math.hypot(mx, my);
↑↓ navigate ↵ open esc close

Keyboard shortcuts