mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
d40344a2c06107c6d59e565b17e15fed96c8a558
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b43ad207c1 |
docs(multiplayer): split spec into MULTIPLAYER_SPEC.md, trim task doc to outstanding work
multiplayer-next.md was a 1662-line mix of standing architecture spec and task-completion tracking, most of which was dense per-task DONE evidence for finished Phases 0-6. Split it: - MULTIPLAYER_SPEC.md (new): the locked architecture decisions, wire format, server-side input handling, prediction/reconciliation, latency/frame-rate budget, and match lifecycle state machine - standing design reference, not task-tracked. - multiplayer-next.md (trimmed 1662 -> ~370 lines): only outstanding work remains - §0 status, §7 Phase 7/8 task tables condensed to "what's left" per task, §8-11 reference material (refactoring notes, gotchas, testing, flagged items). Phases 0-6 collapsed to a pointer at git history instead of ~500 lines of DONE evidence. Also: - Repointed every `multiplayer-next.md §N` code comment (N 1-6) across Game/scripts, Game/tools and Game/tests to MULTIPLAYER_SPEC.md, since those sections moved. Task-number references (`task N.N`, §7-11) correctly still point at multiplayer-next.md. - Updated CLAUDE.md's doc index and docs/TECH_STACK.md's spec-section citations to match. - TODO.md: added a "what's left to actually finish multiplayer (human-actionable)" checklist pulled from multiplayer-next.md §0 and docs/MATCHMAKING.md - things that need a person (hardware, a design decision, a Steam App ID, hands on a controller), not more agent code. |
||
|
|
4fb7ddfecf |
docs(multiplayer): consolidate tracking into one document
multiplayer-todo.md and multiplayer-next.md tracked overlapping information in two places. Fold everything into multiplayer-next.md (architecture decisions, wire format, task breakdown with checkboxes, gotchas list, testing notes) and delete multiplayer-todo.md. Section numbers are unchanged, so existing code comments citing them by section/task number still resolve; update every such reference to point at the new filename. |
||
|
|
06881f05ca |
feat(server): task 6.1/6.3 — dedicated server export preset and a real CLI surface
6.1: "Linux Dedicated Server" preset (dedicated_server=true, custom_features="dedicated_server") mirroring the existing training preset, plus run/main_scene.dedicated_server so the server binary reaches its own entry point with no flag. Builds: an 85MB Linux x86_64 binary, gitignored like the training one. 6.3: scripts/server_config.gd declares every server flag once - name, type, default, section, help - and one parser turns that into parsing, type checking, range validation, config-file backing and --help. The flags had grown to ~30 across server_boot.gd and networked_match.gd, each parsed inline with begins_with, none documented, and an unrecognised flag was SILENTLY IGNORED: --max-clientss=8 ran a server on the default cap and said nothing. Unknown flags, missing values, wrong types, duplicates and out-of-range values are now hard errors, reported all at once. Precedence is command line > config file > default. server_boot.gd parses strictly because it owns the whole command line; networked_match.gd reads the same declaration leniently because it is one consumer of an argv the smoke harnesses also fill with --role= and --drive-seconds=. Nothing is lost - every server flag is declared, so the strict pass already caught any typo before the match scene re-reads its own. 13 unit tests covering the precedence order, the typo rejection that motivated this, --no-<bool> not double-listing in --help, and --help documenting every flag asserted against the declaration rather than a hand-kept list. Verified end to end: --help prints, a typo'd flag refuses to start, and the plain/replay-log/late-joiner smoke scenarios still pass. |
||
|
|
866efa0d9b |
fix(multiplayer): server no longer rate-limits a backlog it caused itself
Closes task 5.10's three recording gaps, and the gap-closing found a real input-loss bug. Replay log: a failed write now ends the log permanently instead of desyncing every later record's framing; close() is called from _exit_tree with a summary, since the RefCounted destructor closes it implicitly but never says whether the log is complete; rejected packets are recorded with their reason in the kind byte (framing unchanged, FORMAT_VERSION 2 so "no rejects" differs from "this build never recorded them"). Recording is capped at 8 per peer per window - uncapped, the diagnostic is a remote disk-fill amplifier, since the attacker picks the packet rate. Uncapped totals live on MatchSim and survive the peer's disconnect. The bug: a 2s host stall has the client sending at 60Hz throughout, and ENet delivers that whole backlog in the first window after resume - 70 of an honest client's packets rejected as "rate limit exceeded". Redundancy does not cover it, because the dropped packets are contiguous: 0 of 70 rescued, and 82 of 923 sequences (8.88%, ~1.4s of input) never reached the server, against 0.00% with no stall. Every prediction gate passed. Fixed by granting each already-tracked peer a capped, two-window packet grace when the server detects its own wall-clock stall. Rate-limit rejects 70 -> 0, sequences missing 8.88% -> 0.00%, seq-guard rejects 9 -> 0. Controls on the unfixed build lost 4.34/7.52/7.86%. All three abuse roles still disconnect and no flood induced a stall, so the grace cannot be farmed. Also corrects an earlier wrong conclusion: the reviewer's free-flight p95 0.688 is real and reproduces on two processes with 0.0% snapshot loss. The plain --role=client drive fails the 0.5 free-flight bound in 3 of 8 runs because that drive is mostly a contact test - the harness comment already said so - leaving a cohort as small as 12 samples. Near-surface error is genuinely several times open-air error, so the calibrated bound now belongs to --exercise-free-flight alone and the plain role asserts the always-well-sampled all-cohort percentiles at 1.2/2.0, printing the free-flight numbers as reported-not-asserted. 6/6 plain runs pass where 3/7 failed; tightening to 0.3 still fails. tools/replay_dump.gd reads a log back: counts by kind, plus how much of the input sequence stream reached the server once redundancy is counted. |
||
|
|
75f485667b |
feat(multiplayer): Phase 4 prediction correctness + two input-death fixes
Closes Phase 4's outstanding action-sequence-correctness invariant, then fixes two server-side bugs an adversarial review of that work uncovered. Server simulation, bot observations, collision resources and tick rate are unchanged: the server_physics_parity trace is byte-for-byte identical to HEAD across 360 ticks including both ships' full observation vectors. 4.11 - prediction history filed under the ISSUING sequence _send_local_input filed each post-step predicted state under the timeline's estimate of the sequence the server would consume this tick, trailing issuance by input_lead. The body had integrated the intent issued under _input_seq, so predicted[S] held "state after the intent from now" while the server's authority for S is "state after action(S)". They agree only while the stick is still. Filing under _input_seq costs nothing: which action the ship uses is decided in LocalNetShipController.get_action() and is untouched. Every prior Phase 4 gate held its input steady, and a steady input cannot falsify a sequence label - the 60s runs honestly reported marker=0/3784. New --exercise-input-transitions role toggles thrust every 6 ticks; it is the only gate that can catch a label regression. Verified non-vacuous: the old label fails it at 50%. 4.12 - issued-but-unsimulated sequences, and the release path An attack (delta > 1) issues and sends several sequences for one local physics step. Those gap sequences had no recorded prediction, so a server ack of one reported missing_not_recorded - indistinguishable from ring loss, costing a teleport and resync suppression several times a minute. They are now recorded stateless via record_unsimulated() and answered with a new "skip" decision mode. Free-flight hard snaps: 25/8/4 -> 0/0/0. A release (delta == 0) re-recorded at the unchanged _input_seq, filing the current intent under a sequence that went out carrying a different action; LocalInputTimeline deliberately refuses to mutate an issued sequence, so the ring contradicted the wire. Recording is now skipped on release ticks. 4.13 - two Phase 3 bugs silently killing player input (a) InputJitterBuffer.consume() advanced last_applied_seq on every tick including a starve. Since ingest() discards seq <= last_applied_seq, one starve on a sequence the client had not sent yet stranded the stream one ahead of arrivals permanently - both sides advancing in lockstep, every honest packet discarded on arrival. The client's own input_lead release is enough to trigger it, so input died for ~30 ticks roughly every 6.5s on a clean LAN. Now only gives up on a sequence once strictly newer data proves it lost. Silent-client stall and ring-overflow resync are unchanged. (b) The seq-range guard bounded incoming seq against highest_ingested_seq, which only advances inside ingest(), which that guard gates. After a ~2s host hitch every packet was rejected forever with no diagnostic (600+ consecutive rejections reproduced via SIGSTOP). Third iteration of this guard; each previous version bounded against a value only the accepted path could advance. Adds an escape after 10 consecutive rejections, which grants an attacker nothing the rate limiter does not already bound. (c) The transitions gate reported PASS at 3.76% while input was completely dead, because suppression stops _record_metrics - a worse outage yields fewer samples and a LOWER rate. Now scales the required sample count with run length and asserts the wire's server_stalled bit. Reverting both fixes makes it fail at samples 292/600, server_stalled=true, input_lead=12. Fixing (a) also explained a residual the review had already traced: 151 of 151 action-marker mismatches were the server repeating a stale action on a starve, not a prediction defect. Marker is now 0.00% in all three conditions (was 1.7-2.5%), and free-flight p99 improved to 0.141/0.168/0.154m from 0.170/0.176/0.184m. Two pre-existing test defects fixed alongside: the ball gate asserted RTT-masking on a link with no RTT (flaked 2 in 5; now asserted only at rtt >= 20ms, 5/5 under latency), and the two-bot CI compared scores across a 3-5s window (now polls the scores the server actually held; note score_changed is emitted only on the client path). QA: 72 unit tests; 60s free-flight at LAN/80+-20ms/5% loss; transition gate in all three; 2.0s and 3.5s host-freeze recovery; ball contact x5; two-bot CI x3; all three abuse roles; net/match_net/clock/lobby smokes. Phase 4 sign-off still pending a human playtest at ~100ms RTT - the milestone asks how it feels, which no gate here answers. |
||
|
|
04691aaa48 |
chore(multiplayer): Phase 0 refactors + graphics/perf settings groundwork
Lands the non-networked Phase 0 tasks from multiplayer-todo.md (ship/camera/ arena refactors, sim constants, background FPS handling) plus a first pass at exposing graphics/performance settings (presets, resolution scaling, vsync, FPS cap, perf overlay) and a GPU profiling harness for the real-hardware follow-up in task 0.15b. |
||
|
|
18fdb0f232 |
feat(arena): resize for 5v5, cut a real goal-wall hole, and bake collision geometry
Retraining from scratch removes the constraint that blocked these: the existing checkpoints in Game/bots/promoted/ no longer need byte-identical geometry. - Fix the goal aperture constants (were 1.85/1.65, now match objects/goal.tscn's actual sensor exactly at 1.75/1.5) so a ball crossing the visible edge can't fail to score. - Cut a real navigable hole in each end wall's collision (_build_end_wall_colliders), replacing the previous solid box — the ball now genuinely enters the net instead of triggering the sensor a hair before hitting a solid wall. Correct for both FLOOR and ELEVATED goal modes via _goal_surround_bounds. A separate, slightly wider GOAL_VISUAL_APERTURE_* pair keeps the collision hole exact while still giving goal.gd's bezel/rim frame clearance to be seen against the hull cut. - Resize the play volume 1.5x (INNER_HALF_X/Z/HEIGHT 12/18/12 -> 18/27/18) to comfortably fit a 5v5 roster: updates every hand-authored literal in arena_boundary.tscn/arena_base.tscn/the elevated arena variants that doesn't derive from those constants, adds 5 spawn markers per team, and rescales ship_observations.gd's normalization scales and arena_02's cosmetic decoration/nebula-dust shader uniforms to match. - Bake the ~170 runtime-generated CollisionShape3D nodes into the scene via a new bake_colliders()/tools/bake_arena_boundary.gd instead of rebuilding them on every load — a real load-time cost repeated in every parallel headless training env. Colliders must be direct children of the StaticBody3D to register at all, so bake_colliders() parents them onto self and tags them with a group for idempotent re-baking, rather than grouping them under an intermediate container node. _ready() self-heals: it skips regenerating only when an existing bake's goal_mode metadata matches the current one, so the FLOOR-mode bake in the shared scene is never silently reused by an ELEVATED arena variant. |