mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
docs: correct stale multiplayer/C#-backend claims, add TECH_STACK doc
CLAUDE.md and README.md described the pre-multiplayer state (local-only MVP, planned C# backend) even though server-authoritative multiplayer, the dedicated server, Docker/CI verification, and Steam transport have since shipped (Phases 1-6). Update both to reflect reality and add a docs index in CLAUDE.md pointing at multiplayer-next.md as the current checklist. - Add docs/TECH_STACK.md, linked from README, explaining the stack and why it's a single GDScript project with no separate backend. - Add one TODO item for the video settings menu (missing presets/vsync/ resolution scaling), blocked on the same profiling gate as the multiplayer 0.17 tasks. - Pick up editor-generated .gd.uid sidecars and minor project.godot formatting noise from opening the project in Godot 4.7.
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# 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-todo.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-todo.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-todo.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:** `multiplayer-todo.md`
|
||||
states plainly that "Docker/VPS is the primary v1 deployment path. Raw ENet
|
||||
self-hosting needs port forwarding, and SDR is Phase 7 — so [the ENet
|
||||
phases] ship something that works on LAN or a VPS and nowhere else." 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
|
||||
(see `multiplayer-next.md`).
|
||||
|
||||
## 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.
|
||||
|
||||
## 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**, despite
|
||||
early project framing (see `README.md`'s history) once describing a
|
||||
"C# backend" — that was never built and is not the current plan.
|
||||
- **No HTTP/WebSocket/gRPC layer** for multiplayer — ENet/Steam SDR over UDP
|
||||
only, via Godot's own `MultiplayerAPI`.
|
||||
- **No ONNX or other ML runtime in the shipped game** — see "AI opponents"
|
||||
above.
|
||||
Reference in New Issue
Block a user