Compare commits

...

5 Commits

Author SHA1 Message Date
Josh Creek fe453ab607 docs: add a version inventory to TECH_STACK.md, refresh stale claims
TECH_STACK.md explained why each choice was made but never listed what is
actually pinned, so there was no single place to answer "what version of X do
we use". Adds a Version inventory section covering the shipped game, the Go
control plane's four direct dependencies, the datastore/platform versions and
the exactly-pinned training stack, plus a Verification toolchain subsection for
the Make/Docker/kind/Kustomize/Actions harness.

Also corrects two things the doc had outgrown: the allocation pipeline is now
wired end to end and gated in CI, so only the provider deployment remains; and
the Steam section covered only the GodotSteam client transport, omitting the
server-side Web API ticket verifier in server/steam.
2026-09-05 23:16:10 +01:00
Josh Creek 52cc478b38 docs: record the assertion-first debugging habit, and refresh CLAUDE.md
Adds the lesson this branch paid for repeatedly: the expensive failures
were not broken behaviour but assertions that could not distinguish the
two states they implicitly claimed to, each reporting its own ambiguity
as a confident verdict about the system under test. Waiting on a Fleet
field that does not exist, treating not-yet-started as exited, a p99
that conflated regression with scheduler noise, a validator reading a
response shape Agones never sends, and a build guard that verified stale
code. Five in one session, several costing multiple CI round trips.

Two habits go with it, both of which beat reading code every time they
were tried: make the script report what it saw before theorising about
why, and verify the diagnostics actually fire -- two dumps were added
here and neither ran, one suppressed by a reachability guard and one by
an ERR trap that cannot fire inside functions without errtrace.

Also fixes two stale claims and one gap. Audio is no longer "none at
all"; a procedural AudioManager covers UI, countdown, impact, goal and
engine cues, and only authored assets remain. Five docs/ contracts that
server/security asserts against the manifests were unlisted. And the Go
control plane -- a third of the codebase and the current focus -- had no
structural description at all, so it now gets one: package layout, which
binary is test-only, and the three things easiest to get wrong
(integration tests hidden behind a build tag, start-time config, the
versioned wire contract).

TODO.md's entry now points at its ordered backlog rather than describing
it as deferred non-multiplayer work.

Every factual claim in the new section was checked against the tree.
2026-09-05 23:10:06 +01:00
Josh Creek 4ea72be581 fix(compose): stop calling a still-starting game server dead
Allocated Compose failed on a docs-only commit, so nothing functional
had changed. Its own diagnostics showed why: the game server logged a
clean `server_started` -- the exact string the readiness loop waits for
-- and the script reported "game server exited before becoming ready".

The guard asked whether the service was absent from
`compose ps --status running`, which is also true of a container that
has been created but has not started yet. On a slow runner the first
poll can land in that window, and the script concluded the server was
dead when it was still coming up. Ask whether it actually exited
instead.

Also set errtrace. This failure produced no "failed at line N" report
despite the ERR trap added in 432e5a11, because a bare `trap ... ERR`
does not fire inside functions or subshells without it -- the
instrumentation had a blind spot exactly where a readiness loop lives.

The other --status running check, after an explicit `compose stop`, is
correct and unchanged: stop is synchronous, so absence there really does
mean stopped.

Verified by two consecutive local runs.
2026-09-05 23:00:03 +01:00
Josh Creek 4560d2de8a docs: say what order the outstanding work goes in
The backlog listed what is left but not what to do first, and priority
labels do not answer that: #33 is P2 yet belongs before the P0 cluster,
because standing the cluster up first means migrating a running one
afterwards.

Add an explicit ordering to TODO.md in three parts -- a critical path
where each item unblocks the next, a Steam track that runs in parallel
and should start early because its lead time is Valve's, and the set
that is unblocked today and waiting on nobody. The playtests, training
runs and asset work need no cluster and could start now, which was not
obvious from a flat list sorted by priority.

#31 is called out as the highest-leverage item: two answers unblock the
whole of Phase 8, and the work behind them is an agent's.

Two open issues were in no list at all -- #23's design question and
#32's backfill work -- so TODO.md now covers every open issue.

