Owner-reported polish on top of #48, plus a legacy-parser gap that prevented verifying stationed ship groups against a real .REP fixture. UI: - Production: drop the empty `(production)` placeholder option. Owned planets always produce something, so the primary select now opens on `industry` by default when `planet.production` is null/unknown, keeping the row inside the four real production kinds at all times. - Production: lock the row to a single line (no flex-wrap) and strip border + padding from the ✓/✗ buttons so the apply/cancel icons read as glyphs and the row no longer breaks into two visual rows for Research / Ship contexts where both selects are present. - Cargo routes: the placeholder option is now an `<option disabled>` styled like a section header (greyed, italic) and reads "manage routes" instead of "cargo routes". The wording shifts the intent from a section label to an action prompt. Legacy parser: - F8-05 (#48 п.32) "Stationed ship groups" couldn't be verified against the dg fixture because the legacy `<Race> Groups` blocks (outside battles) and the `Unidentified Groups` block were dropped by the parser — both are now wired up. Foreign group rows parse the `# T D W S C T Q D P M` columns and resolve the destination against the parsed planet tables (rows with an invisible destination drop, matching the existing local-group convention). The legacy row carries no origin / range columns, so foreign groups surface as stationed at the destination. - Smoke tests on every fixture extended with `otherGroups` and `unidentifiedGroups` counts. New focused unit test `TestParseOtherAndUnidentifiedGroups` covers the column layout, the drop-on-unknown-destination rule, and the `X Y`-only unidentified rows. - `tools/local-dev/reports/dg/KNNTS039.json` and `tools/local-dev/reports/dg/KNNTS041.json` regenerated so the synthetic-loader fixtures carry the new arrays. - README updated: the two sections move out of "Skipped sections" into a "Foreign and unidentified groups" block; package doc-comment reflects the broader scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
9.8 KiB
legacy-report-to-json
Converts legacy text-format Galaxy turn reports (the dg and gplus
engines that lived under tools/local-dev/reports/) into a JSON
envelope around pkg/model/report.Report
plus full BattleReports (Phase 27).
Output envelope
{
"version": 1,
"report": { /* report.Report */ },
"battles": { "<uuid>": { /* report.BattleReport */ }, ... }
}
version: 1 lets the UI distinguish a current-format envelope from a
bare Report JSON. The synthetic-report loader accepts both — pre-
envelope synthetic JSON files still load, just without battle
fixtures. battles is omitted when the legacy file has no combat
events.
The output is consumed by the DEV-only synthetic-report loader on
the UI client's lobby (import.meta.env.DEV). With it, the map view,
inspectors, and order-overlay can be exercised against rich game
states without playing many turns end-to-end against a real backend.
The tool is part of the synthetic-report parity rule documented in
ui/PLAN.md.
Build / run
# from the repo root, with the Go workspace active
go run ./tools/local-dev/legacy-report/cmd/legacy-report-to-json \
--in tools/local-dev/reports/dg/KNNTS041.REP \
--out tools/local-dev/reports/dg/KNNTS041.json
--in reads - as stdin; --out defaults to stdout when empty or
-. The tool exits non-zero on any I/O or parse failure.
Supported input variants
| Variant | Sample dir | Status |
|---|---|---|
| dg | tools/local-dev/reports/dg/*.REP |
First-class |
| gplus | tools/local-dev/reports/gplus/*.REP |
First-class |
| ng | tools/local-dev/reports/ng/*.rep |
Not supported |
| lucky | tools/local-dev/reports/lucky/*.rep |
Not supported |
dg uses CRLF line endings, gplus uses LF and tabs in section indentation;
both are space-aligned tabular inside data blocks. The parser splits on
runs of whitespace (strings.Fields) so the same code handles both.
Pseudo-Cyrillic glyphs (MbI, KAMA3, 9IMA) appear in some races
and ship class names but are stored as plain ASCII letter substitutions
— no encoding conversion is needed.
In-scope fields (current)
The parser only fills the subset of report.Report that the UI client
already decodes from server responses
(ui/frontend/src/api/game-state.ts → decodeReport):
report.Report field |
Source section in legacy file |
|---|---|
Race |
<Race> Report for Galaxy ... line |
Turn |
same |
Width, Height |
Size: N (square galaxies) |
PlanetCount |
Planets: N |
VoteFor, Votes |
Your vote: block |
Player[] |
Status of Players (total ...) |
LocalPlanet[] |
Your Planets |
OtherPlanet[] |
<Race> Planets (one per race) |
UninhabitedPlanet[] |
Uninhabited Planets |
UnidentifiedPlanet[] |
Unidentified Planets |
LocalShipClass[] |
Your Ship Types |
OtherShipClass[] |
<Race> Ship Types (Phase 23) |
LocalScience[] |
Your Sciences (Phase 23) |
OtherScience[] |
<Race> Sciences (Phase 23) |
Bombing[] |
Bombings (Phase 23) |
ShipProduction[] |
Ships In Production (Phase 23) |
LocalGroup[] |
Your Groups (Phase 19) |
LocalFleet[] |
Your Fleets (Phase 19) |
IncomingGroup[] |
Incoming Groups (Phase 19) |
Battle[] (summary) |
Battle at (#N) Name headers + Battle Protocol (Phase 27 follow-up) |
The envelope's battles map carries the full BattleReport-s parsed
out of the same blocks: every roster row turns into a
BattleReportGroup (Number/Tech/LoadType/LoadQuantity/
NumberLeft/InBattle), every ... fires on ... : Destroyed|Shields
line turns into a BattleActionReport. UUIDs are synthesised
deterministically — syntheticBattleID(idx) for the battle
identifier (per-report 0-based index, SHA1 namespace
be01a000-0000-0000-0000-000000000002) and
syntheticBattleRaceID(name) for BattleReport.Races entries (SHA1
namespace be01a000-0000-0000-0000-000000000003). Re-running the
converter on the same input file yields byte-identical JSON, so
synthetic-mode UI URLs (/games/synthetic-…/battle/<uuid>?turn=N)
stay stable across regenerations.
Players whose name in the legacy file ends with _RIP are emitted with
the suffix stripped and Extinct: true.
LocalGroup.ID is synthesised deterministically from the per-report
group index via uuid.NewSHA1, so re-running the converter on the same
input file yields byte-identical JSON.
LocalGroup.Speed is left at zero — the legacy "Your Groups" table does
not expose ship speed; the UI can derive it from pkg/calc.Speed if
ever required.
Origin / Range names that don't resolve against the parsed planet
tables (foreign-only knowledge the local player lacks) cause the entire
group / fleet / incoming row to be dropped — preferable to fabricating
a destination.
ShipProduction.ProdUsed is derived from the on-disk Percent and the
producing planet's material/resources via [pkg/calc.ShipBuildCost]
(the same helper the engine's controller.ProduceShip uses). The
legacy text format does not carry a prod_used column directly; the
derivation gives the cumulative production-equivalent of the build
progress so far. The real engine's ProdUsed is the per-turn
residual production poured into the partial ship, which is not
recoverable from a single legacy snapshot. The two numbers stay in
the same units and the same ballpark, which is good enough for the
synthetic-mode UI — live engine reports come over the FBS wire and
do not flow through this parser. A ships-in-production row pointing
at a planet that did not appear in Your Planets (which would be a
malformed legacy file) is dropped.
Foreign and unidentified groups
The legacy text format does carry top-level <Race> Groups blocks
and a single Unidentified Groups block, both outside the battle
rosters — earlier parser revisions silently dropped them. F8-05
wires them up:
OtherGroup[]— every<Race> Groupssection outside aBattle atblock contributes one entry per row. The legacy row is# T D W S C T Q D P M(count, class, drive/weapons/shields/ cargo tech, cargo type, load, destination, power=drive·20 — not retained, mass). The destination resolves against the parsed planet tables (Your Planets,<Race> Planets,Uninhabited Planets); rows whose destination is invisible to the local player are dropped — preferable to fabricating a number. The legacy row carries no origin / range columns, so foreign groups surface as stationed at the destination (origin / range nil).UnidentifiedGroup[]— theUnidentified Groupssection carriesX Yfloats only. Each row maps directly ontoUnidentifiedGroup{X, Y}; no planet resolution needed.
Skipped sections (today)
These exist in legacy reports but cannot be derived from the legacy text format at all. Each could become in-scope if a strong enough reason arises (see "Adding a new field" below).
- Cargo routes — no dedicated section in the legacy text format; the
synthetic JSON emits
route: []. The UI's overlay path (applyOrderOverlay) supports running on top of an emptyroutes.
Adding a new field
ui/PLAN.md carries a global rule: every UI phase that extends
decodeReport to read a new report.Report field also extends this
parser, in the same PR, to populate it from legacy text — or, if the
field cannot be derived, adds an entry to the Skipped sections
list above with a one-line explanation.
The Go side of the rule is enforced mechanically: this tool imports
galaxy/model/report, so any backwards-incompatible change to the
schema breaks the tool's compilation before the change ships.
When extending:
- Identify the legacy section in
tools/local-dev/reports/dg/*.REP(andgplus/*.REP) that carries the field, usinggame/rules.txtsection "Отчет о результатах хода" as the column-layout reference. - Add a section to the state machine in
parser.go(classifySection, thesectionconstants, theparse*methods). - Cover the new section with a unit test in
parser_test.go(inline minimal fixture) and update the smoke counts inTestParseDgKNNTS039/TestParseGplus40so a future regression that drops the section is caught. - Run
go test ./tools/local-dev/legacy-report/..., then re-run the CLI ondg/KNNTS039.REPandgplus/40.REPand visually skim the JSON — the field should appear with sensible values.
Tests
go test ./tools/local-dev/legacy-report/...
Inline fixtures exercise the per-section row parsers; smoke tests
parse the real fixtures under tools/local-dev/reports/dg/ and
tools/local-dev/reports/gplus/ and assert top-level counts. The
current smoke set spans:
- dg/KNNTS039–041 — KnightErrants saga;
041is the only one withIncoming Groups, exercising deferred name resolution. - dg/Killer031 — Killer engine variant with two
Your Fleetsentries (Fl1,F2). - dg/Tancordia037 — the richest fixture: 311 local groups in 30 fleets, two incoming groups, "Incoming Groups" landing before "Your Planets".
- gplus/40.REP — gplus variant; tabs in headers, pseudo-cyrillic ship class names, single fleet, ten incoming groups.
Field-level fidelity is the inline tests' responsibility; the smoke tests catch regressions where a refactor of the section classifier silently drops a whole table.