mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
Compare commits
5 Commits
aac00f148e
...
fe453ab607
| Author | SHA1 | Date | |
|---|---|---|---|
| fe453ab607 | |||
| 52cc478b38 | |||
| 4ea72be581 | |||
| 4560d2de8a | |||
| 4912837dd7 |
@@ -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 1–6). 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 1–6). 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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user