Imported from PhantomMatthew/ZeroAD-Godot (
AGENTS.md). Install upstream withnpx skills add PhantomMatthew/ZeroAD-Godot. Copyright stays with the author.
AGENTS.md
Guidance for AI agents working in this repo. Every line answers "would an agent likely miss this?"
What this repo is
A Godot 4.7.2 (.NET) + C# rewrite of 0 A.D. (the open-source RTS). It is a hybrid repo with two distinct trees — knowing which one you are in is the single most important thing:
| Tree | Status | Purpose |
|---|---|---|
src/ZeroAD.Sim/ + godot/ |
Active rewrite (C#) | The new Godot game. Edit here. |
<0ad upstream> (external) |
Original 0 A.D. full source (C++20/JS), external reference | The complete original engine source tree. This is the authoritative reference for porting behavior. The path is machine-specific — see below. |
The rewrite reads the original's data verbatim (entity templates XML, art assets) and ports its behavior to C#. Treat the original tree as a reference corpus, not a build target — unless explicitly asked to work on the C++ engine.
Where to find the original C++/JS source: the upstream 0 A.D. tree is checked out somewhere outside this repo, and its path differs per OS / machine — it is NOT fixed. Known checkout locations:
OS Path Windows C:\SourceCode\0admacOS /Users/matthew/SourceCode/gitea/0adSubstitute your machine's path wherever this doc writes
<0ad upstream>. When porting a subsystem, read the reference implementation there (e.g.<0ad upstream>/source/simulation2/...).
No upstream junctions — all assets live in this repo (since 2026-09-12)
The repo previously offered optional binaries/ etc. symlinks/junctions into <0ad upstream>. They are gone and must not be recreated — everything the game, tests, and tooling need is read from inside the repo:
- Game runtime:
RuntimePaths.FindBinariesRootprobes the in-repo staged rootgodot/export/data(generated once bygodot/tools/stage_release_data.sh, 1.4G, gitignored). This is the same tree the release package ships. dotnet testfixtures:src/ZeroAD.Sim.Tests/RepoPaths.csmaps every"binaries/..."relative path togodot/export/...first (walk-up from the test assembly). No test may resolve data through an upstream link.- Asset pipeline (
godot/tools/run_full_pipeline.shetc.): takes the upstream art root as an explicit argument (or$ZEROAD_UPSTREAM) at conversion time; converted output lives undergodot/assets/. The pipeline never reads through repo-root links. - Browsing C++/JS reference code: open
<0ad upstream>directly (external path above).
On a fresh clone, run the staging script once (explicit upstream path, one-time) and everything works link-free:
sh godot/tools/stage_release_data.sh /path/to/0ad/binaries/data # 一次,产出 godot/export/data(1.4G,gitignored)
Master plan: godot-rewrite-plan.md (modules M0–M10, milestones MS1–MS7, risk matrix). Read it before any non-trivial rewrite work.
System deep-dive notes: claude-analyze/*.md (15 docs analyzing the original engine: ECS, network lockstep, pathfinding, rendering, audio, UnitAI, etc.). The fastest way to understand a subsystem you're porting.
Architecture of the rewrite
godot/ Godot presentation layer (non-deterministic, float OK)
Scripts/SimBridge.cs ← the ONLY seam between Godot and the sim kernel
│ references
src/ZeroAD.Sim/ Deterministic simulation kernel (pure C#, zero Godot deps)
- Entry flow:
godot/project.godot→Scenes/Main.tscn→Scripts/Main.cs(Node3D, namespaceZeroAD.Godot). SimBridgeowns theComponentManager/TurnManagerand drives fixed-step ticks (≈10 Hz) from the Godot side. All sim state crosses this bridge.- Determinism is a hard constraint. The kernel must stay Godot-free so it can run headless and be cross-platform hash-checked in CI. Never import
Godot.*intosrc/ZeroAD.Sim/.
Namespaces
ZeroAD.Sim.*— kernel:Components,Content,Events,Maths,Net,Templates,Triggers,Tutorial,Serialization.ZeroAD.Godot— presentation (all ofgodot/Scripts/).ZeroAD.Godot.Tools— editor-side asset tooling (godot/tools/).
Determinism rules (kernel only)
- Fixed-point math only — use
src/ZeroAD.Sim/Maths/Fixed.cs(CFixed_15_16, ported fromsource/maths/Fixed.h). Nofloat/doublein sim logic. - PRNG is
Rand48(deterministic); never useSystem.RandomorDateTime-seeded RNG in the kernel. ZeroAD.Sim.csprojsets<ServerGarbageCollection>false</ServerGarbageCollection>and<AllowUnsafeBlocks>true</AllowUnsafeBlocks>deliberately — do not change these casually.
Build / test / run commands
There is no .sln for the kernel/tests — build each C# project by its .csproj path. The one exception is godot/GodotProject.sln: Godot's C# export plugin (ProjectContainsDotNet()) requires a .sln next to GodotProject.csproj, otherwise --export-release silently ships a PCK with no .NET assemblies and the app segfaults at launch. Keep it; it's generated minimal (dotnet new sln + the one csproj).
# Deterministic kernel (headless, no Godot needed)
dotnet build src/ZeroAD.Sim/ZeroAD.Sim.csproj
dotnet test src/ZeroAD.Sim.Tests/ZeroAD.Sim.Tests.csproj # xUnit; DeterminismTests, FixedTests, VectorTests, ParamNodeTests
# Run a single test class:
dotnet test src/ZeroAD.Sim.Tests/ZeroAD.Sim.Tests.csproj --filter "FullyQualifiedName~DeterminismTests"
# Godot presentation (requires Godot .NET SDK environment)
dotnet build godot/GodotProject.csproj
# Run the game: open godot/project.godot in Godot 4.7.2 (.NET build) and press Play.
Warnings are errors (both C# projects)
ZeroAD.Sim.csproj and ZeroAD.Sim.Tests.csproj both set <TreatWarningsAsErrors>true</TreatWarningsAsErrors>. GodotProject.csproj has <Nullable>enable</Nullable>. Do not silence warnings — fix them. No as-casts-to-dynamic, no pragma suppressions to sneak past this.
Lint / format (these target the ORIGINAL 0 A.D. code, not the C# rewrite)
npm run lint # eslint — lints JS in <0ad upstream>/binaries/data/mods/ (Allman braces, tabs)
npm run lint:fix
ruff check # Python — line-length 99, py311, select=ALL (see ruff.toml)
ruff format
The eslint.config.mjs and ruff.toml at root govern the original tree's JS/Python (mod scripts, tooling like source/tools/entity/checkrefs.py). They do not apply to src/ or godot/ C# code.
Asset conversion pipeline (original → Godot)
Asset placement policy: ALL assets must live under the godot/ tree (godot/assets/, godot/data/, or the staged release dir godot/export/data/). Runtime never reads from the upstream tree — RuntimePaths resolves the in-repo staged root godot/export/data (dev = that same staged root; release = data dir beside the executable). New or converted assets go to godot/, never read from the upstream tree at runtime.
Run from inside godot/:
sh tools/run_full_pipeline.sh <0ad upstream>/binaries/data/mods/public/art
Requires Blender 4.2 LTS (set path via $BLENDER env var, or the script auto-detects default install locations for macOS/Windows) and the original 0 A.D. art, passed as an explicit path argument (or $ZEROAD_UPSTREAM) — there are no repo-root links into upstream. Converts DAE→GLB meshes and copies/converts textures (PNG, DDS→PNG via Blender) into godot/assets/. Per-category/single-asset conversion: godot/tools/convert_dae_to_gltf.py, convert_all_assets.py, build_animated_unit.py. These are Python — ruff governs them.
Where to find reference implementations
When porting a subsystem, start from the original code. The original C++/JS source lives at <0ad upstream> (external to this repo; source/ in-repo is gitignored). Paths below are relative to that root:
| You're working on | Look at |
|---|---|
| Fixed-point math, vectors, trig | source/maths/Fixed*.h, source/maths/... |
| ECS: components, messages, entity lifecycle | source/simulation2/system/ (C++) and binaries/data/mods/public/simulation/components/*.js (JS behavior — UnitAI.js is ~6000 lines, the hardest port) |
| Turn manager / lockstep / netcode | source/simulation2/system/TurnManager*, source/network/ |
| Pathfinding (hierarchical + vertex) | source/simulation2/helpers/HierarchicalPathfinder*, Pathfinding.h, VertexPathfinder* |
| Entity template loading (XML inheritance/merge) | source/simulation2/system/ParamNode |
| Serialization + state hashing (OOS detection) | source/simulation2/serialization/ |
| Rendering / map loading / actors | source/renderer/, source/graphics/ |
| PMP map file format | source/graphics/MapIO.h (FILE_VERSION), source/graphics/MapWriter.cpp (header layout), source/graphics/MapReader.cpp |
| Reference integrity checker | source/tools/entity/checkrefs.py |
Example: to look up the PMP header format, read <0ad upstream>/source/graphics/MapIO.h and MapWriter.cpp.
Entity templates (data, consumed as-is by the rewrite): the C# rewrite and the tests read them from the in-repo staged root godot/export/data/mods/public/simulation/templates/*.xml (see "No upstream junctions" above); the upstream original lives at <0ad upstream>/binaries/data/mods/public/simulation/templates/.
Conventions that differ from defaults
- Godot version is pinned to 4.7.2 (
Godot.NET.Sdk/4.7.2,project.godotfeatures4.7). Don't assume 4.x-generic APIs. Stay on the 4.7.x maintenance line; don't jump to 4.8 without an explicit decision. - Target framework is net8.0 across all three C# projects.
ImplicitUsingsdisabled in the kernel — write explicitusingdirectives.- C# style follows
.claude/rules/csharp/(nullable on,recordfor value models,async/awaitwithCancellationToken). Rundotnet formatif available. - Allman braces + tabs are the original JS convention; the new C# may differ — match the surrounding
ZeroAD.*file, not the JS. - Art assets are large; original 0 A.D. uses git-lfs upstream. If a model file looks like text or a pointer, the LFS pull is incomplete.
Things to verify before claiming done
Compile gate (mandatory after EVERY edit, not just before commit):
- Run
dotnet buildon every project you touched and confirm0 Error(s):dotnet build src/ZeroAD.Sim/ZeroAD.Sim.csprojfor kernel changes,dotnet build godot/GodotProject.csprojfor anything undergodot/.
- Check the error/warning counts in the summary lines (
0 Warning(s) 0 Error(s)), never just the last lines of output — an error scrolled out of a shorttailstill fails the build. Grep forerrorexplicitly when in doubt:dotnet build ... 2>&1 | grep -E "error|Error\(s\)". - Do this after each edit batch, before moving on. Do not assume a small edit compiles; do not commit or push a tree that doesn't build.
dotnet test src/ZeroAD.Sim.Tests/ZeroAD.Sim.Tests.csprojpasses —DeterminismTestsis the canary for cross-platform hash stability.- Any change touching
src/ZeroAD.Sim/must not introducefloat/doubleorGodot.*references into the kernel.
Asset surgery gate (mandatory before ANY batch edit under godot/assets/)
godot/assets/ is largely a gitignored build product — except meshes/**/*.glb and
animations/**/*.glb (+ their .import sidecars), which ARE tracked in git (608MB, committed
2026-09-06 so a fresh clone gets models without running the pipeline). Textures/UI stay
untracked. For tracked GLBs git history gives rollback; for everything else it gives NO rollback. A batch
edit here is an irreversible operation on 5000+ files. The 2026-08-21 regression chain
(occluders → missing heads → dead animations → giant weapons, each from a批量 scale script
applied on an unverified rule) came from skipping this gate. Before touching assets:
- Snapshot first, always — one command, before the edit, no exceptions:
(rsync -a godot/assets "../asset_backups/assets_$(date +%Y%m%d_%H%M%S)/"asset_backups/at repo root is gitignored; keep it outsidegodot/so Godot never imports it.) - Rollback is then trivial:
rsync -a --delete <backup-dir>/ godot/assets/, thencd godot && /Applications/Godot_mono.app/Contents/MacOS/Godot --headless --path . --import(or the Blender-equivalent import step on other machines). - Never apply a blanket rule to many files without measuring first. Size/scale edits must
use the full span (
accessor.max − accessor.minper axis, × node scale) — checking onlymaxmisses negative extents and under-reports long models by 2-3× (the giant-spear bug). - Ground truth before scaling: the DAE corpus is MIXED — some files honor their
<unit meter>declaration, others are raw meters with a lying unit tag. There is no single conversion rule; verify per-class with an in-game measurement (runtime prop-scale dump or screenshot) before batch-applying. When in doubt, clamp by span class, don't "normalize". - Repair scripts go in
godot/tools/(fix_glb_unit_node_scale*.py,fix_glb_weapon_span.py,restore_glb_from_import_cache.gd,convert_dae_to_gltf_pycollada.py) — future rebuilds must be reproducible from committed tools, not from memory. Note: local Blender builds may have no Collada importer (checkbpy.ops.import_scene.colladaexists before planning a Blender-based reconvert; the pycollada converter + import-cache restorer are the fallback pair).