Files
galaxy-game/tools/local-dev/legacy-report
Ilia Denisov 140ee8e0ee
Build · Site / build (push) Successful in 8s
Tests · Go / test (push) Successful in 2m27s
Tests · UI / test (push) Waiting to run
Tests · Integration / integration (pull_request) Successful in 1m45s
Build · Site / build (pull_request) Successful in 9s
Tests · Go / test (pull_request) Successful in 3m14s
Tests · UI / test (pull_request) Successful in 3m14s
docs(site): edit rules for clarity + cross-links; migrate off rules.txt
Editorial pass over site/ru/rules.md (on top of the verbatim port):
- moved the lore intro to the RU home page, rewritten in a modern voice;
- fixed typos, replaced the TODO/WTF cargo-tech note and the abandoned
  (---ссылка---) marker with the verified mechanic and a real cross-link,
  dropped the report TODO row;
- wove organic intra-page cross-links (#combat, #movement, #victory, ...);
- documented engine nuances verified against the code: ore auto-farming
  and the capital / "запасы промышленности" store (industry capped at
  population); cargo lost with ships destroyed in battle; and that a
  losing race's colonists at a neutral planet are NOT lost — they stay
  aboard (this corrects the audit note, verified in route.go).

Migration: delete game/rules.txt (its content now lives, authoritative,
in site/ru/rules.md) and repoint every reference to it (ui/frontend code
comments + tests, ui/docs, tools, ui/PLAN.md links). Record the
RU-authoritative rule in site/README.md and CLAUDE.md. The English
site/rules.md mirror follows in a separate stage.
2026-05-31 15:56:00 +02:00
..

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.tsdecodeReport):

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> Groups section outside a Battle at block 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[] — the Unidentified Groups section carries X Y floats only. Each row maps directly onto UnidentifiedGroup{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 empty routes.

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:

  1. Identify the legacy section in tools/local-dev/reports/dg/*.REP (and gplus/*.REP) that carries the field, using site/ru/rules.md section "Отчет о результатах хода" as the column-layout reference.
  2. Add a section to the state machine in parser.go (classifySection, the section constants, the parse* methods).
  3. Cover the new section with a unit test in parser_test.go (inline minimal fixture) and update the smoke counts in TestParseDgKNNTS039 / TestParseGplus40 so a future regression that drops the section is caught.
  4. Run go test ./tools/local-dev/legacy-report/..., then re-run the CLI on dg/KNNTS039.REP and gplus/40.REP and 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/KNNTS039041 — KnightErrants saga; 041 is the only one with Incoming Groups, exercising deferred name resolution.
  • dg/Killer031 — Killer engine variant with two Your Fleets entries (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.