fix: rate-limit ghost loco rebuilds against flag-flapping peers
Browse files- 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;
|