Also record the dependency direction on GitHub rather than only here:
#17, #32 and #33 carried no blocked-by statement, so the graph was
invisible from the issue tracker. And note in §7 task 8.12 why the
workload namespace enforces privileged and where the split is tracked.
2026-09-05 22:50:26 +01:00
Josh Creek 4912837dd7 docs: narrow what #31 actually needs from a person
The issue read as broadly human-gated. Most of it is not: GHCR accepts
the built-in GITHUB_TOKEN with packages: write for the repository's own
namespace, so publishing needs no account, stored secret or spend
approval, and signing and tagging policy can land as a reviewable
default rather than waiting on a decision.

Two things genuinely block. Every manifest references
ghcr.io/cosmic-clash/*, and no such organisation exists -- the API
returns 404 and it is not among this account's orgs -- so nothing can be
pushed there. And this repository is private, so GHCR packages inherit
that, while no manifest declares imagePullSecrets; public packages work
as written, private ones need pull secrets threaded through every
workload.

Same optimistic-to-pessimistic drift the §7 audit found in fifteen other
entries: work described as blocked on a person when the person only owes
a decision.
2026-09-05 22:45:26 +01:00
5 changed files with 269 additions and 9 deletions
+90 -2
View File
@@ -6,7 +6,7 @@ Important rule: never create co-authored commits. Never mention Claude in commit
## Project overview
Cosmic Clash is an open-source, physics-based "vehicle soccer" game (a spiritual successor to Rocket League) built in Godot 4.7, using space ships instead of cars. The game and the dedicated server are GDScript/Godot only — the "C# backend" an early README described was never started, and the dedicated server is an export of this same Godot project. There is one component outside the Godot project: a **Go matchmaking control plane** in `server/` for casual/ranked queues, ranked ratings and Agones-based server allocation. It is a 1.0 launch blocker — see `docs/MATCHMAKING.md` for the design, `multiplayer-next.md` §0 and §7 for what remains (the allocation-to-connect pipeline is now wired end to end; what is left is external — a Steamworks App ID, custom GodotSteam builds, and a live Agones cluster), and `docs/TECH_STACK.md` for why the control plane is Go rather than C#, Rust or C++. README.md's "MVP is local-only against bots" section is historical: server-authoritative online multiplayer, a headless dedicated server, Docker/CI verification, and an optional Steam transport are all implemented (Phases 16). See `multiplayer-next.md` for what actually remains.
Cosmic Clash is an open-source, physics-based "vehicle soccer" game (a spiritual successor to Rocket League) built in Godot 4.7, using space ships instead of cars. The game and the dedicated server are GDScript/Godot only — the "C# backend" an early README described was never started, and the dedicated server is an export of this same Godot project. There is one component outside the Godot project: a **Go matchmaking control plane** in `server/` for casual/ranked queues, ranked ratings and Agones-based server allocation. It is a 1.0 launch blocker — see `docs/MATCHMAKING.md` for the design, `multiplayer-next.md` §0 and §7 for what remains (the allocation-to-connect pipeline is now wired end to end; what is left is external — a Steamworks App ID, custom GodotSteam builds, a registry to publish images to, and a live Agones cluster; `TODO.md` orders them), and `docs/TECH_STACK.md` for why the control plane is Go rather than C#, Rust or C++. README.md's "MVP is local-only against bots" section is historical: server-authoritative online multiplayer, a headless dedicated server, Docker/CI verification, and an optional Steam transport are all implemented (Phases 16). See `multiplayer-next.md` for what actually remains.
Because the gameplay concept (vehicle soccer) can't be copyrighted but specific expression can, all code/art/assets must be original — this is why the project uses Godot instead of Unreal/Unity and ships instead of cars. Keep this in mind when writing code or pulling in assets: don't port or closely mirror Rocket League's actual implementation.
@@ -22,7 +22,9 @@ The prose docs carry far more design rationale than the code comments, and sever
- `FLIGHT_MANUAL.md` — the player-facing flight model.
- `docs/MATCHMAKING.md` — casual/ranked queue design, and the locked constraints (Go/PostgreSQL/Redis/Agones) the `server/` module implements. Partially implemented; a 1.0 launch blocker, and the reason a backend service outside the Godot project exists at all.
- `docs/TECH_STACK.md` — what the project is built with and why, including the Go-vs-C#/Rust/C++ rationale for the matchmaking control plane.
- `TODO.md` — deferred non-multiplayer work (audio is the big one: there is none at all).
- `TODO.md` — deferred non-multiplayer work, **and** the ordered human-actionable backlog: which GitHub issue to do first, what each one unblocks, and which items are waiting on nobody. Start there when asking "what next". Audio is no longer absent — a procedural `AudioManager` covers UI, countdown, impact, goal and engine/turbo cues; what remains is authored assets.
- `docs/THREAT-MODEL.md`, `docs/SUPPLY-CHAIN.md`, `docs/OBSERVABILITY.md`, `docs/MATCHMAKING-SLOs.md`, `docs/ADR-001-matchmaking-platform.md` — the control plane's security, release, telemetry and SLO contracts. `server/security/*.py` asserts several of them against the checked-in manifests, so changing a manifest often means changing one of these.
- `docs/REVIEW-2026-09-feat-multiplayer.md` — a point-in-time adversarial review of this branch. Every finding in it is fixed; it is kept for the reasoning, not as a status report, and its header says so.
## Godot MCP server
@@ -149,6 +151,46 @@ To run one by hand, and for every config flag, see `SERVER.md`. `--smoke-force-g
Note what is *not* in CI: `make verify-multiplayer-local` (the combined local gate, which also runs the Python manifest/contract suites) and the `integration`-tagged Go tests, which need a real PostgreSQL/Redis and live in `scripts/run_*_integration.sh`. Run those by hand before landing server changes.
### When a gate fails, suspect the assertion first
The most expensive failures in this repo have not been broken behaviour. They
have been **assertions that cannot distinguish the two states they implicitly
claim to**, each reporting its own ambiguity as a confident verdict about the
system under test. Five in one session, several costing multiple CI round trips:
| Assertion | What it actually conflated |
|---|---|
| `kubectl wait --for=jsonpath='{.status.ready}'` on an Agones Fleet | field does not exist vs. condition unmet — it could never pass |
| `compose ps --status running \| grep -qx game-server` | not started *yet* vs. exited |
| `remote_residual_position_p99 < 0.3` | real regression vs. host scheduling noise |
| a validator reading `status.gameServer` | Agones' real response vs. an invented one, with unit tests asserting the invention |
| `docker image inspect` guarding a build | image is current vs. image merely exists, so a rerun verified stale code |
Before theorising about the code, ask: **can this check tell "broken" from
"not ready yet", "absent" from "unset", or "regressed" from "slow"?** If not,
that is the bug, whatever else is also true.
Two habits follow from it, and both repeatedly beat reading code:
- **Make the script say what it saw before diagnosing why.** Most gates here are
`curl -fsS` and bare `[[ ]]` under `set -e`, which abort silently — several CI
runs produced nothing but `make: *** Error 1`. Report the failing line and
command, print the value that failed its comparison, and dump the surrounding
state *before* any cleanup trap destroys it. Every root cause found in that
session came from doing this; essentially every confident guess made without
it was wrong.
- **Verify the diagnostics fire.** Two separate dumps were added and neither ran:
one behind a `kubectl cluster-info` guard that misjudged reachability, one
because a bare `trap ... ERR` does not fire inside functions or subshells
without `set -E`. A diagnostic that has never been seen working is not
evidence.
And when a test and the code agree but reality disagrees, suspect they were
written together. A validator and its fixtures both encoded a response shape
Agones never sends; nothing caught it because the gate had never run far enough
to see a real one.
### Other
- The `mcp/godot-mcp` submodule is a separate Node/TypeScript project with its own `npm install` / `npm run build` (see above) — it is tooling, not part of the game itself.
@@ -199,6 +241,52 @@ Server process: `scenes/server_boot.tscn` (`server_boot.gd`) is the shell — st
Known-insecure, and the reason public hosting is gated: **slot reclaim is keyed by display name**, so anyone who knows a disconnected player's name can take their reserved slot. Verified Steam identity (Phase 7) is the fix. Don't expose a server to strangers before then.
### Matchmaking control plane (`server/`, Go)
The only component outside the Godot project, and roughly a third of the
codebase. Layered so policy is testable without a database and persistence
without a network:
- `domain/` (~3.2k lines) — **pure policy, no I/O**: matcher formation and
rating tolerance, Glicko ratings and tiers, proposal/queue/match state
machines, casual lineup and backfill selection, probe validation, join
authorisations. Most behaviour worth asserting lives here and needs no
fixture. `ranked.go`'s arena list is checked against `arena_registry.gd` (see
Arena registry above).
- `store/` (~5.3k) — PostgreSQL boundaries. Every mutation goes through
`RunSerializable`; contention is expected rather than exceptional, so the
retry budget and jittered backoff there are load-bearing, not decoration.
- `api/` (~2.5k) — HTTP surface and the outbox dispatchers. `Service` is a
struct of optional providers, each nil-guarded into a 503, which is why a
binary can look healthy while a whole feature is unreachable — check what
`cmd/*/main.go` actually assigns before concluding a feature is broken.
- `allocator/`, `supervisor/`, `agones/` — allocation, the Go process that
wraps the exported Godot server in an Agones pod, and the Agones client.
- `matcher/`, `workload/`, `observability/`, `steam/`, `testkit/` — the matcher
worker loop, workload-token signing, metrics, the Steam Web API adapter, and
deterministic offline fakes.
`cmd/` holds seven binaries: `control-plane`, `matcher`, `allocator`,
`maintenance`, `game-server-supervisor`, `migrate`, and `testkit-api`.
**`testkit-api` is test-only** — it injects a fake Steam login that accepts any
ticket, and must never be deployed in place of `control-plane`.
Three things that are easy to get wrong:
- **Integration tests are behind `//go:build integration`** and need a real
PostgreSQL/Redis, so `go test ./...` silently skips them. Run them through
`scripts/run_*_integration.sh`, which start their own disposable containers.
`go vet -tags integration ./...` is worth running too, or those files rot
uncompiled.
- **Config is start-time.** Tier bands, the join-signing key set, Steam
credentials and the probe providers are all read once in `main()`. Changing
them is a rolling restart, not a hot reload — deliberate, and consistent with
how everything else in these binaries is supplied.
- **The wire contract is versioned.** `contracts/v1/openapi.json` and
`state-transitions.json` are asserted by `contracts/v1/test_contracts.py`;
changing a status code or operation ID without updating them breaks generated
clients silently.
### Steam transport
`net_transport.gd` (`NetTransport`) is a deliberately narrow boundary: a transport only *creates a peer*; `NetworkManager` keeps ownership of polling, RPC policy and lifecycle. `enet_transport.gd` and `steam_transport.gd` implement it. `NetworkManager.host()/join()` default to `"enet"`; passing `"steam"` **never falls back** — a missing custom build or failed init returns an error naming the missing prerequisite (`steam_bootstrap.gd` produces those messages). Discovery and server advertisement are intentionally unimplemented until a project-owned App ID exists; the local default is Valve's Spacewar App ID 480, which must never be used to advertise servers or ship.
+58 -1
View File
@@ -43,6 +43,61 @@ Everything below needs a person — hardware, a design decision, an external acc
Each item is also a GitHub issue (linked inline), labelled `needs:human` plus a
`P0-blocker``P3-low` priority. Close the issue and tick the box together.
**Priority labels say how much something matters; this says what to do first.**
They differ: #33 is P2 but belongs before the P0 cluster, because standing the
cluster up first means migrating a running one afterwards.
#### Do these in order — each unblocks the next
1. **[#31](https://github.com/jcreek/CosmicClash/issues/31) — answer two
questions.** Which registry namespace (`ghcr.io/cosmic-clash/*` is in every
manifest and no such org exists), and whether packages are public (this repo
is private and no manifest declares `imagePullSecrets`). Publishing needs no
new credential. **This is the highest-leverage thing on the list**: two
answers unblock the whole of Phase 8, and the work behind them is an agent's.
2. **[#33](https://github.com/jcreek/CosmicClash/issues/33) — split the
game-server namespace.** Agent work, no decision owed. Before #17 rather than
after, so the cluster is stood up on the final topology instead of being
migrated later.
3. **[#17](https://github.com/jcreek/CosmicClash/issues/17) — stand up the
cluster.** Needs #31's images to exist. Unblocks the production halves of
most of Phase 8.
4. **[#32](https://github.com/jcreek/CosmicClash/issues/32) — casual backfill.**
Mostly agent work; the design decision is already made. Needs #17 to verify
a late roster reaching a running server.
5. **[#22](https://github.com/jcreek/CosmicClash/issues/22) — release gates.**
Last: needs the cluster and the App ID.
#### Steam, in parallel — long external lead time, start early
6. **[#15](https://github.com/jcreek/CosmicClash/issues/15) — App ID and
publisher key.** Valve coordination, so the calendar time is theirs, not
yours. The adapter is written and config-gated: sign-in returns 503 until
both values are set.
7. **[#16](https://github.com/jcreek/CosmicClash/issues/16) — GodotSteam build
templates.** The client-side ticket code is written and needs the custom
build to run.
#### Unblocked today — nothing is stopping these
- **[#19](https://github.com/jcreek/CosmicClash/issues/19)** then
**[#18](https://github.com/jcreek/CosmicClash/issues/18)**: the 3v3 gate is
the cheaper session to arrange and exercises #18's latency conditions
incidentally, so doing it first can settle both.
**[#20](https://github.com/jcreek/CosmicClash/issues/20)** needs two machines
and the internet, not a cluster.
- **[#24](https://github.com/jcreek/CosmicClash/issues/24)** then
**[#25](https://github.com/jcreek/CosmicClash/issues/25)**: training runs,
independent of everything above.
- **[#21](https://github.com/jcreek/CosmicClash/issues/21)**,
**[#26](https://github.com/jcreek/CosmicClash/issues/26)**,
**[#27](https://github.com/jcreek/CosmicClash/issues/27)**,
**[#28](https://github.com/jcreek/CosmicClash/issues/28)**: hardware, audio,
font, graphics QA. No dependencies, no ordering between them.
- **[#23](https://github.com/jcreek/CosmicClash/issues/23)**,
**[#29](https://github.com/jcreek/CosmicClash/issues/29)**: open design
questions with no deadline. Neither blocks anything.
- [x] ([#14](https://github.com/jcreek/CosmicClash/issues/14)) **Join-signing design decided and implemented.** Resolved as HMAC-SHA256 over the canonical claim bytes with a **key ID inside those bytes**: the allocator signs with one named key while allocated servers hold the set of currently-valid keys, so rotation does not invalidate authorisations already issued for in-flight matches. `allocator.Worker` now publishes the signed roster after binding, and `cmd/allocator` refuses to start without key material. Rotation procedure is in `docs/MATCHMAKING.md` §2; see `multiplayer-next.md` §8.31. Nothing human-only remains here — live verification is covered by [#17](https://github.com/jcreek/CosmicClash/issues/17).
- [ ] ([#18](https://github.com/jcreek/CosmicClash/issues/18)) **Phase 4 playtest at ~100 ms RTT** — does the ship/ball feel local, do contact corrections read as bumps or glitches? Every numeric gate is green; this is a feel judgment no metric can answer. `multiplayer-next.md` §0, gate A.
- [ ] ([#19](https://github.com/jcreek/CosmicClash/issues/19)) **Phase 5 3v3 gate** — a full 6-player match start to finish, with a mid-match disconnect and a late joiner. Only verified so far at 1v1 plus a two-bot CI match. `multiplayer-next.md` §0, gate B.
@@ -51,9 +106,11 @@ Each item is also a GitHub issue (linked inline), labelled `needs:human` plus a
- [ ] ([#16](https://github.com/jcreek/CosmicClash/issues/16)) **Supply custom GodotSteam client/server build templates** and pin them in `steam-dependencies.lock.json` (`COSMIC_CLASH_STEAM_CLIENT_GODOT` / `COSMIC_CLASH_STEAM_SERVER_GODOT`) — `make verify-steam-templates` refuses a stock Godot binary until these exist. `STEAM.md`.
- [ ] ([#21](https://github.com/jcreek/CosmicClash/issues/21)) **Reference-hardware profiling (task 0.15b)** in the live editor on real low/mid-tier hardware — blocks 0.16, 0.17/0.17b/0.17c/0.17d, 0.26 (arena GI bake), and 0.28 (physics separate-thread prototype). Covered above; listed again here because it also gates Phase 5.5's graphics QA gate for multiplayer sign-off.
- [ ] ([#17](https://github.com/jcreek/CosmicClash/issues/17)) **Stand up the live Kubernetes cluster and Agones deployment** for Phase 8 — provider-portable manifests exist, but nothing has run against a real cluster; needs the provider-specific deployment overlay (network, DNS, secrets) per `docs/MATCHMAKING.md`.
- [ ] ([#31](https://github.com/jcreek/CosmicClash/issues/31)) **Build, push and pin the container images the Kubernetes manifests reference.** Every image target builds, but no workflow publishes any of them and all manifest digests are still all-zero placeholders, so `deploy/k8s/base` cannot pull running images. Needs a registry namespace, publish credentials, and a signing/provenance decision; once digests are real, turn on `--require-concrete` in `make verify-supply-chain` so a placeholder can no longer pass. Blocks [#17](https://github.com/jcreek/CosmicClash/issues/17).
- [ ] ([#31](https://github.com/jcreek/CosmicClash/issues/31)) **Build, push and pin the container images the Kubernetes manifests reference.** Every image target builds, but no workflow publishes any of them and all manifest digests are still all-zero placeholders, so `deploy/k8s/base` cannot pull running images. **Only two things need a person**: the `ghcr.io/cosmic-clash/*` namespace in the manifests does not exist (no such org), and this repo is private while no manifest declares `imagePullSecrets`, so package visibility must be chosen. Publishing itself needs no new credential — GHCR accepts the built-in `GITHUB_TOKEN` with `packages: write` — so the workflow, digest pinning and enabling `--require-concrete` are agent work once those two are answered. Blocks [#17](https://github.com/jcreek/CosmicClash/issues/17).
- [ ] ([#33](https://github.com/jcreek/CosmicClash/issues/33)) **Move game servers to their own namespace** so `cosmic-clash` can enforce `restricted` again. Agones' Dynamic port policy needs a `hostPort`, which `baseline`/`restricted` forbid, so the whole namespace dropped to `privileged` — including the control plane, which mounts the database DSN, workload secret and Steam publisher key. Deferred until the Agones gate was green so a new failure could not be ambiguous.
- [x] (no issue — agent-actionable) **Phase 8.48 has its own Compose smoke fixture.** `compose.allocated-smoke.yml` and `scripts/verify_allocated_compose.sh` are independent of `compose.phase6-smoke.yml` — the script states so explicitly and reuses none of its ports — so the allocated-mode flow no longer inherits that fixture's hardcoded port, first-come slots or `--max-matches=2`. Exercised by `make verify-allocated-compose`.
- [ ] ([#23](https://github.com/jcreek/CosmicClash/issues/23)) **Decide the contact-cohort-only client-side shadow world** (open question F in `multiplayer-next.md` §0). A design call about whether contact pairs get a client-side shadow simulation; nothing is blocked on it, and it can stay open indefinitely without holding anything up.
- [ ] ([#32](https://github.com/jcreek/CosmicClash/issues/32)) **Implement casual backfill** — proposal, matcher pass, client offer UI and late roster delivery. Listed here because it has an issue, not because it needs you: the roster-delivery design is decided (`docs/MATCHMAKING.md` § Casual) and candidate selection has landed, so the rest is agent work. End-to-end verification needs the cluster ([#17](https://github.com/jcreek/CosmicClash/issues/17)).
- [ ] ([#22](https://github.com/jcreek/CosmicClash/issues/22)) **Release-evidence and human sign-off gates for Phase 8 production launch** — once the above are done, someone needs to actually run and sign off the production-shaped checks `multiplayer-next.md` §7 lists as infrastructure/production-dependent.
Defect **C** (slot reservation keyed on display name alone — real, demonstrated, exploitable during the 30 s disconnect window) is not its own action item: it is fixed for free by the Steam auth tickets in task 7.4 above, so nothing to do until Steam identity lands.
+113 -3
View File
@@ -105,6 +105,15 @@ port-forwarding requirement and to supply verified player identity —
direct-IP ENet's slot-reclaim logic is keyed by display name today, which is
insecure against a public server (`multiplayer-next.md` §0, known defect C).
There is a **second, independent** use of Steam that does not involve
GodotSteam at all: `server/steam/` verifies session tickets server-side against
Valve's `ISteamUserAuth/AuthenticateUserTicket` Web API over plain HTTP, which
is what turns a claimed identity into a trusted one for matchmaking and for
slot reclaim. It distinguishes "Valve rejected this ticket" (401) from "Valve
is unreachable" (503) so an outage cannot be mistaken for an authentication
failure, and refuses family-shared and banned accounts. It needs a **publisher
Web API key**, which is a server-side secret and must never reach a client.
## Dedicated server hosting: Docker (primary) or native systemd
The dedicated server is not a separately-written service — it's the same
@@ -206,6 +215,81 @@ file and runs **inside the game** in pure GDScript — shipped bots need no
Python, no .NET, no network." Keeping the shipped game GDScript-only (no
.NET Godot build) is consistent with the rest of the stack.
## Version inventory
Everything the project actually pins, in one place. The rest of this document
explains *why* these were chosen; this is *what* is in use. Versions here are
the source of truth's values at the time of writing — when they disagree with
the files named, the files win.
### Shipped game and dedicated server
| Thing | Version | Pinned in |
|---|---|---|
| Godot | 4.7.1 | `Dockerfile` (digest-pinned `barichello/godot-ci`) |
| Physics | Jolt | `Game/project.godot``3d/physics_engine="Jolt Physics"` |
| Runtime dependencies | none | pure GDScript; no .NET, no ONNX, no native extensions in the default build |
| GodotSteam | custom build, opt-in | `steam-dependencies.lock.json` |
The shipped client and server carry **no third-party runtime dependency at
all** in the default ENet build. That is a deliberate constraint, not an
accident of scope — see "What's deliberately absent".
### Matchmaking control plane (Go)
| Thing | Version | Notes |
|---|---|---|
| Go | 1.23 | `server/go.mod` |
| `jackc/pgx/v5` | 5.7.4 | PostgreSQL driver; used through `database/sql` for pooling, and directly for `LISTEN`/`NOTIFY`, which needs a dedicated session |
| `redis/go-redis/v9` | 9.7.0 | transient candidate index only; the durable queue is PostgreSQL |
| `alicebob/miniredis/v2` | 2.38.0 | test-only in-process Redis |
Four direct dependencies, three of them drivers. There is no web framework, no
ORM, no DI container and no code generation: HTTP is `net/http` with a hand-
written mux (`server/api/service.go`), SQL is hand-written, and migrations are
numbered `.sql` files under `server/migrations/` — each with a `down/`
counterpart — applied by the `cmd/migrate` binary. That is a deliberate choice
about a service whose whole job is a small number of carefully-fenced
transactions.
Rating maths is Glicko-2, implemented in `server/domain/rating.go` rather than
taken from a library.
### Datastores and platform
| Thing | Version | Pinned in |
|---|---|---|
| PostgreSQL | 17 (alpine) | `compose.*.yml`, `scripts/run_*_integration.sh` |
| Redis | 7 (alpine) | `compose.*.yml`, `scripts/run_redis_integration.sh` |
| Agones | 1.49.0 | `scripts/verify_kind_agones.sh` (`AGONES_VERSION`) |
| Kubernetes | 1.33 in CI | `kindest/node:v1.33.1` |
| Manifests | Kustomize | `deploy/k8s/base` + `overlays/{eu,na}` |
| Metrics | Prometheus | `deploy/observability/` — ServiceMonitors and PrometheusRules |
Container images are referenced by digest, never by tag; `scripts/verify_supply_chain.py`
fails the build on any mutable reference. All six digests under `deploy/` are
currently all-zero placeholders, and the `ghcr.io/cosmic-clash/*` registry
namespace does not exist yet — publishing the images is the open work tracked
in issue #31, and is the last thing standing between the manifests and a real
deployment.
### Training (out-of-process, not shipped)
| Thing | Version |
|---|---|
| Python | 3.12 |
| `godot-rl` | 0.8.2 |
| `stable-baselines3` | 2.4.0 |
| `torch` | 2.13.0 |
| `gymnasium` | 1.0.0 |
| `tensorboard` | 2.21.0 |
Pinned exactly, and `training/requirements.txt` explains why in unusual detail:
the curriculum depends on specific library *internals* rather than documented
public APIs, so an unpinned reinstall could silently change behaviour partway
through a 12-hour training stage. None of this ships — the game runs exported
policies through a pure-GDScript MLP.
## Tooling (not shipped with the game)
- **`mcp/godot-mcp`** (git submodule, Node/TypeScript) — drives a live
@@ -218,6 +302,29 @@ Python, no .NET, no network." Keeping the shipped game GDScript-only (no
(Blender's embedded Python, plus texture generators) used to produce the
project's original meshes and textures.
### Verification toolchain
Everything is driven from `Makefile` targets so that CI and a local run are the
same command:
- **GNU Make** — the single entry point (`verify-phase6`,
`verify-enet-integration`, `verify-kind-agones`, `verify-supply-chain`, …).
- **Docker and Docker Compose** — the multi-process gates. The game gates use
a staged `Dockerfile`; the control-plane gates use `compose.*.yml` fixtures.
- **kind** (`kindest/node:v1.33.1`) **and Helm** — a throwaway Kubernetes
cluster with Agones installed, for the allocation gate.
- **Kustomize** — `deploy/k8s/base` plus `overlays/{eu,na}`, validated by
`kubectl kustomize` in CI rather than only at deploy time.
- **GitHub Actions** — eight workflows under `.github/workflows/`, each one a
thin wrapper around a Make target, with path filters so a docs-only change
doesn't spin up a Kubernetes cluster.
- **`scripts/verify_supply_chain.py`** — fails the build on any mutable image
reference, which is why every manifest pins by digest.
Godot itself has **no build step and no linter** — the project runs from
source, so "the tests pass" is the only mechanical check that exists on the
GDScript side.
## What's deliberately absent
- **No C# or .NET runtime anywhere in the shipped game or server.** The
@@ -238,8 +345,11 @@ Python, no .NET, no network." Keeping the shipped game GDScript-only (no
- **The remaining Go matchmaking control-plane deployment** — independently
runnable matcher, allocator and maintenance roles backed by PostgreSQL and
Redis, deployed on provider-portable Kubernetes with Agones-managed game
fleets. The authenticated API boundary exists; durable production wiring and
provider deployment remain. The cloud provider remains deliberately
replaceable; the application stack is locked.
fleets. The durable wiring now exists end to end — queue, latency probes,
proposal, allocation, signed assignment rosters and result submission — and
is exercised by Compose and kind/Agones gates in CI. What remains is the
provider deployment itself: a registry to publish the images to, and a live
cluster. The cloud provider remains deliberately replaceable; the application
stack is locked.
This is a 1.0 launch blocker and the single largest departure from "one
Godot project, no backend". See [`MATCHMAKING.md`](MATCHMAKING.md).
+1 -1
View File
@@ -237,7 +237,7 @@ are done; everything below is what's left on the tasks still open.
| 8.8 `[D:8.7]` | Session policy (opaque tokens, digests, revocation) | Distributed revocation coordination is done by construction: sessions are durable and `PostgresSessions.Authenticate` reads the row on every authenticated request, so a revocation takes effect immediately on every replica without any cross-replica protocol, and `ApplyIdentityBan` revokes an identity's sessions in the same transaction as the ban. Live Steam/session integration remains ([#15](https://github.com/jcreek/CosmicClash/issues/15)) |
| 8.9 `[D:8.4,8.7]` | Join policy, durable reconnect leases | Live PostgreSQL/Godot process-restart and outage recovery verification remains |
| 8.10 `[D:8.5,8.31]` | Workload credential policy (signed tokens, not Kubernetes JWTs), delivery channel, conflict alerting | Never run against a real Agones cluster; alert validated only statically, not against live Prometheus/Alertmanager traffic |
| 8.12 `[D:8.11]` | Kubernetes hardening baseline, rate/quota limiting, degraded-mode gate | Private-store provisioning, distributed/global quotas, edge DDoS/WAF/origin shielding, encrypted backups, live policy/load tests remain |
| 8.12 `[D:8.11]` | Kubernetes hardening baseline, rate/quota limiting, degraded-mode gate | Private-store provisioning, distributed/global quotas, edge DDoS/WAF/origin shielding, encrypted backups, live policy/load tests remain. The workload namespace currently enforces `privileged` because Agones' Dynamic port policy injects a `hostPort` that `baseline`/`restricted` forbid; splitting game servers into their own namespace so `cosmic-clash` can enforce `restricted` again is tracked by [#33](https://github.com/jcreek/CosmicClash/issues/33) |
| 8.13 `[D:8.12]` | Digest-pinned images, supply-chain policy checker | Registry SBOM/scan/sign/admission execution and a concrete production overlay remain — the build-and-pin half is tracked by [#31](https://github.com/jcreek/CosmicClash/issues/31) |
#### 8C — Queueing, matchmaking, playlists and rating
+7 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
set -Eeuo pipefail
# Independent allocated-flow fixture for multiplayer-next.md §8.48. This
# intentionally does not call compose.phase6-smoke.yml or reuse its ports.
@@ -88,7 +88,12 @@ for attempt in $(seq 1 180); do
if "${compose[@]}" logs game-server 2>/dev/null | grep -q ' server_started '; then
break
fi
if ! "${compose[@]}" ps --status running --services | grep -qx game-server; then
# Ask whether it EXITED, not whether it is absent from the running list.
# Those differ: a container that has been created but has not started yet is
# missing from --status running too, so the previous check called a
# still-starting server dead on the first poll. It failed intermittently
# against a game server whose own logs showed a clean `server_started`.
if "${compose[@]}" ps -a --status exited --services 2>/dev/null | grep -qx game-server; then
"${compose[@]}" logs game-server >&2
echo "allocated Compose game server exited before becoming ready" >&2
exit 1