feat(ui+legacy): F8-05 owner-feedback round 1 — inspector tweaks + parser
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>
This commit is contained in:
@@ -4,11 +4,13 @@
|
||||
// Scope is intentionally narrow: only the fields the UI client decodes
|
||||
// from server reports today (planets, players, own ship classes,
|
||||
// header data, plus — added in Phase 19 — own ship groups, own fleets
|
||||
// and incoming groups). Everything else in the legacy file is silently
|
||||
// skipped. The synthetic-report parity rule in ui/PLAN.md is the
|
||||
// source of truth for when to extend this parser; the package's
|
||||
// README.md tracks every legacy section that could be wired up later
|
||||
// when the corresponding UI decoder lands.
|
||||
// and incoming groups; and — added in F8-05 — foreign `<Race> Groups`
|
||||
// blocks outside battles together with `Unidentified Groups`).
|
||||
// Everything else in the legacy file is silently skipped. The
|
||||
// synthetic-report parity rule in ui/PLAN.md is the source of truth
|
||||
// for when to extend this parser; the package's README.md tracks
|
||||
// every legacy section that could be wired up later when the
|
||||
// corresponding UI decoder lands.
|
||||
package legacyreport
|
||||
|
||||
import (
|
||||
@@ -65,6 +67,8 @@ const (
|
||||
sectionYourGroups
|
||||
sectionYourFleets
|
||||
sectionIncomingGroups
|
||||
sectionOtherGroups
|
||||
sectionUnidentifiedGroups
|
||||
sectionYourSciences
|
||||
sectionOtherSciences
|
||||
sectionOtherShipTypes
|
||||
@@ -94,6 +98,7 @@ type parser struct {
|
||||
pendingFleets []pendingFleet
|
||||
pendingIncomings []pendingIncoming
|
||||
pendingShipProducts []pendingShipProduction
|
||||
pendingOtherGroups []pendingOtherGroup
|
||||
|
||||
// Battle accumulator. `battles` collects every parsed BattleReport;
|
||||
// `pendingBattle` carries the in-flight battle until its block
|
||||
@@ -148,6 +153,25 @@ type pendingGroup struct {
|
||||
state string
|
||||
}
|
||||
|
||||
// pendingOtherGroup buffers a foreign "<Race> Groups" row outside any
|
||||
// battle block — these are visible to the local player but live on
|
||||
// foreign planets, so the destination resolves against the parsed
|
||||
// planet tables in [parser.resolvePending]. The legacy row carries
|
||||
// no origin / range / fleet columns, so foreign groups are always
|
||||
// treated as stationed at the destination.
|
||||
type pendingOtherGroup struct {
|
||||
count uint
|
||||
class string
|
||||
drive float64
|
||||
weapons float64
|
||||
shields float64
|
||||
cargoTech float64
|
||||
cargoType string
|
||||
load float64
|
||||
destinationName string
|
||||
mass float64
|
||||
}
|
||||
|
||||
type pendingFleet struct {
|
||||
name string
|
||||
groups uint
|
||||
@@ -290,6 +314,10 @@ func (p *parser) handle(line string) error {
|
||||
p.parseYourFleet(fields)
|
||||
case sectionIncomingGroups:
|
||||
p.parseIncomingGroup(fields)
|
||||
case sectionOtherGroups:
|
||||
p.parseOtherGroup(fields)
|
||||
case sectionUnidentifiedGroups:
|
||||
p.parseUnidentifiedGroup(fields)
|
||||
case sectionYourSciences:
|
||||
p.parseYourScience(fields)
|
||||
case sectionOtherSciences:
|
||||
@@ -381,6 +409,8 @@ func classifySection(line string) (sec section, owner string, isHeader bool) {
|
||||
return sectionYourFleets, "", true
|
||||
case "Incoming Groups":
|
||||
return sectionIncomingGroups, "", true
|
||||
case "Unidentified Groups":
|
||||
return sectionUnidentifiedGroups, "", true
|
||||
case "Uninhabited Planets":
|
||||
return sectionUninhabitedPlanets, "", true
|
||||
case "Unidentified Planets":
|
||||
@@ -417,8 +447,8 @@ func classifySection(line string) (sec section, owner string, isHeader bool) {
|
||||
if owner, ok := singleTokenPrefix(line, " Sciences"); ok {
|
||||
return sectionOtherSciences, owner, true
|
||||
}
|
||||
if _, ok := singleTokenPrefix(line, " Groups"); ok {
|
||||
return sectionNone, "", true
|
||||
if owner, ok := singleTokenPrefix(line, " Groups"); ok {
|
||||
return sectionOtherGroups, owner, true
|
||||
}
|
||||
return sectionNone, "", false
|
||||
}
|
||||
@@ -1067,6 +1097,68 @@ func (p *parser) parseYourFleet(fields []string) {
|
||||
})
|
||||
}
|
||||
|
||||
// parseOtherGroup buffers a foreign "<Race> Groups" row. The row's
|
||||
// race is the current `otherOwner` set by classifySection. Columns
|
||||
// (11 fields, last is mass):
|
||||
//
|
||||
// # T D W S C T Q D P M
|
||||
//
|
||||
// where the second T is the cargo type, the second D is the
|
||||
// destination planet name, P is the flight-distance hint (drive*20,
|
||||
// not retained) and M is the mass.
|
||||
func (p *parser) parseOtherGroup(fields []string) {
|
||||
if len(fields) < 11 {
|
||||
return
|
||||
}
|
||||
count, err := strconv.ParseUint(fields[0], 10, 32)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
drive, _ := parseFloat(fields[2])
|
||||
weapons, _ := parseFloat(fields[3])
|
||||
shields, _ := parseFloat(fields[4])
|
||||
cargoTech, _ := parseFloat(fields[5])
|
||||
load, _ := parseFloat(fields[7])
|
||||
mass, _ := parseFloat(fields[10])
|
||||
|
||||
p.pendingOtherGroups = append(p.pendingOtherGroups, pendingOtherGroup{
|
||||
count: uint(count),
|
||||
class: fields[1],
|
||||
drive: drive,
|
||||
weapons: weapons,
|
||||
shields: shields,
|
||||
cargoTech: cargoTech,
|
||||
cargoType: fields[6],
|
||||
load: load,
|
||||
destinationName: fields[8],
|
||||
mass: mass,
|
||||
})
|
||||
}
|
||||
|
||||
// parseUnidentifiedGroup appends an "Unidentified Groups" row
|
||||
// directly to the report — the legacy format only carries the
|
||||
// floating-point world coordinates of the in-flight blip, so
|
||||
// there is nothing to defer to [parser.finish].
|
||||
//
|
||||
// X Y
|
||||
func (p *parser) parseUnidentifiedGroup(fields []string) {
|
||||
if len(fields) < 2 {
|
||||
return
|
||||
}
|
||||
x, err := parseFloat(fields[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
y, err := parseFloat(fields[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.rep.UnidentifiedGroup = append(p.rep.UnidentifiedGroup, report.UnidentifiedGroup{
|
||||
X: report.F(x),
|
||||
Y: report.F(y),
|
||||
})
|
||||
}
|
||||
|
||||
// parseIncomingGroup buffers an "Incoming Groups" row. Columns:
|
||||
//
|
||||
// O D R S M
|
||||
@@ -1145,6 +1237,28 @@ func (p *parser) resolvePending() {
|
||||
})
|
||||
}
|
||||
|
||||
for _, pg := range p.pendingOtherGroups {
|
||||
dest, ok := p.lookupPlanetNumber(pg.destinationName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tech := map[string]report.Float{
|
||||
"drive": report.F(pg.drive),
|
||||
"weapons": report.F(pg.weapons),
|
||||
"shields": report.F(pg.shields),
|
||||
"cargo": report.F(pg.cargoTech),
|
||||
}
|
||||
p.rep.OtherGroup = append(p.rep.OtherGroup, report.OtherGroup{
|
||||
Number: pg.count,
|
||||
Class: pg.class,
|
||||
Tech: tech,
|
||||
Cargo: pg.cargoType,
|
||||
Load: report.F(pg.load),
|
||||
Destination: dest,
|
||||
Mass: report.F(pg.mass),
|
||||
})
|
||||
}
|
||||
|
||||
for _, pf := range p.pendingFleets {
|
||||
dest, ok := p.lookupPlanetNumber(pf.destinationName)
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user