Files
galaxy-game/tools/local-dev/legacy-report/README.md
T
Ilia Denisov 8839f46c25 ui/phase-19: legacy parser learns Your Groups / Your Fleets / Incoming Groups
The parity rule from ui/PLAN.md says every UI phase that decodes a
new Report field must extend the legacy converter in lockstep.
Phase 19 brings ship groups (LocalGroup / OtherGroup /
UnidentifiedGroup / IncomingGroup) and LocalFleet onto the wire-
compatible UI surface; this commit teaches
tools/local-dev/legacy-report to populate the three sections that
exist in the legacy text format:

  - "Your Groups" → []LocalGroup. Cargo type, load, fleet name,
    state, on-planet vs hyperspace position (origin / range) all
    decoded; LocalGroup.ID is synthesised deterministically from
    the per-report group index so re-running the converter
    produces byte-identical JSON. Speed is left zero — the legacy
    table doesn't expose it.
  - "Your Fleets" → []LocalFleet. Origin / range / state mirror
    the row layout used by Killer / Tancordia variants; gplus's
    state-less rows still resolve.
  - "Incoming Groups" → []IncomingGroup. Origin / destination
    names — and `#NN` by-id references — resolve against the
    parsed planet tables. Because the section can land before
    "Your Planets" in some engines, group / fleet / incoming rows
    are buffered and resolved in `parser.finish` after every
    planet is known.

Battles, OtherGroup (only ever in battle rosters), and
UnidentifiedGroup stay out of scope — README.md spells out what
remains not-derivable.

Adds Killer031–033 / TSERCON_Z032–033 / Tancordia036–039 fixtures
to the dg directory and exercises three of them through new
TestParseDg{Killer031,Tancordia037,KNNTS041} smoke tests, plus
inline tests for each new section parser. Drops the stale
KNNTS039.json artefact left over from Phase 18 development.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 13:23:17 +02:00

7.2 KiB
Raw Blame History

legacy-report-to-json

Converts legacy text-format Galaxy turn reports (the dg and gplus engines that lived under tools/local-dev/reports/) into the JSON shape of pkg/model/report.Report.

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/KNNTS039.REP \
    --out tools/local-dev/reports/dg/KNNTS039.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
LocalGroup[] Your Groups (Phase 19)
LocalFleet[] Your Fleets (Phase 19)
IncomingGroup[] Incoming Groups (Phase 19)

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.

Skipped sections (today)

These exist in legacy reports but either have no UI decoder yet or cannot be derived from the legacy text format at all. Each becomes in-scope as soon as its UI phase lands (see "Adding a new field" below).

  • Foreign / other ship types (<Race> Ship Types)
  • Sciences, both local (Your Sciences) and foreign (<Race> Sciences)
  • Battles (Battle at (#N) Name, Battle Protocol) — battle rosters inside these blocks carry minimal columns (no origin / range / destination) and are intentionally skipped: parsing them would produce mostly-empty OtherGroup records that drift away from the typed contract.
  • Bombings (Bombings)
  • Ships in production (Ships In Production)
  • OtherGroup[] — no top-level legacy section. Foreign groups appear only inside battle rosters (see above), with stripped columns; the synthetic JSON emits otherGroup: [].
  • UnidentifiedGroup[] — no legacy section at all; synthetic JSON emits unidentifiedGroup: [].
  • OtherShipClass[] — present in legacy as <Race> Ship Types, but no UI decoder yet; synthetic JSON emits otherShipClass: [].
  • 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 game/rules.txt 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.