current state
This commit is contained in:
+17
-3
@@ -58,7 +58,7 @@ Currently carries the chunk manifest for the 3×3 neighbourhood around the playe
|
||||
|
||||
The `entity_checksum` lets the client detect a lost `EntityPacket` without a dedicated
|
||||
ACK: if the checksum differs from the one computed over the last received entity update,
|
||||
the client knows to retransmit an `ActionPacket` (sequence preserved, no-op action) to
|
||||
the client knows to retransmit an `ActionPacket` (`target_tick = 0`, no-op action) to
|
||||
prompt the server to re-send the current entity state.
|
||||
|
||||
---
|
||||
@@ -71,13 +71,27 @@ Sent by the client on player action or on a chunk cache miss.
|
||||
|--------|------|--------------|-----------------|--------------------------------|
|
||||
| 0 | 6 | Header | `header` | packet_type = 1 |
|
||||
| 6 | 8 | u64 | `auth_token` | Token of the current session |
|
||||
| 14 | 4 | u32 | `sequence` | Monotonically increasing |
|
||||
| 14 | 4 | u32 | `target_tick` | Tick the action is scheduled for (see below); 0 = keep-alive/ack only |
|
||||
| 18 | 54 | ChunkEntry | `cache[9]` | Versions client currently holds |
|
||||
| 72 | 2 | PlayerAction | `player_action` | Derived from user input |
|
||||
| 74 | ? | ActionData | `action_data` | Dependent on PlayerAction |
|
||||
|
||||
**Minimum: 74 bytes** (no ActionData)
|
||||
|
||||
**Tick-addressed scheduling.** Actions are scheduled onto the server's tick timeline
|
||||
instead of being consumed in arrival order. `target_tick` selects the movement window
|
||||
(`target_tick / TICKS_PER_MOVE`, rounded up) the action executes in:
|
||||
|
||||
- A second action addressed to the same window **replaces** the first — this is how the
|
||||
client retracts (NOOP) or changes a scheduled step until its window executes, and how
|
||||
retransmits dedupe for free.
|
||||
- A **late** action (window already passed on arrival) moves to the next window, but
|
||||
only if that slot is empty: late actions fill gaps, they never override newer intent.
|
||||
- Only the next `ACTION_WINDOW_HORIZON` windows are addressable; anything beyond is
|
||||
dropped. Combined with one-action-per-window execution this bounds server memory and
|
||||
movement speed regardless of client behavior.
|
||||
- `target_tick = 0` carries no scheduling intent (keep-alive / cache-ack packets).
|
||||
|
||||
**ChunkEntry (6 bytes)**
|
||||
|
||||
| Offset | Size | Type | Field |
|
||||
@@ -302,7 +316,7 @@ whatever is still missing. No deduplication logic is required.
|
||||
the client simply waits one tick (~100 ms).
|
||||
|
||||
**Lost EntityPacket** — detected via the `entity_checksum` in the next `StatePacket`.
|
||||
The client retransmits a no-op `ActionPacket` (same sequence number, no-op action,
|
||||
The client retransmits a no-op `ActionPacket` (`target_tick = 0`, no-op action,
|
||||
current cache state); the server treats this as a normal diff request and re-sends the
|
||||
full entity update for the tick.
|
||||
|
||||
|
||||
+92
-9
@@ -37,10 +37,93 @@ terminal). The platform layer — window, GPU, input, RGB332 palette — lives i
|
||||
(currently empty — every tile walkable) keeps client prediction and server sim in lockstep.
|
||||
- [x] Click-to-move: framebuffer click → world tile, A* over the chunk cache
|
||||
(`client/src/game/pathfind.rs`, unknown chunks count as blocked), route translated into
|
||||
cardinal actions one step per movement interval — the server only ever sees N/E/S/W and
|
||||
stays authoritative. Keyboard input cancels the route; each step is re-validated at send
|
||||
time and a blocked step voids the route. In-flight steps render bright blue, planned
|
||||
route dim blue.
|
||||
cardinal actions one step per movement interval — the server only ever sees movement
|
||||
actions and stays authoritative. Keyboard input cancels the route; each step is
|
||||
re-validated at send time and a blocked step voids the route. In-flight steps render
|
||||
bright blue, planned route dim blue. Click-and-hold steers continuously: while the
|
||||
button is held the route keeps replanning toward the tile under the cursor (only
|
||||
when that tile changes — cursor or camera movement), sweeping across blocked tiles
|
||||
keeps the current route, and a voided route replans automatically while held.
|
||||
- [x] 8-directional movement in chessboard geometry: world physics use the Chebyshev
|
||||
metric — diagonal and cardinal steps are the same distance, a "circle" is a square
|
||||
of tiles, matching the square viewport. Four diagonal actions in
|
||||
`shared::player_action`; the single-step rule lives in `shared::step_allowed`
|
||||
(king move onto a free tile, diagonals additionally need both orthogonal neighbors
|
||||
free — no corner cutting) and is the one function used by the server sim, client
|
||||
send-time validation and the client A* (8-connected, Chebyshev heuristic). Two held
|
||||
keys walk diagonally.
|
||||
- [x] Collision vocabulary seeded with test values (`shared::tile_collidable`): id 146
|
||||
(trees/rocks) and id 0 — id 0 doubles as the server's invisible world border, which
|
||||
the client previously mispredicted as walkable. A proper tile-data file format
|
||||
replaces this table later.
|
||||
- [x] Tick-addressed action scheduling (supersedes two interim designs — a sequence-
|
||||
deduped FIFO queue and its flow control — that fixed a periodic walking hitch and
|
||||
a path/route deadlock but kept two free-running clocks racing each other). Actions
|
||||
are now scheduled onto the server's tick timeline: `ActionPacket.target_tick`
|
||||
(formerly `sequence`) selects the movement window, a second action to the same
|
||||
window *replaces* the first (retraction via NOOP, rescheduling, retransmit dedup),
|
||||
late actions fill only an *empty* next window (gap-filling without overriding
|
||||
newer intent — needed over real internet links so actions don't die pointlessly),
|
||||
and only `ACTION_WINDOW_HORIZON = 3` future windows are addressable. The sim keeps
|
||||
per-entity window slot maps and executes at most one action per window, so floods
|
||||
can neither grow memory nor speed anyone up. The client estimates the server tick
|
||||
from `StatePacket.tick` plus elapsed time and schedules each step into the next
|
||||
window — one send per window by construction, no local send timer, no clock-rate
|
||||
race. `target_tick = 0` marks keep-alive/cache-ack packets with no scheduling
|
||||
intent. See `notes/protocol.md` (ActionPacket).
|
||||
- [x] Client-side unexpected-state handling: the movement goal is persistent (outlives
|
||||
the planned route) and every surprise reroutes toward it — a blocked route step
|
||||
replans instead of voiding the plan, and a confirmed position off the predicted
|
||||
path (lost/rejected/overridden step) retracts all still-scheduled steps (NOOP to
|
||||
their windows), drops the stale prediction and replans from the confirmed tile.
|
||||
Changing plans mid-run (new click/steer target) likewise retracts scheduled-but-
|
||||
unexecuted steps, so old intent stops playing out on the server within a window.
|
||||
The goal is released on arrival, unreachability, keyboard override, or a discrete
|
||||
click on an unreachable tile. This is the "reactive replanning" item formerly
|
||||
parked under Later — Robustness.
|
||||
- [x] `netsim` — bad-internet simulator (workspace member, dev tool): a UDP proxy adding
|
||||
delay, jitter and loss per direction (`--delay/--jitter/--loss`, `--up-*`/`--down-*`
|
||||
overrides; reordering emerges from jitter). No root, game-traffic only, zero deps.
|
||||
The client takes an optional server address argument to point at it:
|
||||
`cargo run -p netsim -- --delay 80 --jitter 30 --loss 5` + `client 127.0.0.1:7778`.
|
||||
- [x] Bad-link hardening (netsim immediately broke the naive scheduling — locks under
|
||||
isolated loss 15% and isolated delay 120 ms):
|
||||
(a) RTT-adaptive scheduling lead — the client pings automatically (1 Hz, smoothed),
|
||||
and schedules `ceil((rtt + margin) / window)` windows ahead instead of always one:
|
||||
under systematic latency "late" had been the *normal* case. The retraction horizon
|
||||
moves out the same way (a cancellation needs the same lead an action does).
|
||||
(b) Late-rule cleanup in the sim — a late movement action keeps its *order*, not
|
||||
its time (first still-empty upcoming window), so bunched late arrivals no longer
|
||||
collapse onto one slot and eat each other; late NOOPs are dropped outright (as
|
||||
gap-fillers they used to block real steps: retract → late NOOPs poison upcoming
|
||||
windows → replanned steps eaten → retract again — a lock loop).
|
||||
(c) Client stall watchdog — steps in flight but nothing confirmed for 750 ms means
|
||||
the prediction is dead no matter why (e.g. *all* in-flight actions lost: the server
|
||||
never moves, so the moved-off-plan desync detection never fires, path stays full,
|
||||
nothing is ever sent again): retract, drop, replan toward the goal.
|
||||
`PATH_MAX_LEN` is back to 8 as a pure prediction bound — server safety now comes
|
||||
from window addressing, and on a slow link several correct steps are legitimately
|
||||
unconfirmed at once (confirmations lag a full RTT).
|
||||
- [x] Prediction rebuilt as predict → ack → replay (replaces the per-problem patches
|
||||
above with structural robustness; fixed multi-second replan storms at high ping).
|
||||
The old model stored absolute predicted tiles and reconciled by tile matching, so
|
||||
any surprise "invalidated" the whole prediction and recovery meant clear + replan
|
||||
from the confirmed position — but steps inside the retraction horizon cannot be
|
||||
cancelled and still execute ("zombies"), shifting the server off every fresh plan
|
||||
and re-triggering recovery in a loop. Now: pending steps are `(window, delta)`
|
||||
pairs, `EntityPacket.tick` is the acknowledgment cursor (every window ≤ tick/4 is
|
||||
provably consumed — executed, rejected or lost, it no longer matters which), and
|
||||
the predicted position is always *derived* by replaying pending deltas on top of
|
||||
the confirmed position. A surprise shifts the prediction instead of killing it; a
|
||||
route that no longer connects triggers one clean replan toward the persistent goal
|
||||
via the existing send-time validation. Deleted outright: tile-matching reconcile,
|
||||
the moved-off-plan desync heuristic, the stall watchdog, and the idle path drop —
|
||||
acked windows expire pending steps automatically, so the path cannot go stale.
|
||||
The replay applies each pending delta through the shared `step_allowed` rule —
|
||||
exactly as the server will — so a delta the server is going to reject does not
|
||||
move the prediction either, and the predicted position can never sit inside a
|
||||
wall (previously a diverged prediction could, causing a brief walk-into-wall
|
||||
lock until the acks caught up).
|
||||
|
||||
---
|
||||
|
||||
@@ -65,8 +148,9 @@ This supersedes the original 20/10/1 Hz three-tier sketch.
|
||||
|
||||
Remaining follow-ups when the need is concrete:
|
||||
|
||||
- Collision vocabulary: `tile_flags` in `map.rs` is currently empty (all walkable). Fill in
|
||||
collidable / opaque tile ids as the level design requires.
|
||||
- Collision vocabulary: seeded with test values (0, 146) in `shared::tile_collidable`.
|
||||
Decide on a proper file format for tile data (collision, opacity, …) instead of a
|
||||
hardcoded match, then feed both server and client from it.
|
||||
- Tile flipping: orientation is discarded on load; revisit with the sprite pass (09).
|
||||
- Multiple / object layers (spawns, triggers) — not yet parsed.
|
||||
|
||||
@@ -119,10 +203,9 @@ Sprite rendering already landed (see Achieved). Camera status:
|
||||
|
||||
### Later — Robustness + auth
|
||||
|
||||
- Reactive pathfinding: replan from the confirmed position toward the original goal when a
|
||||
route step is blocked or the server diverges from the prediction (currently the route is
|
||||
simply voided)
|
||||
- Auth token handshake: replace source-address identity (`auth_token` field exists but is unused)
|
||||
- Per-address rate limiting for the action ingest (only meaningful once identity is real —
|
||||
address spoofing bypasses any limit before then)
|
||||
- Multi-datagram `EntityPacket`: server currently truncates at 66 entities (`net.rs` TODO)
|
||||
- Asset embedding: `include_bytes!` for single-binary distribution
|
||||
|
||||
|
||||
Reference in New Issue
Block a user