# 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). ## 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. ## 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. ## 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 authenticated API boundary exists; durable production wiring and provider deployment remain. 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).