Echelon is the difficulty curve
The scale you command determines how much of the command problem exists at all
Commanding a fireteam has no C2 problem — you are the unit, you see what it sees, and orders are instant. By battalion, subordinates are out of sight and reports mediate what you know. At corps and above, you work from reports already stale and issue intent rather than instructions.
| Commanding | What command feels like |
|---|---|
| Fireteam / squad | No C2 problem — you are the unit. You see what it sees; orders are instant. |
| Platoon / company | Subordinates exist but are close. Orders are near-immediate. |
| Battalion | Subordinates out of sight. Reports mediate what you know. Delay begins to bite. |
| Brigade / division | You command through subordinate headquarters. You cannot see the front. |
| Corps / army | You work from reports already stale. You issue intent, not instructions. |
| Theater | Coalition. Some formations you cannot order at all, only ask. |
Communications degradation, order propagation delay and intelligence fusion aren't features bolted onto a finished game — they're what makes each echelon feel different rather than merely bigger. Introduced all at once at squad level they would be noise; unlocked as the player climbs, they're the learning curve. The consequence for the code: these effects are parameterised by echelon, not toggled — order delay is a function of echelon, distance and terrain that returns approximately zero at squad level and grows from there, one code path from tutorial to theatre rather than a mode switch.
Factory builds the base; decorators append layers
The symbol system
NatoSymbolComposer.Compose is the entry point and fixes the order — later
stages read what earlier ones wrote:
SIDCParser → SIDCCode
└─ SymbolFactory.CreateBase frame + fill
└─ IconDecorator entity icon + entity-type variant mark
└─ SectorModifierDecorator sector 1 (upper) / sector 2 (lower)
└─ AmplifierDecorator echelon, HQ staff, TF bracket, feint
└─ ConditionDecorator condition bar + combat-power bar
└─ TextAmplifierDecorator fields T, M, F
→ NatoSymbolBaker.Bake → one Sprite
INatoSymbol is data only — an ordered list of draw layers plus text
amplifiers. Nothing rasterises until Bake runs, once, at the end.
The map system mirrors it
Same shape: an ordered pipeline over a data-only container, and nothing rasterises until
the renderer runs. MapGenerator.Generate hard-codes its stage order for the
same reason Compose does:
MapGenerationSettings → ReliefParameters
└─ TectonicStage base landform, sea level
└─ (authored relief hook)
└─ ErosionStage weathers the surface
└─ HydrologyStage fill, D8 routing, lakes, rivers, moisture
└─ LandcoverStage classification from slope + moisture
└─ SettlementStage towns, stamped into landcover
└─ NetworkStage roads between them, bridges and fords
└─ (authored features hook)
MapData is data only. MapRasterizer.RenderPixels is the only
thing that draws, and its layer order is fixed too — ground, contours, areas, lines,
point marks, labels, grid. Labels come after every mark because placement can only avoid
collisions it can see; the grid comes last because a grid line that gives way to a road
has stopped being a coordinate reference.
The two systems share one primitive library. Symbols bake into a square buffer, maps
into a rectangular one, so every drawing primitive has a (w, h) overload
with the square one forwarding to it — one place to add a new shape, not two.
Orders and reports are messages, not calls
Why
A direct call from the UI into a unit is the obvious implementation and the wrong one: drawing in-progress orders, replaying a saved battle, shipping orders over a network for multiplayer, and an AI issuing orders through the same path a player does all need to observe or inject orders. Each is painful to retrofit onto direct calls and nearly free if an order is a message on a topic. Two topics, matching the direction of real command flow:
┌─────────────────────┐
player / AI ────►│ COMMAND topic │────► units (filter by addressee)
│ (orders down) │────► UI (read-only)
└─────────────────────┘────► recorder (read-only)
┌─────────────────────┐
units ──────────►│ SITUATION topic │────► UI (read-only)
│ (reports up) │────► other units (reaction)
└─────────────────────┘────► intel system (planned)
Four delivery rules, kept because determinism depends on them
- Publish to the next step. A message published during step N is delivered at step N+1, never within the step that produced it — otherwise a report can trigger a command that triggers another report, unbounded, inside one step. The same mechanism is the degenerate case of order propagation delay: a uniform one-step delay today, a function of echelon/distance/terrain later, one mechanism throughout.
- Dispatch synchronously, in a defined order. Not async, not thread-pooled. Determinism requires a single stable order, every time.
- Only the owner mutates. Any subscriber may observe; only a unit may modify its own queue, and only the executor may change world state. Otherwise dispatch order becomes semantics, and the resulting bugs reproduce at only one interleaving.
- Subscribe for events, read for state. If a subscriber has to remember what it heard to stay correct, it should be reading state instead — the live plan in PLAY draws by walking each unit's actual queue every frame, not by reconstructing a shadow copy from messages it happened to see.
The payoff: the whole simulation is deterministic and replayable from the command log alone. Two different machines, or the same machine twice, produce the identical run from the identical order stream.
Generated at runtime, verified as a picture
Nothing visual is an imported file
Every sprite, texture and glyph — symbols, map sheets, UI chrome, the aged-paper stock the field manual is drawn on — is generated procedurally at runtime, not authored in an image editor and imported. It's a deliberate property, not an accident of what got built first: it means any symbol/echelon/condition combination or any map at any seed exists without an artist drawing it, and it's why introducing the project's first real imported binary asset (audio) gets treated as a threshold crossed deliberately, not a formality.
A generator's output is a picture, so read the picture
This project verifies changes by building the player, capturing a screenshot, and reading it — not by reading the code and assuming it's correct. Several real bugs (a symbol rendering at a barely-visible alpha, an objective one cell inside a lake) were invisible to code review and obvious in one frame. The same discipline applies to generators directly: contact sheets bake every combination of a system — every symbol permutation, every relief profile against every render mode — to one image, checked before the numbers next to it, because a generator whose output no one looked at is a generator no one has actually verified.
This page summarises; the source documents are the reference — CLAUDE.md for the map/symbol pipelines and the project's verification discipline, docs/command-architecture.md for the full messaging design.