b23649059f
Side activity on top of Phase 27: the legacy-report tool now extracts
the "Battle at (#N) Name" / "Battle Protocol" blocks the parser used
to skip. Both the per-battle summary (Report.Battle: []BattleSummary)
and the full BattleReport (rosters + protocol) flow through.
Parser:
- new sectionBattle / sectionBattleProtocol states, with handle()
trapping the per-race "<Race> Groups" sub-headers so the roster
stays attributed to the right race;
- parseBattleHeader extracts (planet, planetName) from
"Battle at (#NN) <Name>";
- parseBattleRosterRow maps the 10-token row into
BattleReportGroup; column 8 ("L") is NumberLeft, confirmed against
KNNTS fixtures;
- parseBattleProtocolLine counts shots and builds
BattleActionReport entries from the 8-token "X Y fires on A B :
Destroyed|Shields" lines;
- flushPendingBattle finalises a battle on next "Battle at" or any
top-level section change and appends both the summary and the
full report;
- syntheticBattleID(idx) + syntheticBattleRaceID(name) synthesise
stable UUIDs in dedicated namespaces so re-runs produce
byte-identical JSON.
Parse() signature widens to (Report, []BattleReport, error); the
single caller — the CLI — is updated.
CLI emits a v1 envelope:
{ "version": 1, "report": <Report>, "battles": { <uuid>: <BR>, ... } }
Bare-Report JSONs still load on the UI side for backward compat.
UI synthetic loader: loadSyntheticReportFromJSON detects the v1
envelope, decodes the report as before, and forwards every battle
through registerSyntheticBattle so the Battle Viewer resolves any
UUID offline. Pre-envelope JSON files (no `version` field) still
load — the battle registry stays empty for them.
Docs: legacy-report README moves Battles from "Skipped" to
in-scope, documents the envelope and UUID namespaces;
docs/FUNCTIONAL.md §6.5 (and the ru mirror) note that synthetic
mode is now end-to-end via the envelope.
Tests:
- TestParseBattles covers two battles with full rosters,
per-shot destroyed/shielded mapping, NumberLeft from column 8,
deterministic UUIDs across re-parses, and proves a trailing
top-level section still parses (battle state closes cleanly);
- smokeWant gains a battles count; runSmoke cross-checks
BattleSummary ↔ BattleReport alignment (id/planet/shots);
- all six real-fixture smoke tests pinned to their `Battle at`
counts (28, 79, 56, 30, 83, 57);
- Vitest covers the synthetic-report envelope path (battles
forwarded, missing-battles tolerated, bare-Report backward
compat);
- KNNTS041.json regenerated against the new parser (existing
diff was stale w.r.t. Phase 23 anyway; this commit brings it
in line with the v1 envelope).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
2.7 KiB
Go
105 lines
2.7 KiB
Go
// Command legacy-report-to-json converts a legacy text-format Galaxy
|
|
// turn report (the "dg" / "gplus" engines) into a JSON envelope
|
|
// readable by the UI client's DEV-only synthetic-report loader:
|
|
//
|
|
// {
|
|
// "version": 1,
|
|
// "report": <Report JSON>,
|
|
// "battles": { "<uuid>": <BattleReport JSON>, ... }
|
|
// }
|
|
//
|
|
// Carrying the per-turn report and the full BattleReports in one
|
|
// payload lets the synthetic loader register the battles up-front
|
|
// so the Battle Viewer can render any battle without a network
|
|
// fetch. The bare Report shape (no envelope) the lobby loader
|
|
// historically accepted remains backward-compatible on the UI side.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
legacyreport "galaxy/legacy-report"
|
|
"galaxy/model/report"
|
|
)
|
|
|
|
// envelope is the on-disk shape emitted by this CLI. `Version` lets
|
|
// the UI loader distinguish a v1 envelope from a bare Report; future
|
|
// versions can bump it without breaking older synthetic JSON files.
|
|
type envelope struct {
|
|
Version int `json:"version"`
|
|
Report report.Report `json:"report"`
|
|
Battles map[string]report.BattleReport `json:"battles,omitempty"`
|
|
}
|
|
|
|
func main() {
|
|
in := flag.String("in", "", "path to legacy .REP file (use - for stdin)")
|
|
out := flag.String("out", "", "path to write JSON to (use - or empty for stdout)")
|
|
flag.Parse()
|
|
|
|
if *in == "" {
|
|
fmt.Fprintln(os.Stderr, "usage: legacy-report-to-json --in <path|-> [--out <path|->]")
|
|
os.Exit(2)
|
|
}
|
|
|
|
r, closeIn, err := openInput(*in)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "open input: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer closeIn()
|
|
|
|
rep, battles, err := legacyreport.Parse(r)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "parse: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
w, closeOut, err := openOutput(*out)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "open output: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer closeOut()
|
|
|
|
env := envelope{Version: 1, Report: rep}
|
|
if len(battles) > 0 {
|
|
env.Battles = make(map[string]report.BattleReport, len(battles))
|
|
for i := range battles {
|
|
env.Battles[battles[i].ID.String()] = battles[i]
|
|
}
|
|
}
|
|
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(env); err != nil {
|
|
fmt.Fprintf(os.Stderr, "encode: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func openInput(path string) (io.Reader, func(), error) {
|
|
if path == "-" {
|
|
return os.Stdin, func() {}, nil
|
|
}
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return f, func() { _ = f.Close() }, nil
|
|
}
|
|
|
|
func openOutput(path string) (io.Writer, func(), error) {
|
|
if path == "" || path == "-" {
|
|
return os.Stdout, func() {}, nil
|
|
}
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return f, func() { _ = f.Close() }, nil
|
|
}
|