# Tech stack What this project is built with, and why each piece was chosen, sourced from the project's own docs and code comments. Where the reasoning for a choice isn't recorded anywhere, this doc says so rather than guessing. ## Engine: Godot 4.7 The whole game — client and dedicated server alike — is one Godot 4.7 project, GDScript only. There is no C#, ONNX, or .NET code involved at runtime anywhere in the shipped product. **Why Godot, specifically:** legal, not technical. Per `README.md`'s "Legality" section, the concept of "vehicle soccer" cannot be copyrighted, but Rocket League's specific expression of it can be. Building on Unreal or Unity — the engines Psyonix and most Rocket-League-likes use — would invite comparison to that specific expression. Using a different engine (Godot) and different vehicles (space ships instead of cars) is a deliberate part of keeping the project's own expression original and legally distinct. ## Physics: Jolt Physics Set via `Game/project.godot`'s `3d/physics_engine="Jolt Physics"` — Godot 4's alternative physics backend, not the engine's own default (`GodotPhysics3D`). All ship and ball movement is force/torque-based (`_integrate_forces`), never kinematic. **Why Jolt over Godot's default physics:** not written down anywhere in the project's own docs, but per the project owner, the goal was a physics engine whose behavior isn't tied to Godot's own release cycle — so upgrading to a future major Godot version doesn't silently change how the game feels, the way an engine-version upgrade has repeatedly worried Rocket League's own playerbase (players have specifically flagged that Unreal Engine upgrades risk changing timestep/continuous-collision behavior enough to break muscle memory built over thousands of hours). This holds up under scrutiny. `GodotPhysics3D` (Godot's built-in default) is an internal engine subsystem, versioned and rewritten alongside Godot itself, and has a real history of behavior changing across Godot releases — for example a kinematic-body regression introduced between 4.3-dev4 and 4.3-dev5, and collision-detection differences reported across the 4.0 line. Jolt, by contrast, is developed as an independent upstream library ([jrouwe/JoltPhysics](https://github.com/jrouwe/JoltPhysics)) with its own semantic versioning and a user base beyond just Godot, so its collision behavior changes on its own release cadence rather than as a side effect of unrelated Godot core work. It's a real reduction in coupling, not a complete guarantee: Godot still pins (and can bump) a specific Jolt version per release, and Godot's own Jolt *integration layer* can itself introduce differences — e.g. a sleeping `RigidBody3D` wakes differently under Jolt than under `GodotPhysics3D` when another body approaches it. Separately documented, and a real consequence either physics backend would share: **Jolt is not bit-deterministic** across platforms or even across differing contact orderings on the same platform, and Godot exposes no world snapshot/restore API. That fact is why the multiplayer architecture is server-authoritative with client-side prediction of only the local ship, rather than rollback/resimulation netcode — rollback would require deterministic replay, which no physics engine choice here provides (`MULTIPLAYER_SPEC.md` §1, decision 1). ## Multiplayer transport: Godot's built-in `MultiplayerAPI` over ENet The default and fully-supported transport is `ENetMultiplayerPeer` — Godot's built-in high-level multiplayer networking, direct-IP over UDP, port 7777 by default. A thin `NetTransport` abstraction (`Game/scripts/net_transport.gd`) exists specifically so a second transport (Steam) can be swapped in without touching the rest of the networking code. Design choices layered on top of the built-in peer, and why: - **`ENetMultiplayerPeer.server_relay` is forced to `false`.** It defaults to `true`, which lets any client `rpc()` any other client *through the server* — incompatible with a server-authoritative model. Called out in `MULTIPLAYER_SPEC.md` §2.1 as "the single highest-value one-line security change in the document." - **Manual multiplayer polling**, not Godot's automatic idle-frame poll. `NetworkManager` calls `set_multiplayer_poll_enabled(false)` because the automatic poll runs on the idle frame, which would tax every RPC issued from `_physics_process` up to a full frame in each direction — unacceptable for a physics-tick-rate game. - **Server-authoritative simulation with client-side prediction of the local ship and ball only; no world rollback.** Direct consequence of Jolt's non-determinism (see above). - **A custom binary wire format** (`net_codec.gd`) rather than raw RPC argument marshalling, for compact, quantised input/snapshot packets sent at high frequency — no stated alternative was considered in the docs, but the packet-size/channel-intent design in `MULTIPLAYER_SPEC.md` §2 is extensive and deliberate. ## Optional multiplayer transport: Steam (GodotSteam) `Game/scripts/steam_transport.gd` implements the same `NetTransport` interface using `SteamMultiplayerPeer` over Steam's SDR (Steam Datagram Relay), from a custom GodotSteam-patched Godot build (not stock Godot — `STEAM.md`). It is entirely optional: the default build and every CI check use ENet only, and a build without the `steam` feature is fully functional without it. **Why it's optional and why raw ENet remains primary:** per `multiplayer-next.md`, Docker/VPS is the primary v1 deployment path, and raw ENet self-hosting needs port forwarding while SDR is Phase 7 — so the ENet phases ship something that works on LAN or a VPS today, and nowhere else yet. Steam/SDR is being added later specifically to remove the 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 Godot project, exported headless (`res://scenes/server_boot.tscn`) via Godot's own `--export-release "Linux Dedicated Server"` preset. Two deployment paths are documented (`SERVER.md`): - **Docker**, the primary path: a multi-stage `Dockerfile` builds the export inside a pinned `barichello/godot-ci:4.7.1` image and produces a slim `ubuntu:24.04` runtime image. `make verify-phase6` builds it, runs it, joins two independent client processes to it, and asserts on match/goal/ arena-rotation behaviour — this is also the entire Phase 6 GitHub Actions workflow. - **Native systemd**, for a VPS: copy the exported binary to `/opt/cosmic-clash`, run it as a dedicated `cosmicclash` service user via `deploy/cosmic-clash-server.service`. Per the project owner, Docker was chosen as the primary path for three reasons: it gives a pinned, reproducible build environment that behaves identically across local development, CI, and hosted production servers (rather than three separately-drifting setups); it's portable across hosting providers instead of assuming a specific Linux distro/init system the way the systemd unit does; and it's the tooling the team is already most familiar with. That matches what's independently visible in the repo — `SERVER.md` documents Docker as the one path CI actually exercises (`make verify-phase6`), while the systemd unit is native-deployment documentation only, with no automated verification of its own. ## Matchmaking control plane: Go, PostgreSQL, Redis, Agones The one part of the project that is *not* the Godot project. `server/` is a Go module (~13k lines of non-test code across `matcher`, `allocator`, `api`, `store`, `security`, `supervisor`, `agones`, `migrations`, `observability`) implementing the casual/ranked queue design in [`MATCHMAKING.md`](MATCHMAKING.md), plus a small PID-1 supervisor that exists because Godot/GDScript cannot intercept `SIGTERM` and Agones needs a graceful drain signal to land somewhere. **Why Go, and why "performance" is the wrong reason to give:** the control plane is not in the simulation hot path. Physics, snapshots and 60 Hz input all live in the Godot dedicated server over ENet/SDR (see the transport sections above); Go never touches a game packet. Its actual workload is many mostly-idle WebSocket connections, a matcher loop that runs on a sub-second tick, and I/O against PostgreSQL, Redis and the Kubernetes API. That is I/O- and concurrency-bound, not CPU-bound, so the raw single-thread speed a systems language would buy is spent on work this service doesn't do. What actually drove the choice: - **Agones and Kubernetes are Go-native.** Allocation, the GameServer SDK and the k8s client are all first-party Go. Any other language means hand-rolling REST against the Agones allocation service — see `server/agones/`, which uses those clients directly. - **Goroutines plus `context` are the right shape for the problem** — many concurrent idle connections, a few periodic loops, and cancel-everything-on- shutdown semantics that the PID-1 supervisor depends on. - **The surrounding operational ecosystem is Go** — Prometheus instrumentation (`server/observability/`), structured logging, migrations, and the provider-portable deployment tooling. - **Static binaries and slim containers**, which matters for the supervisor and for keeping the allocated game-server image close to the existing one. **Alternatives, honestly weighed:** Rust or C++ would be the correct answer for a custom UDP relay or the simulation server itself, and buy nothing measurable for a queue-and-allocate service — while costing significantly in iteration speed. C# is the only serious contender (ASP.NET Core is fast, its async model is excellent, and Postgres/Redis/WebSocket support is mature); it loses on the Agones/Kubernetes side, where the clients are community-maintained rather than first-party, and on container weight. TypeScript or Python would prototype faster but fit poorly for a service whose failure modes are almost entirely races and timeouts. None of those gaps is large enough to justify rewriting the Go that already exists. ## AI opponents: reinforcement learning, trained out-of-process, run in pure GDScript Two entirely separate pieces, deliberately joined only at a JSON file: - **Training** (Python, not shipped): [Godot RL Agents](https://github.com/edbeeching/godot_rl_agents) (`godot-rl==0.8.2`, vendored bridge plugin at `Game/addons/godot_rl_agents`, MIT-licensed) drives self-play PPO via **Stable-Baselines3** (`==2.4.0`) over **PyTorch** (`==2.13.0`) and **Gymnasium** (`==1.0.0`), against a headless instance of the actual game (`training/train.py` launches real parallel `godot --headless` processes from source — the training environment *is* the game, not a reimplementation of its physics). `training/requirements.txt` pins these versions strictly, because the training code (`ship_action_codec.gd`, `train.py`) depends on specific library-internal behaviour (godot_rl's discrete-action-space branch, SB3's logit layout, Gymnasium's dict-key sorting) that an unpinned upgrade could silently change mid-curriculum. - **In-game inference** (`Game/scripts/policy_network.gd`): the trained checkpoint is exported to a small JSON file (`training/export_policy.py`) and evaluated at runtime by a hand-written, dependency-free GDScript MLP. `Game/addons/godot_rl_agents/VENDORED.md` notes the plugin's ONNX/C# files are present upstream but unused here — they require the .NET Godot build, which this project does not use. **Why this split, rather than shipping ONNX/.NET inference:** stated directly in `TRAINING.md` — "the trained policy is exported to a small JSON 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 Godot editor/runtime instance for AI-assisted development; not part of the game. - **`mcp/blender-mcp`** (git submodule, Python/`uv`) — drives a live Blender instance for generating original 3D assets (ships, arenas), for the same originality reasons covered under "Why Godot" above. - **`tools/blender/`, `tools/textures/`** — standalone Python scripts (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 "C# backend" an early version of `README.md` described was never built — that wording is long gone from the README itself. A backend service *does* now exist for matchmaking, but it is Go, not C# — that framing predates every real decision here. See "Matchmaking control plane" above for why Go was chosen over C# and over Rust/C++. - **No HTTP/WebSocket/gRPC layer for simulation traffic** — the live game uses ENet/Steam SDR over UDP via Godot's own `MultiplayerAPI`. The matchmaking control plane now has an authenticated Go REST/WebSocket boundary for queue, proposal, assignment and recovery traffic; simulation remains on ENet/SDR. - **No ONNX or other ML runtime in the shipped game** — see "AI opponents" above. ## Planned, not yet built - **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 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).