feat(model+ui): F8-05 — race on OtherGroup, real attribution + N×M label
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m6s
Tests · Go / test (push) Successful in 2m6s
Tests · Integration / integration (pull_request) Successful in 1m51s
Tests · UI / test (pull_request) Successful in 3m53s
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m6s
Tests · Go / test (push) Successful in 2m6s
Tests · Integration / integration (pull_request) Successful in 1m51s
Tests · UI / test (pull_request) Successful in 3m53s
Issue #48 п.32 ("Stationed ship groups") shipped with a fragile race fallback: when a foreign group sat on a non-`other`-kind planet the inspector printed a generic "foreign" label, which collapsed the race dropdown to a single uninformative bucket. The engine FBS contract did not carry per-group race either, so live games hit the same gap. This patch carries race authoritatively from the engine through every layer down to the inspector. Wire format & engine - `pkg/schema/fbs/report.fbs`: add `race:string` to `OtherGroup` and `LocalGroup` (additive — old clients ignore). - `pkg/schema/fbs/report/`: regenerated Go bindings. - `ui/frontend/src/proto/galaxy/fbs/report/`: regenerated TS bindings. - `pkg/model/report.OtherGroup.Race`: new field; carried through `LocalGroup` via the embedded `OtherGroup`. - `pkg/transcoder/report.go`: encode + decode `race` on both `LocalGroup` and `OtherGroup`. - `game/internal/controller/report.go.otherGroup`: set `v.Race` from `c.g.Race[c.RaceIndex(sg.OwnerID)].Name` so every emitted group — own or foreign — carries the resolved race name. Legacy parser - `tools/local-dev/legacy-report/parser.go`: capture the `<Race> Groups` header into `pendingOtherGroup.race`, fill local group `Race` from `p.rep.Race`, propagate both into the `report.OtherGroup` rows. - Tests + smoke counts updated; regenerated `KNNTS{039,041}.json` fixtures so the synthetic loader carries the new field. UI - `ui/frontend/src/api/`: `ReportShipGroupBase.race` field; synthetic loader + FBS decoder populate it. - `ui/frontend/src/lib/inspectors/planet/ship-groups.svelte`: the stationed-groups inspector picks race directly from `group.race` (own falls back to `localRace`, both finally to the `race.unknown` placeholder). The planet-owner / "foreign" heuristic is gone. - Row label changes from "N ships mass M" to a compact `<class>` | `<N ×>` | `<mass>` three-column layout: the count cell is right-aligned tabular, the mass cell is right-aligned monospace + tabular, matching the inspector / calculator number conventions. Stale i18n keys removed (`ship_groups.row.count`, `.row.mass`, `.race.foreign`). - All affected unit tests (8 files) carry the new `race` field. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,13 @@ type OtherGroup struct {
|
||||
Range *Float `json:"range,omitempty"`
|
||||
Speed Float `json:"speed"`
|
||||
Mass Float `json:"mass"`
|
||||
// Race is the owner's display name resolved from `sg.OwnerID`
|
||||
// (or, for legacy reports, the section header that introduced
|
||||
// the row). The local race fills this in for its own
|
||||
// `LocalGroup` copies via the embedded `OtherGroup` field so
|
||||
// every group carries an authoritative attribution rather than a
|
||||
// "foreign" fallback heuristic at render time.
|
||||
Race string `json:"race,omitempty"`
|
||||
}
|
||||
|
||||
type UnidentifiedGroup struct {
|
||||
|
||||
@@ -168,6 +168,7 @@ table OtherGroup {
|
||||
range:float32 = null;
|
||||
speed:float32;
|
||||
mass:float32;
|
||||
race:string;
|
||||
}
|
||||
|
||||
table LocalGroup {
|
||||
@@ -184,6 +185,7 @@ table LocalGroup {
|
||||
id:common.UUID (required);
|
||||
state:string;
|
||||
fleet:string;
|
||||
race:string;
|
||||
}
|
||||
|
||||
table LocalFleet {
|
||||
|
||||
@@ -194,8 +194,16 @@ func (rcv *LocalGroup) Fleet() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *LocalGroup) Race() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(30))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LocalGroupStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(13)
|
||||
builder.StartObject(14)
|
||||
}
|
||||
func LocalGroupAddNumber(builder *flatbuffers.Builder, number uint64) {
|
||||
builder.PrependUint64Slot(0, number, 0)
|
||||
@@ -241,6 +249,9 @@ func LocalGroupAddState(builder *flatbuffers.Builder, state flatbuffers.UOffsetT
|
||||
func LocalGroupAddFleet(builder *flatbuffers.Builder, fleet flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(12, flatbuffers.UOffsetT(fleet), 0)
|
||||
}
|
||||
func LocalGroupAddRace(builder *flatbuffers.Builder, race flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(13, flatbuffers.UOffsetT(race), 0)
|
||||
}
|
||||
func LocalGroupEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
|
||||
@@ -163,8 +163,16 @@ func (rcv *OtherGroup) MutateMass(n float32) bool {
|
||||
return rcv._tab.MutateFloat32Slot(22, n)
|
||||
}
|
||||
|
||||
func (rcv *OtherGroup) Race() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(24))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func OtherGroupStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(10)
|
||||
builder.StartObject(11)
|
||||
}
|
||||
func OtherGroupAddNumber(builder *flatbuffers.Builder, number uint64) {
|
||||
builder.PrependUint64Slot(0, number, 0)
|
||||
@@ -201,6 +209,9 @@ func OtherGroupAddSpeed(builder *flatbuffers.Builder, speed float32) {
|
||||
func OtherGroupAddMass(builder *flatbuffers.Builder, mass float32) {
|
||||
builder.PrependFloat32Slot(9, mass, 0.0)
|
||||
}
|
||||
func OtherGroupAddRace(builder *flatbuffers.Builder, race flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(10, flatbuffers.UOffsetT(race), 0)
|
||||
}
|
||||
func OtherGroupEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
|
||||
@@ -513,6 +513,7 @@ func encodeReportLocalGroup(builder *flatbuffers.Builder, group *model.LocalGrou
|
||||
class := builder.CreateString(group.Class)
|
||||
cargo := builder.CreateString(group.Cargo)
|
||||
state := builder.CreateString(group.State)
|
||||
race := builder.CreateString(group.Race)
|
||||
|
||||
tech := encodeReportTechEntryVector(builder, group.Tech)
|
||||
var fleet flatbuffers.UOffsetT
|
||||
@@ -544,6 +545,7 @@ func encodeReportLocalGroup(builder *flatbuffers.Builder, group *model.LocalGrou
|
||||
if group.Fleet != nil {
|
||||
fbs.LocalGroupAddFleet(builder, fleet)
|
||||
}
|
||||
fbs.LocalGroupAddRace(builder, race)
|
||||
return fbs.LocalGroupEnd(builder)
|
||||
}
|
||||
|
||||
@@ -551,6 +553,7 @@ func encodeReportOtherGroup(builder *flatbuffers.Builder, group *model.OtherGrou
|
||||
class := builder.CreateString(group.Class)
|
||||
cargo := builder.CreateString(group.Cargo)
|
||||
tech := encodeReportTechEntryVector(builder, group.Tech)
|
||||
race := builder.CreateString(group.Race)
|
||||
|
||||
fbs.OtherGroupStart(builder)
|
||||
fbs.OtherGroupAddNumber(builder, uint64(group.Number))
|
||||
@@ -569,6 +572,7 @@ func encodeReportOtherGroup(builder *flatbuffers.Builder, group *model.OtherGrou
|
||||
}
|
||||
fbs.OtherGroupAddSpeed(builder, reportFloatToFBS(group.Speed))
|
||||
fbs.OtherGroupAddMass(builder, reportFloatToFBS(group.Mass))
|
||||
fbs.OtherGroupAddRace(builder, race)
|
||||
return fbs.OtherGroupEnd(builder)
|
||||
}
|
||||
|
||||
@@ -1134,6 +1138,7 @@ func decodeReportLocalGroupVector(flatReport *fbs.Report, result *model.Report)
|
||||
Destination: destination,
|
||||
Speed: reportFloatFromFBS(item.Speed()),
|
||||
Mass: reportFloatFromFBS(item.Mass()),
|
||||
Race: string(item.Race()),
|
||||
},
|
||||
ID: uuidFromHiLo(id.Hi(), id.Lo()),
|
||||
State: string(item.State()),
|
||||
@@ -1196,6 +1201,7 @@ func decodeReportOtherGroupVector(flatReport *fbs.Report, result *model.Report)
|
||||
Destination: destination,
|
||||
Speed: reportFloatFromFBS(item.Speed()),
|
||||
Mass: reportFloatFromFBS(item.Mass()),
|
||||
Race: string(item.Race()),
|
||||
}
|
||||
|
||||
if origin := item.Origin(); origin != nil {
|
||||
|
||||
@@ -352,6 +352,7 @@ func sampleReport() *model.Report {
|
||||
Range: &rangeB,
|
||||
Speed: model.Float(2.5),
|
||||
Mass: model.Float(12.0),
|
||||
Race: "Earthlings",
|
||||
},
|
||||
ID: uuid.MustParse("33333333-3333-3333-3333-333333333333"),
|
||||
State: "in_orbit",
|
||||
@@ -359,7 +360,7 @@ func sampleReport() *model.Report {
|
||||
},
|
||||
},
|
||||
OtherGroup: []model.OtherGroup{
|
||||
{Number: 2, Class: "scout", Tech: map[string]model.Float{"CARGO": model.Float(1.25), "DRIVE": model.Float(1.75)}, Cargo: "CAP", Load: model.Float(3.5), Destination: 5, Speed: model.Float(2.25), Mass: model.Float(8.5)},
|
||||
{Number: 2, Class: "scout", Tech: map[string]model.Float{"CARGO": model.Float(1.25), "DRIVE": model.Float(1.75)}, Cargo: "CAP", Load: model.Float(3.5), Destination: 5, Speed: model.Float(2.25), Mass: model.Float(8.5), Race: "Klingons"},
|
||||
},
|
||||
UnidentifiedGroup: []model.UnidentifiedGroup{
|
||||
{X: model.Float(10.0), Y: model.Float(11.0)},
|
||||
|
||||
Reference in New Issue
Block a user