tfrere HF Staff Cursor commited on
Commit
a7feb6c
·
1 Parent(s): 4dcadb5

fix: rate-limit ghost loco rebuilds against flag-flapping peers

Browse files
Files changed (1) hide show
  1. app/src/game/ghosts.js +10 -2
app/src/game/ghosts.js CHANGED
@@ -53,6 +53,9 @@ const RESPACE_MIN_MS = 1000 / SEND_HZ / 2; // de-jitter floor between stamps
53
  const GAP_RELEASE = 0.15; // per-packet EWMA rate when arrivals speed up
54
  const DELAY_SLEW = 0.5; // max delay change per ms of real time
55
  const BUF_MAX = 40; // snapshots kept per ghost (~2.5 s at 15 Hz)
 
 
 
56
  // Idle visitors stay invisible: a ghost is only shown once its peer has
57
  // strayed from the pose of its first state message (everyone spawns at
58
  // the arena center). Once revealed it stays visible for the whole peer
@@ -294,14 +297,19 @@ export async function initGhosts(env) {
294
  // cloneRig shares geometry). The snapshot buffer carries over so the
295
  // interpolated motion stays continuous across the swap. While the
296
  // wanted rig is still loading, keep warming it and stay on legs.
 
 
 
 
297
  if (state.l !== g.loco) {
 
298
  if (servedLoco(state.l) !== state.l) {
299
  env.prepareRigFor?.(state.l);
300
- } else {
301
  const { lastSeen, spawn, revealed, buf, gapAvg, delay } = g;
302
  removeGhost(peerId);
303
  g = makeGhost(state);
304
- Object.assign(g, { lastSeen, spawn, revealed, buf, gapAvg, delay });
305
  g.rig.placer.visible = revealed;
306
  ghosts.set(peerId, g);
307
  return;
 
53
  const GAP_RELEASE = 0.15; // per-packet EWMA rate when arrivals speed up
54
  const DELAY_SLEW = 0.5; // max delay change per ms of real time
55
  const BUF_MAX = 40; // snapshots kept per ghost (~2.5 s at 15 Hz)
56
+ // Floor between legs<->rollers ghost rebuilds for one peer: a rebuild
57
+ // clones a full rig, so the rate is capped against flag-flapping peers.
58
+ const LOCO_SWAP_MIN_MS = 500;
59
  // Idle visitors stay invisible: a ghost is only shown once its peer has
60
  // strayed from the pose of its first state message (everyone spawns at
61
  // the arena center). Once revealed it stays visible for the whole peer
 
297
  // cloneRig shares geometry). The snapshot buffer carries over so the
298
  // interpolated motion stays continuous across the swap. While the
299
  // wanted rig is still loading, keep warming it and stay on legs.
300
+ // Rate-limited per peer: the lobby is public, so a hostile peer
301
+ // flapping the flag at packet rate must not turn into a 15 Hz
302
+ // teardown/rebuild loop on every viewer's tab (a real switch takes a
303
+ // ~1 s button hold, so legit swaps are never this close together).
304
  if (state.l !== g.loco) {
305
+ const now = performance.now();
306
  if (servedLoco(state.l) !== state.l) {
307
  env.prepareRigFor?.(state.l);
308
+ } else if (now - (g.locoSwapAt ?? 0) >= LOCO_SWAP_MIN_MS) {
309
  const { lastSeen, spawn, revealed, buf, gapAvg, delay } = g;
310
  removeGhost(peerId);
311
  g = makeGhost(state);
312
+ Object.assign(g, { lastSeen, spawn, revealed, buf, gapAvg, delay, locoSwapAt: now });
313
  g.rig.placer.visible = revealed;
314
  ghosts.set(peerId, g);
315
  return;