The gap Stage 1 left open: snapshots were encoded once and broadcast to every peer in an instance, so clients were handed enemies the fog then hid. Fog is a rendering rule -- a modified client draws whatever it holds -- so that was no defence at all. Snapshots are now encoded per peer, and actors beyond ACTOR_INTEREST_RADIUS (800u) are never sent. Measured cost of four filtered encodes against one shared: 95.6us vs 24.9us, or 0.032 ms/tick amortised over the snapshot interval. Two asymmetries worth keeping: - The observer's own player record is never filtered, however far out the arithmetic puts it. The client reconciles its prediction against that record, so dropping it breaks the player's own movement rather than hiding someone. - Only bullet SPAWNS are filtered, never despawns. A client told about a bullet must always be told it died, or it keeps a phantom. Bullet spawns use a much wider radius (2200u) than actors, deliberately. An enemy appearing at the edge of sight is cosmetic; a bullet withheld at spawn that later flies into view is invisible damage. The floor is longest bullet travel + fog radius -- 1500 for the Warden's Collapse snipe -- and test_interest.gd recomputes that from live content, so adding a faster or longer-lived bullet fails a test instead of producing bullets that wink into existence. Also clamped bosses to their room. A no-op today since every boss is stationary, which is exactly when the invariant is cheap to establish: boss rooms have no door that locks, so the only thing keeping a fight in the boss room is the boss, and Stage 5's movement work would otherwise break it quietly. 146 tests (was 137). check.sh, test.sh and smoke.sh pass. Stage 1 has no partials left; docs/ROADMAP.md now records the three interest radii and the floor each must respect.
This commit is contained in:
@@ -158,6 +158,35 @@ Hard fog is therefore a *rendering* rule, not a secrecy mechanism. The secrecy
|
||||
lives in what the server declines to send: terrain outside the stream radius,
|
||||
and (next stage) actors outside it.
|
||||
|
||||
## Actors are scoped per peer
|
||||
|
||||
Snapshots are encoded **once per peer**, not once and broadcast: an actor
|
||||
further than `ACTOR_INTEREST_RADIUS` (800u) from a player is never sent to that
|
||||
player. Before this, fog was hiding enemies the client had already been handed,
|
||||
which is no defence at all against a modified client.
|
||||
|
||||
Measured cost of encoding four filtered snapshots instead of one shared:
|
||||
95.6µs against 24.9µs, which amortised over the 3-tick snapshot interval is
|
||||
0.032 ms/tick — 0.2% of the frame budget.
|
||||
|
||||
Two rules fall out of it:
|
||||
|
||||
- **The observer's own player record is never filtered**, however far outside
|
||||
the radius the arithmetic puts it. The client reconciles its prediction
|
||||
against that record; dropping it would break the player's own movement rather
|
||||
than merely hide someone.
|
||||
- **Bullet despawns are not filtered, only spawns.** A client that was told
|
||||
about a bullet must always be told it died, or it keeps a phantom until the
|
||||
lifetime expires.
|
||||
|
||||
Bullet spawns use a much wider radius (`BULLET_INTEREST_RADIUS`, 2200u) than
|
||||
actors, because the failure modes are not symmetric. An enemy appearing at the
|
||||
edge of sight is cosmetic; a bullet withheld at spawn that later flies into view
|
||||
is invisible damage. The floor is *longest bullet travel + fog radius* — 1500 +
|
||||
460 for the Warden's "Collapse" snipe — and `test_interest.gd` recomputes that
|
||||
from the live content, so adding a faster or longer-lived bullet fails a test
|
||||
rather than producing bullets that wink into existence.
|
||||
|
||||
## No contact damage
|
||||
|
||||
Nothing hurts you by touching it. Every threat is a bullet you can see and
|
||||
|
||||
Reference in New Issue
Block a user