From ebd156ece22eadc41af668069045893be21a71e2 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Sat, 16 May 2026 12:41:54 +0200 Subject: [PATCH] battle-fetch: migrate to user.games.battle ConnectRPC command The Phase 27 BattleViewer was the last UI surface still issuing raw fetch() against the backend REST contract (`/api/v1/user/games/... /battles/...`). The dev-deploy gateway never proxied that path, so the viewer worked only in tools/local-dev/. Move it onto the signed ConnectRPC channel every other authenticated surface already uses. Wire pieces: - FBS GameBattleRequest in pkg/schema/fbs/battle.fbs, regenerated Go + TS bindings. - MessageTypeUserGamesBattle constant + GameBattleRequest struct in pkg/model/report/messages.go. - pkg/transcoder/battle.go gains GameBattleRequestToPayload and PayloadToGameBattleRequest helpers. - gateway games_commands.go switches on the new message type and GETs /api/v1/user/games/{id}/battles/{turn}/{battle_id}; the JSON response is re-encoded as a FlatBuffers BattleReport before being returned. 404 from backend surfaces as the canonical `not_found` gateway error. - ui/frontend/src/api/battle-fetch.ts now builds the FBS request, calls GalaxyClient.executeCommand, and decodes the FBS response into the existing UI shape (Record race/ship maps, string-form UUID). BattleFetchError carries an HTTP-style status derived from the result code so the active-view's not_found branch keeps working. - battle.svelte pulls the GalaxyClient from the in-game shell context. While the layout's boot Promise.all is in flight the effect stays in `loading` until the client handle becomes non-null. - ui/Makefile FBS_INPUTS gains battle.fbs. Co-Authored-By: Claude Opus 4.7 --- .../internal/backendclient/games_commands.go | 55 +++++ .../backendclient/games_commands_test.go | 73 ++++++ gateway/internal/backendclient/routes.go | 1 + gateway/internal/backendclient/routes_test.go | 1 + pkg/model/report/messages.go | 23 ++ pkg/schema/fbs/battle.fbs | 10 + pkg/schema/fbs/battle/GameBattleRequest.go | 96 ++++++++ pkg/transcoder/battle.go | 52 ++++ ui/Makefile | 2 +- ui/frontend/src/api/battle-fetch.ts | 227 +++++++++++++++--- ui/frontend/src/lib/active-view/battle.svelte | 17 +- ui/frontend/src/proto/galaxy/fbs/battle.ts | 12 + .../galaxy/fbs/battle/battle-action-report.ts | 130 ++++++++++ .../galaxy/fbs/battle/battle-report-group.ts | 201 ++++++++++++++++ .../proto/galaxy/fbs/battle/battle-report.ts | 215 +++++++++++++++++ .../galaxy/fbs/battle/game-battle-request.ts | 99 ++++++++ .../src/proto/galaxy/fbs/battle/race-entry.ts | 85 +++++++ .../src/proto/galaxy/fbs/battle/ship-entry.ts | 86 +++++++ .../src/proto/galaxy/fbs/battle/tech-entry.ts | 92 +++++++ .../src/proto/galaxy/fbs/battle/uuid.ts | 65 +++++ 20 files changed, 1513 insertions(+), 29 deletions(-) create mode 100644 pkg/schema/fbs/battle/GameBattleRequest.go create mode 100644 ui/frontend/src/proto/galaxy/fbs/battle.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/battle/battle-action-report.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/battle/battle-report-group.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/battle/battle-report.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/battle/game-battle-request.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/battle/race-entry.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/battle/ship-entry.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/battle/tech-entry.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/battle/uuid.ts diff --git a/gateway/internal/backendclient/games_commands.go b/gateway/internal/backendclient/games_commands.go index 799c538..2688464 100644 --- a/gateway/internal/backendclient/games_commands.go +++ b/gateway/internal/backendclient/games_commands.go @@ -63,6 +63,12 @@ func (c *RESTClient) ExecuteGameCommand(ctx context.Context, command downstream. return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command %q: %w", command.MessageType, err) } return c.executeUserGamesReport(ctx, command.UserID, req) + case reportmodel.MessageTypeUserGamesBattle: + req, err := transcoder.PayloadToGameBattleRequest(command.PayloadBytes) + if err != nil { + return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command %q: %w", command.MessageType, err) + } + return c.executeUserGamesBattle(ctx, command.UserID, req) default: return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command: unsupported message type %q", command.MessageType) } @@ -127,6 +133,26 @@ func (c *RESTClient) executeUserGamesReport(ctx context.Context, userID string, return projectUserGamesReportResponse(status, respBody) } +func (c *RESTClient) executeUserGamesBattle(ctx context.Context, userID string, req *reportmodel.GameBattleRequest) (downstream.UnaryResult, error) { + if req.GameID == uuid.Nil { + return downstream.UnaryResult{}, errors.New("execute user.games.battle: game_id must not be empty") + } + if req.BattleID == uuid.Nil { + return downstream.UnaryResult{}, errors.New("execute user.games.battle: battle_id must not be empty") + } + target := fmt.Sprintf("%s/api/v1/user/games/%s/battles/%d/%s", + c.baseURL, + url.PathEscape(req.GameID.String()), + req.Turn, + url.PathEscape(req.BattleID.String()), + ) + respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil) + if err != nil { + return downstream.UnaryResult{}, fmt.Errorf("execute user.games.battle: %w", err) + } + return projectUserGamesBattleResponse(status, respBody) +} + // buildEngineCommandBody serialises a slice of typed commands into the // JSON shape expected by backend's command/order handlers (a // `gamerest.Command` with the actor field left empty — backend rebinds @@ -262,3 +288,32 @@ func projectUserGamesReportResponse(statusCode int, payload []byte) (downstream. return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode) } } + +// projectUserGamesBattleResponse decodes the engine's BattleReport JSON +// payload (forwarded by backend's user.games.battle proxy) and +// re-encodes it as a FlatBuffers BattleReport for the signed-gRPC +// client. 404 from backend surfaces as the canonical `not_found` +// gateway error so the UI can render its "battle not found" state. +func projectUserGamesBattleResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) { + switch { + case statusCode == http.StatusOK: + var report reportmodel.BattleReport + if err := json.Unmarshal(payload, &report); err != nil { + return downstream.UnaryResult{}, fmt.Errorf("decode engine battle report: %w", err) + } + encoded, err := transcoder.BattleReportToPayload(&report) + if err != nil { + return downstream.UnaryResult{}, fmt.Errorf("encode battle report payload: %w", err) + } + return downstream.UnaryResult{ + ResultCode: userCommandResultCodeOK, + PayloadBytes: encoded, + }, nil + case statusCode == http.StatusServiceUnavailable: + return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable + case statusCode >= 400 && statusCode <= 599: + return projectUserBackendError(statusCode, payload) + default: + return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode) + } +} diff --git a/gateway/internal/backendclient/games_commands_test.go b/gateway/internal/backendclient/games_commands_test.go index 05ca497..4382de9 100644 --- a/gateway/internal/backendclient/games_commands_test.go +++ b/gateway/internal/backendclient/games_commands_test.go @@ -11,6 +11,7 @@ import ( "galaxy/gateway/internal/backendclient" "galaxy/gateway/internal/downstream" ordermodel "galaxy/model/order" + reportmodel "galaxy/model/report" "galaxy/transcoder" "github.com/google/uuid" @@ -170,6 +171,78 @@ func TestExecuteUserGamesOrderGetRejectsNegativeTurn(t *testing.T) { assert.Contains(t, err.Error(), "user.games.order.get") } +func TestExecuteUserGamesBattleForwardsAndDecodesResponse(t *testing.T) { + t.Parallel() + + gameID := uuid.MustParse("66666666-7777-8888-9999-aaaaaaaaaaaa") + battleID := uuid.MustParse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + require.Equal(t, + "/api/v1/user/games/"+gameID.String()+"/battles/7/"+battleID.String(), + r.URL.Path, + ) + require.Equal(t, "user-1", r.Header.Get(backendclient.HeaderUserID)) + raceID := uuid.MustParse("11111111-2222-3333-4444-555555555555") + writeJSON(t, w, http.StatusOK, map[string]any{ + "id": battleID.String(), + "planet": uint(42), + "planetName": "Tau Ceti II", + "races": map[string]string{"1": raceID.String()}, + "ships": map[string]map[string]any{}, + "protocol": []any{}, + }) + })) + t.Cleanup(server.Close) + + client := newRESTClient(t, server) + payload, err := transcoder.GameBattleRequestToPayload(&reportmodel.GameBattleRequest{ + GameID: gameID, + Turn: 7, + BattleID: battleID, + }) + require.NoError(t, err) + result, err := client.ExecuteGameCommand(context.Background(), newAuthCommand(t, reportmodel.MessageTypeUserGamesBattle, payload)) + require.NoError(t, err) + assert.Equal(t, "ok", result.ResultCode) + + decoded, err := transcoder.PayloadToBattleReport(result.PayloadBytes) + require.NoError(t, err) + require.NotNil(t, decoded) + assert.Equal(t, battleID, decoded.ID) + assert.Equal(t, uint(42), decoded.Planet) + assert.Equal(t, "Tau Ceti II", decoded.PlanetName) +} + +func TestExecuteUserGamesBattleMapsNotFound(t *testing.T) { + t.Parallel() + + gameID := uuid.MustParse("77777777-8888-9999-aaaa-bbbbbbbbbbbb") + battleID := uuid.MustParse("99999999-aaaa-bbbb-cccc-dddddddddddd") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeJSON(t, w, http.StatusNotFound, map[string]any{ + "error": map[string]any{ + "code": "not_found", + "message": "battle not found", + }, + }) + })) + t.Cleanup(server.Close) + + client := newRESTClient(t, server) + payload, err := transcoder.GameBattleRequestToPayload(&reportmodel.GameBattleRequest{ + GameID: gameID, + Turn: 2, + BattleID: battleID, + }) + require.NoError(t, err) + result, err := client.ExecuteGameCommand(context.Background(), newAuthCommand(t, reportmodel.MessageTypeUserGamesBattle, payload)) + require.NoError(t, err) + assert.Equal(t, "not_found", result.ResultCode) +} + // writeJSON copy below mirrors the helper used by other test files // in this package; keeping it adjacent to its callers avoids // reaching across files in a fresh test. diff --git a/gateway/internal/backendclient/routes.go b/gateway/internal/backendclient/routes.go index ecd9e1f..86d6ae5 100644 --- a/gateway/internal/backendclient/routes.go +++ b/gateway/internal/backendclient/routes.go @@ -65,6 +65,7 @@ func GameRoutes(client *RESTClient) map[string]downstream.Client { ordermodel.MessageTypeUserGamesOrder: target, ordermodel.MessageTypeUserGamesOrderGet: target, reportmodel.MessageTypeUserGamesReport: target, + reportmodel.MessageTypeUserGamesBattle: target, } } diff --git a/gateway/internal/backendclient/routes_test.go b/gateway/internal/backendclient/routes_test.go index 2c4df89..aba6d75 100644 --- a/gateway/internal/backendclient/routes_test.go +++ b/gateway/internal/backendclient/routes_test.go @@ -60,6 +60,7 @@ func TestRoutesCoverAllAuthenticatedMessageTypes(t *testing.T) { ordermodel.MessageTypeUserGamesOrder, ordermodel.MessageTypeUserGamesOrderGet, reportmodel.MessageTypeUserGamesReport, + reportmodel.MessageTypeUserGamesBattle, }, actual: backendclient.GameRoutes(nil), }, diff --git a/pkg/model/report/messages.go b/pkg/model/report/messages.go index f3668b9..f8a8eee 100644 --- a/pkg/model/report/messages.go +++ b/pkg/model/report/messages.go @@ -9,6 +9,13 @@ import "github.com/google/uuid" // `Report`. const MessageTypeUserGamesReport = "user.games.report" +// MessageTypeUserGamesBattle is the authenticated gateway message type +// used to fetch one battle report through +// `GET /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}`. The +// signed payload is a FlatBuffers `GameBattleRequest`; the response is +// a FlatBuffers `BattleReport`. +const MessageTypeUserGamesBattle = "user.games.battle" + // GameReportRequest is the typed payload of MessageTypeUserGamesReport. // `GameID` selects the target game (the message_type alone is not // enough; this scope is per-game) and `Turn` selects the requested @@ -20,3 +27,19 @@ type GameReportRequest struct { // Turn is the zero-based turn number whose report is requested. Turn uint `json:"turn"` } + +// GameBattleRequest is the typed payload of MessageTypeUserGamesBattle. +// `GameID` selects the target game; `Turn` is the turn the battle +// happened at (the engine partitions battles by turn for cheap lookup); +// `BattleID` is the in-game identifier returned in the report's +// battle-summary list. All three fields are required. +type GameBattleRequest struct { + // GameID identifies the game the battle belongs to. + GameID uuid.UUID `json:"game_id"` + + // Turn is the turn number the battle happened at. + Turn uint `json:"turn"` + + // BattleID is the engine-assigned id of the battle to fetch. + BattleID uuid.UUID `json:"battle_id"` +} diff --git a/pkg/schema/fbs/battle.fbs b/pkg/schema/fbs/battle.fbs index 0f57b83..254019a 100644 --- a/pkg/schema/fbs/battle.fbs +++ b/pkg/schema/fbs/battle.fbs @@ -49,4 +49,14 @@ table BattleReport { protocol:[BattleActionReport]; } +// GameBattleRequest is the signed-gRPC request payload for +// `MessageTypeUserGamesBattle`. Gateway forwards this into the +// backend's `GET /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}` +// endpoint after resolving the caller's runtime player mapping. +table GameBattleRequest { + game_id:UUID (required); + turn:uint32; + battle_id:UUID (required); +} + root_type BattleReport; diff --git a/pkg/schema/fbs/battle/GameBattleRequest.go b/pkg/schema/fbs/battle/GameBattleRequest.go new file mode 100644 index 0000000..0651b7f --- /dev/null +++ b/pkg/schema/fbs/battle/GameBattleRequest.go @@ -0,0 +1,96 @@ +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + +package battle + +import ( + flatbuffers "github.com/google/flatbuffers/go" +) + +type GameBattleRequest struct { + _tab flatbuffers.Table +} + +func GetRootAsGameBattleRequest(buf []byte, offset flatbuffers.UOffsetT) *GameBattleRequest { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &GameBattleRequest{} + x.Init(buf, n+offset) + return x +} + +func FinishGameBattleRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsGameBattleRequest(buf []byte, offset flatbuffers.UOffsetT) *GameBattleRequest { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &GameBattleRequest{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedGameBattleRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *GameBattleRequest) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *GameBattleRequest) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *GameBattleRequest) GameId(obj *UUID) *UUID { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + x := o + rcv._tab.Pos + if obj == nil { + obj = new(UUID) + } + obj.Init(rcv._tab.Bytes, x) + return obj + } + return nil +} + +func (rcv *GameBattleRequest) Turn() uint32 { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + return rcv._tab.GetUint32(o + rcv._tab.Pos) + } + return 0 +} + +func (rcv *GameBattleRequest) MutateTurn(n uint32) bool { + return rcv._tab.MutateUint32Slot(6, n) +} + +func (rcv *GameBattleRequest) BattleId(obj *UUID) *UUID { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + x := o + rcv._tab.Pos + if obj == nil { + obj = new(UUID) + } + obj.Init(rcv._tab.Bytes, x) + return obj + } + return nil +} + +func GameBattleRequestStart(builder *flatbuffers.Builder) { + builder.StartObject(3) +} +func GameBattleRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) { + builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0) +} +func GameBattleRequestAddTurn(builder *flatbuffers.Builder, turn uint32) { + builder.PrependUint32Slot(1, turn, 0) +} +func GameBattleRequestAddBattleId(builder *flatbuffers.Builder, battleId flatbuffers.UOffsetT) { + builder.PrependStructSlot(2, flatbuffers.UOffsetT(battleId), 0) +} +func GameBattleRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} diff --git a/pkg/transcoder/battle.go b/pkg/transcoder/battle.go index 5190227..cd154de 100644 --- a/pkg/transcoder/battle.go +++ b/pkg/transcoder/battle.go @@ -382,3 +382,55 @@ func encodeBattleOffsetVector( } return builder.EndVector(length) } + +// GameBattleRequestToPayload converts model.GameBattleRequest to +// FlatBuffers bytes suitable for the authenticated gateway transport. +func GameBattleRequestToPayload(req *model.GameBattleRequest) ([]byte, error) { + if req == nil { + return nil, errors.New("encode game battle request payload: request is nil") + } + + builder := flatbuffers.NewBuilder(64) + + gameHi, gameLo := uuidToHiLo(req.GameID) + battleHi, battleLo := uuidToHiLo(req.BattleID) + + fbs.GameBattleRequestStart(builder) + fbs.GameBattleRequestAddGameId(builder, fbs.CreateUUID(builder, gameHi, gameLo)) + fbs.GameBattleRequestAddTurn(builder, uint32(req.Turn)) + fbs.GameBattleRequestAddBattleId(builder, fbs.CreateUUID(builder, battleHi, battleLo)) + offset := fbs.GameBattleRequestEnd(builder) + fbs.FinishGameBattleRequestBuffer(builder, offset) + + return builder.FinishedBytes(), nil +} + +// PayloadToGameBattleRequest converts FlatBuffers payload bytes into +// model.GameBattleRequest. +func PayloadToGameBattleRequest(data []byte) (result *model.GameBattleRequest, err error) { + if len(data) == 0 { + return nil, errors.New("decode game battle request payload: data is empty") + } + + defer func() { + if recovered := recover(); recovered != nil { + result = nil + err = fmt.Errorf("decode game battle request payload: panic recovered: %v", recovered) + } + }() + + req := fbs.GetRootAsGameBattleRequest(data, 0) + gameID := req.GameId(nil) + if gameID == nil { + return nil, errors.New("decode game battle request payload: game_id is missing") + } + battleID := req.BattleId(nil) + if battleID == nil { + return nil, errors.New("decode game battle request payload: battle_id is missing") + } + return &model.GameBattleRequest{ + GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()), + Turn: uint(req.Turn()), + BattleID: uuidFromHiLo(battleID.Hi(), battleID.Lo()), + }, nil +} diff --git a/ui/Makefile b/ui/Makefile index e55aa65..db05e4f 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -6,7 +6,7 @@ WASM_OUT := frontend/static/core.wasm WASM_EXEC := frontend/static/wasm_exec.js TINYGO_ROOT := $(shell tinygo env TINYGOROOT 2>/dev/null) FBS_OUT := frontend/src/proto/galaxy/fbs -FBS_INPUTS := ../pkg/schema/fbs/common.fbs ../pkg/schema/fbs/lobby.fbs ../pkg/schema/fbs/user.fbs ../pkg/schema/fbs/report.fbs ../pkg/schema/fbs/order.fbs ../pkg/schema/fbs/diplomail.fbs +FBS_INPUTS := ../pkg/schema/fbs/common.fbs ../pkg/schema/fbs/lobby.fbs ../pkg/schema/fbs/user.fbs ../pkg/schema/fbs/report.fbs ../pkg/schema/fbs/order.fbs ../pkg/schema/fbs/diplomail.fbs ../pkg/schema/fbs/battle.fbs help: @echo "ui targets:" diff --git a/ui/frontend/src/api/battle-fetch.ts b/ui/frontend/src/api/battle-fetch.ts index 154c67a..fa18a56 100644 --- a/ui/frontend/src/api/battle-fetch.ts +++ b/ui/frontend/src/api/battle-fetch.ts @@ -1,21 +1,33 @@ // Battle-report fetcher used by the Battle Viewer page. // -// Phase 27 ships the BattleViewer as a logically isolated component -// that accepts a `BattleReport` matching `pkg/model/report/battle.go`. -// This module owns the type mirror and a single `fetchBattle` entry -// point. In synthetic mode (development & e2e fixtures), the loader -// falls back to a local fixture so the UI tests don't depend on a -// running engine; otherwise it issues a real `GET` against the -// backend gateway route added in Phase 27 step 3. +// Phase 28 migrates this surface off the raw REST passthrough onto the +// `user.games.battle` ConnectRPC command — the same signed envelope the +// other authenticated traffic rides. The synthetic-mode short-circuit +// stays so DEV / e2e tests can render fixtures without a live gateway. +import { Builder, ByteBuffer } from "flatbuffers"; + +import type { GalaxyClient } from "./galaxy-client"; +import { uuidToHiLo } from "./game-state"; import { isSyntheticGameId } from "./synthetic-report"; import { lookupSyntheticBattle } from "./synthetic-battle"; +import { + BattleActionReport as FbsBattleActionReport, + BattleReport as FbsBattleReport, + BattleReportGroup as FbsBattleReportGroup, + GameBattleRequest, + RaceEntry, + ShipEntry, + UUID, +} from "../proto/galaxy/fbs/battle"; +import { ErrorResponse as FbsErrorResponse } from "../proto/galaxy/fbs/lobby"; /** - * BattleReport is the wire shape returned by the engine endpoint - * `GET /api/v1/battle/:turn/:uuid` and forwarded by the backend - * gateway as `GET /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}`. - * Fields mirror `pkg/model/report/battle.go`. + * BattleReport mirrors the on-wire battle shape the BattleViewer + * renders. Fields match `pkg/model/report/battle.go`; integer-keyed + * maps from the underlying model are surfaced as string-keyed + * `Record`s so the existing components (race / ship lookup, mass + * scaling, timeline) keep their current types. */ export interface BattleReport { id: string; @@ -46,20 +58,28 @@ export interface BattleActionReport { } export class BattleFetchError extends Error { - constructor(public readonly status: number, message: string) { + constructor( + public readonly status: number, + message: string, + ) { super(message); this.name = "BattleFetchError"; } } +const MESSAGE_TYPE = "user.games.battle"; +const RESULT_CODE_OK = "ok"; + /** * fetchBattle returns the `BattleReport` for the supplied game, turn, * and battle id. In synthetic-report mode (DEV / e2e) the lookup is * served from `synthetic-battle.ts`; otherwise the function calls the - * backend gateway route. Throws `BattleFetchError` with the upstream - * status on validation or transport failure. + * `user.games.battle` ConnectRPC command through the supplied + * `GalaxyClient`. Throws `BattleFetchError` with the upstream HTTP + * status (or `0` for transport-level failures) on error. */ export async function fetchBattle( + client: GalaxyClient, gameId: string, turn: number, battleId: string, @@ -71,18 +91,171 @@ export async function fetchBattle( } return fixture; } - const path = `/api/v1/user/games/${encodeURIComponent(gameId)}/battles/${turn}/${encodeURIComponent(battleId)}`; - const response = await fetch(path, { - headers: { Accept: "application/json" }, - }); - if (response.status === 404) { - throw new BattleFetchError(404, "battle not found"); + + const payload = encodeRequest(gameId, turn, battleId); + const result = await client.executeCommand(MESSAGE_TYPE, payload); + if (result.resultCode !== RESULT_CODE_OK) { + throw decodeError(result.resultCode, result.payloadBytes); } - if (!response.ok) { - throw new BattleFetchError( - response.status, - `battle fetch failed: ${response.status}`, - ); - } - return (await response.json()) as BattleReport; + return decodeBattleReport(result.payloadBytes); +} + +function encodeRequest( + gameId: string, + turn: number, + battleId: string, +): Uint8Array { + const builder = new Builder(96); + const [gameHi, gameLo] = uuidToHiLo(gameId); + const [battleHi, battleLo] = uuidToHiLo(battleId); + GameBattleRequest.startGameBattleRequest(builder); + GameBattleRequest.addGameId( + builder, + UUID.createUUID(builder, gameHi, gameLo), + ); + GameBattleRequest.addTurn(builder, turn); + GameBattleRequest.addBattleId( + builder, + UUID.createUUID(builder, battleHi, battleLo), + ); + builder.finish(GameBattleRequest.endGameBattleRequest(builder)); + return builder.asUint8Array(); +} + +function decodeError(resultCode: string, payload: Uint8Array): BattleFetchError { + let message = resultCode; + try { + const errorResponse = FbsErrorResponse.getRootAsErrorResponse( + new ByteBuffer(payload), + ); + const body = errorResponse.error(); + if (body) { + message = body.message() ?? resultCode; + } + } catch (_err) { + // fall through to the raw result code + } + const status = mapResultCodeToStatus(resultCode); + return new BattleFetchError(status, message); +} + +function mapResultCodeToStatus(resultCode: string): number { + switch (resultCode) { + case "not_found": + return 404; + case "invalid_request": + return 400; + case "forbidden": + return 403; + case "conflict": + return 409; + case "service_unavailable": + return 503; + default: + return 500; + } +} + +function decodeBattleReport(bytes: Uint8Array): BattleReport { + const fb = FbsBattleReport.getRootAsBattleReport(new ByteBuffer(bytes)); + const id = uuidStringFromFB(fb.id()); + if (id === null) { + throw new BattleFetchError(500, "battle response missing id"); + } + return { + id, + planet: Number(fb.planet()), + planetName: fb.planetName() ?? "", + races: decodeRaces(fb), + ships: decodeShips(fb), + protocol: decodeProtocol(fb), + }; +} + +function decodeRaces(fb: FbsBattleReport): Record { + const out: Record = {}; + const total = fb.racesLength(); + const item = new RaceEntry(); + for (let i = 0; i < total; i++) { + if (!fb.races(i, item)) continue; + const valueUUID = item.value(); + const value = uuidStringFromFB(valueUUID); + if (value === null) continue; + out[item.key().toString()] = value; + } + return out; +} + +function decodeShips(fb: FbsBattleReport): Record { + const out: Record = {}; + const total = fb.shipsLength(); + const entry = new ShipEntry(); + for (let i = 0; i < total; i++) { + if (!fb.ships(i, entry)) continue; + const group = entry.value(); + if (group === null) continue; + out[entry.key().toString()] = decodeGroup(group); + } + return out; +} + +function decodeGroup(group: FbsBattleReportGroup): BattleReportGroup { + const tech: Record = {}; + const techLen = group.techLength(); + for (let i = 0; i < techLen; i++) { + const t = group.tech(i); + if (!t) continue; + const key = t.key(); + if (key === null) continue; + tech[key] = t.value(); + } + return { + race: (group.race() ?? "") as string, + className: (group.className() ?? "") as string, + tech, + num: Number(group.number()), + numLeft: Number(group.numberLeft()), + loadType: (group.loadType() ?? "") as string, + loadQuantity: group.loadQuantity(), + inBattle: group.inBattle(), + }; +} + +function decodeProtocol(fb: FbsBattleReport): BattleActionReport[] { + const out: BattleActionReport[] = []; + const total = fb.protocolLength(); + const item = new FbsBattleActionReport(); + for (let i = 0; i < total; i++) { + if (!fb.protocol(i, item)) continue; + out.push({ + a: Number(item.attacker()), + sa: Number(item.attackerShipClass()), + d: Number(item.defender()), + sd: Number(item.defenderShipClass()), + x: item.destroyed(), + }); + } + return out; +} + +function uuidStringFromFB(uuid: UUID | null): string | null { + if (uuid === null) return null; + const hi = uuid.hi(); + const lo = uuid.lo(); + const hex = bigUintTo16Hex(hi) + bigUintTo16Hex(lo); + return ( + hex.slice(0, 8) + + "-" + + hex.slice(8, 12) + + "-" + + hex.slice(12, 16) + + "-" + + hex.slice(16, 20) + + "-" + + hex.slice(20, 32) + ); +} + +function bigUintTo16Hex(value: bigint): string { + return value.toString(16).padStart(16, "0"); } diff --git a/ui/frontend/src/lib/active-view/battle.svelte b/ui/frontend/src/lib/active-view/battle.svelte index 7393ece..8b9448c 100644 --- a/ui/frontend/src/lib/active-view/battle.svelte +++ b/ui/frontend/src/lib/active-view/battle.svelte @@ -25,6 +25,10 @@ viewer keeps its prop-driven contract. RENDERED_REPORT_CONTEXT_KEY, type RenderedReportSource, } from "$lib/rendered-report.svelte"; + import { + GALAXY_CLIENT_CONTEXT_KEY, + type GalaxyClientHandle, + } from "$lib/galaxy-client-context.svelte"; import { MapShipClassLookup, type ShipClassLookup, @@ -46,6 +50,9 @@ viewer keeps its prop-driven contract. const rendered = getContext( RENDERED_REPORT_CONTEXT_KEY, ); + const galaxyClient = getContext( + GALAXY_CLIENT_CONTEXT_KEY, + ); const shipClassLookup = $derived.by(() => { const map = new Map(); @@ -85,8 +92,16 @@ viewer keeps its prop-driven contract. state = { kind: "not_found" }; return; } + const client = galaxyClient?.client ?? null; + if (!client) { + // Layout populates the client after the boot Promise.all + // resolves; stay in `loading` so the effect re-runs once + // the handle becomes non-null. + state = { kind: "loading" }; + return; + } state = { kind: "loading" }; - fetchBattle(gameId, turn, battleId) + fetchBattle(client, gameId, turn, battleId) .then((report) => { state = { kind: "ready", report }; }) diff --git a/ui/frontend/src/proto/galaxy/fbs/battle.ts b/ui/frontend/src/proto/galaxy/fbs/battle.ts new file mode 100644 index 0000000..7262f42 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/battle.ts @@ -0,0 +1,12 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +export { BattleActionReport, BattleActionReportT } from './battle/battle-action-report.js'; +export { BattleReport, BattleReportT } from './battle/battle-report.js'; +export { BattleReportGroup, BattleReportGroupT } from './battle/battle-report-group.js'; +export { GameBattleRequest, GameBattleRequestT } from './battle/game-battle-request.js'; +export { RaceEntry, RaceEntryT } from './battle/race-entry.js'; +export { ShipEntry, ShipEntryT } from './battle/ship-entry.js'; +export { TechEntry, TechEntryT } from './battle/tech-entry.js'; +export { UUID, UUIDT } from './battle/uuid.js'; diff --git a/ui/frontend/src/proto/galaxy/fbs/battle/battle-action-report.ts b/ui/frontend/src/proto/galaxy/fbs/battle/battle-action-report.ts new file mode 100644 index 0000000..2fd62b2 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/battle/battle-action-report.ts @@ -0,0 +1,130 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + + + +export class BattleActionReport implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):BattleActionReport { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsBattleActionReport(bb:flatbuffers.ByteBuffer, obj?:BattleActionReport):BattleActionReport { + return (obj || new BattleActionReport()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsBattleActionReport(bb:flatbuffers.ByteBuffer, obj?:BattleActionReport):BattleActionReport { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new BattleActionReport()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +attacker():bigint { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0'); +} + +attackerShipClass():bigint { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0'); +} + +defender():bigint { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0'); +} + +defenderShipClass():bigint { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0'); +} + +destroyed():boolean { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +static startBattleActionReport(builder:flatbuffers.Builder) { + builder.startObject(5); +} + +static addAttacker(builder:flatbuffers.Builder, attacker:bigint) { + builder.addFieldInt64(0, attacker, BigInt('0')); +} + +static addAttackerShipClass(builder:flatbuffers.Builder, attackerShipClass:bigint) { + builder.addFieldInt64(1, attackerShipClass, BigInt('0')); +} + +static addDefender(builder:flatbuffers.Builder, defender:bigint) { + builder.addFieldInt64(2, defender, BigInt('0')); +} + +static addDefenderShipClass(builder:flatbuffers.Builder, defenderShipClass:bigint) { + builder.addFieldInt64(3, defenderShipClass, BigInt('0')); +} + +static addDestroyed(builder:flatbuffers.Builder, destroyed:boolean) { + builder.addFieldInt8(4, +destroyed, +false); +} + +static endBattleActionReport(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createBattleActionReport(builder:flatbuffers.Builder, attacker:bigint, attackerShipClass:bigint, defender:bigint, defenderShipClass:bigint, destroyed:boolean):flatbuffers.Offset { + BattleActionReport.startBattleActionReport(builder); + BattleActionReport.addAttacker(builder, attacker); + BattleActionReport.addAttackerShipClass(builder, attackerShipClass); + BattleActionReport.addDefender(builder, defender); + BattleActionReport.addDefenderShipClass(builder, defenderShipClass); + BattleActionReport.addDestroyed(builder, destroyed); + return BattleActionReport.endBattleActionReport(builder); +} + +unpack(): BattleActionReportT { + return new BattleActionReportT( + this.attacker(), + this.attackerShipClass(), + this.defender(), + this.defenderShipClass(), + this.destroyed() + ); +} + + +unpackTo(_o: BattleActionReportT): void { + _o.attacker = this.attacker(); + _o.attackerShipClass = this.attackerShipClass(); + _o.defender = this.defender(); + _o.defenderShipClass = this.defenderShipClass(); + _o.destroyed = this.destroyed(); +} +} + +export class BattleActionReportT implements flatbuffers.IGeneratedObject { +constructor( + public attacker: bigint = BigInt('0'), + public attackerShipClass: bigint = BigInt('0'), + public defender: bigint = BigInt('0'), + public defenderShipClass: bigint = BigInt('0'), + public destroyed: boolean = false +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + return BattleActionReport.createBattleActionReport(builder, + this.attacker, + this.attackerShipClass, + this.defender, + this.defenderShipClass, + this.destroyed + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/battle/battle-report-group.ts b/ui/frontend/src/proto/galaxy/fbs/battle/battle-report-group.ts new file mode 100644 index 0000000..fe7c001 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/battle/battle-report-group.ts @@ -0,0 +1,201 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { TechEntry, TechEntryT } from '../battle/tech-entry.js'; + + +export class BattleReportGroup implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):BattleReportGroup { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsBattleReportGroup(bb:flatbuffers.ByteBuffer, obj?:BattleReportGroup):BattleReportGroup { + return (obj || new BattleReportGroup()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsBattleReportGroup(bb:flatbuffers.ByteBuffer, obj?:BattleReportGroup):BattleReportGroup { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new BattleReportGroup()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +inBattle():boolean { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false; +} + +number():bigint { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0'); +} + +numberLeft():bigint { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0'); +} + +loadQuantity():number { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.readFloat32(this.bb_pos + offset) : 0.0; +} + +tech(index: number, obj?:TechEntry):TechEntry|null { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? (obj || new TechEntry()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null; +} + +techLength():number { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +} + +race():string|null +race(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +race(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +className():string|null +className(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +className(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 16); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +loadType():string|null +loadType(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +loadType(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 18); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +static startBattleReportGroup(builder:flatbuffers.Builder) { + builder.startObject(8); +} + +static addInBattle(builder:flatbuffers.Builder, inBattle:boolean) { + builder.addFieldInt8(0, +inBattle, +false); +} + +static addNumber(builder:flatbuffers.Builder, number:bigint) { + builder.addFieldInt64(1, number, BigInt('0')); +} + +static addNumberLeft(builder:flatbuffers.Builder, numberLeft:bigint) { + builder.addFieldInt64(2, numberLeft, BigInt('0')); +} + +static addLoadQuantity(builder:flatbuffers.Builder, loadQuantity:number) { + builder.addFieldFloat32(3, loadQuantity, 0.0); +} + +static addTech(builder:flatbuffers.Builder, techOffset:flatbuffers.Offset) { + builder.addFieldOffset(4, techOffset, 0); +} + +static createTechVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset { + builder.startVector(4, data.length, 4); + for (let i = data.length - 1; i >= 0; i--) { + builder.addOffset(data[i]!); + } + return builder.endVector(); +} + +static startTechVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(4, numElems, 4); +} + +static addRace(builder:flatbuffers.Builder, raceOffset:flatbuffers.Offset) { + builder.addFieldOffset(5, raceOffset, 0); +} + +static addClassName(builder:flatbuffers.Builder, classNameOffset:flatbuffers.Offset) { + builder.addFieldOffset(6, classNameOffset, 0); +} + +static addLoadType(builder:flatbuffers.Builder, loadTypeOffset:flatbuffers.Offset) { + builder.addFieldOffset(7, loadTypeOffset, 0); +} + +static endBattleReportGroup(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createBattleReportGroup(builder:flatbuffers.Builder, inBattle:boolean, number:bigint, numberLeft:bigint, loadQuantity:number, techOffset:flatbuffers.Offset, raceOffset:flatbuffers.Offset, classNameOffset:flatbuffers.Offset, loadTypeOffset:flatbuffers.Offset):flatbuffers.Offset { + BattleReportGroup.startBattleReportGroup(builder); + BattleReportGroup.addInBattle(builder, inBattle); + BattleReportGroup.addNumber(builder, number); + BattleReportGroup.addNumberLeft(builder, numberLeft); + BattleReportGroup.addLoadQuantity(builder, loadQuantity); + BattleReportGroup.addTech(builder, techOffset); + BattleReportGroup.addRace(builder, raceOffset); + BattleReportGroup.addClassName(builder, classNameOffset); + BattleReportGroup.addLoadType(builder, loadTypeOffset); + return BattleReportGroup.endBattleReportGroup(builder); +} + +unpack(): BattleReportGroupT { + return new BattleReportGroupT( + this.inBattle(), + this.number(), + this.numberLeft(), + this.loadQuantity(), + this.bb!.createObjList(this.tech.bind(this), this.techLength()), + this.race(), + this.className(), + this.loadType() + ); +} + + +unpackTo(_o: BattleReportGroupT): void { + _o.inBattle = this.inBattle(); + _o.number = this.number(); + _o.numberLeft = this.numberLeft(); + _o.loadQuantity = this.loadQuantity(); + _o.tech = this.bb!.createObjList(this.tech.bind(this), this.techLength()); + _o.race = this.race(); + _o.className = this.className(); + _o.loadType = this.loadType(); +} +} + +export class BattleReportGroupT implements flatbuffers.IGeneratedObject { +constructor( + public inBattle: boolean = false, + public number: bigint = BigInt('0'), + public numberLeft: bigint = BigInt('0'), + public loadQuantity: number = 0.0, + public tech: (TechEntryT)[] = [], + public race: string|Uint8Array|null = null, + public className: string|Uint8Array|null = null, + public loadType: string|Uint8Array|null = null +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + const tech = BattleReportGroup.createTechVector(builder, builder.createObjectOffsetList(this.tech)); + const race = (this.race !== null ? builder.createString(this.race!) : 0); + const className = (this.className !== null ? builder.createString(this.className!) : 0); + const loadType = (this.loadType !== null ? builder.createString(this.loadType!) : 0); + + return BattleReportGroup.createBattleReportGroup(builder, + this.inBattle, + this.number, + this.numberLeft, + this.loadQuantity, + tech, + race, + className, + loadType + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/battle/battle-report.ts b/ui/frontend/src/proto/galaxy/fbs/battle/battle-report.ts new file mode 100644 index 0000000..66c5aea --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/battle/battle-report.ts @@ -0,0 +1,215 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { BattleActionReport, BattleActionReportT } from '../battle/battle-action-report.js'; +import { RaceEntry, RaceEntryT } from '../battle/race-entry.js'; +import { ShipEntry, ShipEntryT } from '../battle/ship-entry.js'; +import { UUID, UUIDT } from '../battle/uuid.js'; + + +export class BattleReport implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):BattleReport { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsBattleReport(bb:flatbuffers.ByteBuffer, obj?:BattleReport):BattleReport { + return (obj || new BattleReport()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsBattleReport(bb:flatbuffers.ByteBuffer, obj?:BattleReport):BattleReport { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new BattleReport()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +id(obj?:UUID):UUID|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null; +} + +planet():bigint { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0'); +} + +planetName():string|null +planetName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +planetName(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +races(index: number, obj?:RaceEntry):RaceEntry|null { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? (obj || new RaceEntry()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null; +} + +racesLength():number { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +} + +ships(index: number, obj?:ShipEntry):ShipEntry|null { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? (obj || new ShipEntry()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null; +} + +shipsLength():number { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +} + +protocol(index: number, obj?:BattleActionReport):BattleActionReport|null { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? (obj || new BattleActionReport()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null; +} + +protocolLength():number { + const offset = this.bb!.__offset(this.bb_pos, 14); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +} + +static startBattleReport(builder:flatbuffers.Builder) { + builder.startObject(6); +} + +static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) { + builder.addFieldStruct(0, idOffset, 0); +} + +static addPlanet(builder:flatbuffers.Builder, planet:bigint) { + builder.addFieldInt64(1, planet, BigInt('0')); +} + +static addPlanetName(builder:flatbuffers.Builder, planetNameOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, planetNameOffset, 0); +} + +static addRaces(builder:flatbuffers.Builder, racesOffset:flatbuffers.Offset) { + builder.addFieldOffset(3, racesOffset, 0); +} + +static createRacesVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset { + builder.startVector(4, data.length, 4); + for (let i = data.length - 1; i >= 0; i--) { + builder.addOffset(data[i]!); + } + return builder.endVector(); +} + +static startRacesVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(4, numElems, 4); +} + +static addShips(builder:flatbuffers.Builder, shipsOffset:flatbuffers.Offset) { + builder.addFieldOffset(4, shipsOffset, 0); +} + +static createShipsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset { + builder.startVector(4, data.length, 4); + for (let i = data.length - 1; i >= 0; i--) { + builder.addOffset(data[i]!); + } + return builder.endVector(); +} + +static startShipsVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(4, numElems, 4); +} + +static addProtocol(builder:flatbuffers.Builder, protocolOffset:flatbuffers.Offset) { + builder.addFieldOffset(5, protocolOffset, 0); +} + +static createProtocolVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset { + builder.startVector(4, data.length, 4); + for (let i = data.length - 1; i >= 0; i--) { + builder.addOffset(data[i]!); + } + return builder.endVector(); +} + +static startProtocolVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(4, numElems, 4); +} + +static endBattleReport(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + builder.requiredField(offset, 4) // id + return offset; +} + +static finishBattleReportBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { + builder.finish(offset); +} + +static finishSizePrefixedBattleReportBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) { + builder.finish(offset, undefined, true); +} + +static createBattleReport(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, planet:bigint, planetNameOffset:flatbuffers.Offset, racesOffset:flatbuffers.Offset, shipsOffset:flatbuffers.Offset, protocolOffset:flatbuffers.Offset):flatbuffers.Offset { + BattleReport.startBattleReport(builder); + BattleReport.addId(builder, idOffset); + BattleReport.addPlanet(builder, planet); + BattleReport.addPlanetName(builder, planetNameOffset); + BattleReport.addRaces(builder, racesOffset); + BattleReport.addShips(builder, shipsOffset); + BattleReport.addProtocol(builder, protocolOffset); + return BattleReport.endBattleReport(builder); +} + +unpack(): BattleReportT { + return new BattleReportT( + (this.id() !== null ? this.id()!.unpack() : null), + this.planet(), + this.planetName(), + this.bb!.createObjList(this.races.bind(this), this.racesLength()), + this.bb!.createObjList(this.ships.bind(this), this.shipsLength()), + this.bb!.createObjList(this.protocol.bind(this), this.protocolLength()) + ); +} + + +unpackTo(_o: BattleReportT): void { + _o.id = (this.id() !== null ? this.id()!.unpack() : null); + _o.planet = this.planet(); + _o.planetName = this.planetName(); + _o.races = this.bb!.createObjList(this.races.bind(this), this.racesLength()); + _o.ships = this.bb!.createObjList(this.ships.bind(this), this.shipsLength()); + _o.protocol = this.bb!.createObjList(this.protocol.bind(this), this.protocolLength()); +} +} + +export class BattleReportT implements flatbuffers.IGeneratedObject { +constructor( + public id: UUIDT|null = null, + public planet: bigint = BigInt('0'), + public planetName: string|Uint8Array|null = null, + public races: (RaceEntryT)[] = [], + public ships: (ShipEntryT)[] = [], + public protocol: (BattleActionReportT)[] = [] +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + const planetName = (this.planetName !== null ? builder.createString(this.planetName!) : 0); + const races = BattleReport.createRacesVector(builder, builder.createObjectOffsetList(this.races)); + const ships = BattleReport.createShipsVector(builder, builder.createObjectOffsetList(this.ships)); + const protocol = BattleReport.createProtocolVector(builder, builder.createObjectOffsetList(this.protocol)); + + return BattleReport.createBattleReport(builder, + (this.id !== null ? this.id!.pack(builder) : 0), + this.planet, + planetName, + races, + ships, + protocol + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/battle/game-battle-request.ts b/ui/frontend/src/proto/galaxy/fbs/battle/game-battle-request.ts new file mode 100644 index 0000000..e7aaaaf --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/battle/game-battle-request.ts @@ -0,0 +1,99 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { UUID, UUIDT } from '../battle/uuid.js'; + + +export class GameBattleRequest implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):GameBattleRequest { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsGameBattleRequest(bb:flatbuffers.ByteBuffer, obj?:GameBattleRequest):GameBattleRequest { + return (obj || new GameBattleRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsGameBattleRequest(bb:flatbuffers.ByteBuffer, obj?:GameBattleRequest):GameBattleRequest { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new GameBattleRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +gameId(obj?:UUID):UUID|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null; +} + +turn():number { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0; +} + +battleId(obj?:UUID):UUID|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null; +} + +static startGameBattleRequest(builder:flatbuffers.Builder) { + builder.startObject(3); +} + +static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) { + builder.addFieldStruct(0, gameIdOffset, 0); +} + +static addTurn(builder:flatbuffers.Builder, turn:number) { + builder.addFieldInt32(1, turn, 0); +} + +static addBattleId(builder:flatbuffers.Builder, battleIdOffset:flatbuffers.Offset) { + builder.addFieldStruct(2, battleIdOffset, 0); +} + +static endGameBattleRequest(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + builder.requiredField(offset, 4) // game_id + builder.requiredField(offset, 8) // battle_id + return offset; +} + + +unpack(): GameBattleRequestT { + return new GameBattleRequestT( + (this.gameId() !== null ? this.gameId()!.unpack() : null), + this.turn(), + (this.battleId() !== null ? this.battleId()!.unpack() : null) + ); +} + + +unpackTo(_o: GameBattleRequestT): void { + _o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null); + _o.turn = this.turn(); + _o.battleId = (this.battleId() !== null ? this.battleId()!.unpack() : null); +} +} + +export class GameBattleRequestT implements flatbuffers.IGeneratedObject { +constructor( + public gameId: UUIDT|null = null, + public turn: number = 0, + public battleId: UUIDT|null = null +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + GameBattleRequest.startGameBattleRequest(builder); + GameBattleRequest.addGameId(builder, (this.gameId !== null ? this.gameId!.pack(builder) : 0)); + GameBattleRequest.addTurn(builder, this.turn); + GameBattleRequest.addBattleId(builder, (this.battleId !== null ? this.battleId!.pack(builder) : 0)); + + return GameBattleRequest.endGameBattleRequest(builder); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/battle/race-entry.ts b/ui/frontend/src/proto/galaxy/fbs/battle/race-entry.ts new file mode 100644 index 0000000..2a01dd7 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/battle/race-entry.ts @@ -0,0 +1,85 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { UUID, UUIDT } from '../battle/uuid.js'; + + +export class RaceEntry implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):RaceEntry { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsRaceEntry(bb:flatbuffers.ByteBuffer, obj?:RaceEntry):RaceEntry { + return (obj || new RaceEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsRaceEntry(bb:flatbuffers.ByteBuffer, obj?:RaceEntry):RaceEntry { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new RaceEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +key():bigint { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0'); +} + +value(obj?:UUID):UUID|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null; +} + +static startRaceEntry(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addKey(builder:flatbuffers.Builder, key:bigint) { + builder.addFieldInt64(0, key, BigInt('0')); +} + +static addValue(builder:flatbuffers.Builder, valueOffset:flatbuffers.Offset) { + builder.addFieldStruct(1, valueOffset, 0); +} + +static endRaceEntry(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + builder.requiredField(offset, 6) // value + return offset; +} + + +unpack(): RaceEntryT { + return new RaceEntryT( + this.key(), + (this.value() !== null ? this.value()!.unpack() : null) + ); +} + + +unpackTo(_o: RaceEntryT): void { + _o.key = this.key(); + _o.value = (this.value() !== null ? this.value()!.unpack() : null); +} +} + +export class RaceEntryT implements flatbuffers.IGeneratedObject { +constructor( + public key: bigint = BigInt('0'), + public value: UUIDT|null = null +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + RaceEntry.startRaceEntry(builder); + RaceEntry.addKey(builder, this.key); + RaceEntry.addValue(builder, (this.value !== null ? this.value!.pack(builder) : 0)); + + return RaceEntry.endRaceEntry(builder); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/battle/ship-entry.ts b/ui/frontend/src/proto/galaxy/fbs/battle/ship-entry.ts new file mode 100644 index 0000000..212efec --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/battle/ship-entry.ts @@ -0,0 +1,86 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { BattleReportGroup, BattleReportGroupT } from '../battle/battle-report-group.js'; + + +export class ShipEntry implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):ShipEntry { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsShipEntry(bb:flatbuffers.ByteBuffer, obj?:ShipEntry):ShipEntry { + return (obj || new ShipEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsShipEntry(bb:flatbuffers.ByteBuffer, obj?:ShipEntry):ShipEntry { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new ShipEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +key():bigint { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0'); +} + +value(obj?:BattleReportGroup):BattleReportGroup|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? (obj || new BattleReportGroup()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +static startShipEntry(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addKey(builder:flatbuffers.Builder, key:bigint) { + builder.addFieldInt64(0, key, BigInt('0')); +} + +static addValue(builder:flatbuffers.Builder, valueOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, valueOffset, 0); +} + +static endShipEntry(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + + +unpack(): ShipEntryT { + return new ShipEntryT( + this.key(), + (this.value() !== null ? this.value()!.unpack() : null) + ); +} + + +unpackTo(_o: ShipEntryT): void { + _o.key = this.key(); + _o.value = (this.value() !== null ? this.value()!.unpack() : null); +} +} + +export class ShipEntryT implements flatbuffers.IGeneratedObject { +constructor( + public key: bigint = BigInt('0'), + public value: BattleReportGroupT|null = null +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + const value = (this.value !== null ? this.value!.pack(builder) : 0); + + ShipEntry.startShipEntry(builder); + ShipEntry.addKey(builder, this.key); + ShipEntry.addValue(builder, value); + + return ShipEntry.endShipEntry(builder); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/battle/tech-entry.ts b/ui/frontend/src/proto/galaxy/fbs/battle/tech-entry.ts new file mode 100644 index 0000000..4a06771 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/battle/tech-entry.ts @@ -0,0 +1,92 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + + + +export class TechEntry implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):TechEntry { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsTechEntry(bb:flatbuffers.ByteBuffer, obj?:TechEntry):TechEntry { + return (obj || new TechEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsTechEntry(bb:flatbuffers.ByteBuffer, obj?:TechEntry):TechEntry { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new TechEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +key():string|null +key(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +key(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +value():number { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.readFloat32(this.bb_pos + offset) : 0.0; +} + +static startTechEntry(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addKey(builder:flatbuffers.Builder, keyOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, keyOffset, 0); +} + +static addValue(builder:flatbuffers.Builder, value:number) { + builder.addFieldFloat32(1, value, 0.0); +} + +static endTechEntry(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createTechEntry(builder:flatbuffers.Builder, keyOffset:flatbuffers.Offset, value:number):flatbuffers.Offset { + TechEntry.startTechEntry(builder); + TechEntry.addKey(builder, keyOffset); + TechEntry.addValue(builder, value); + return TechEntry.endTechEntry(builder); +} + +unpack(): TechEntryT { + return new TechEntryT( + this.key(), + this.value() + ); +} + + +unpackTo(_o: TechEntryT): void { + _o.key = this.key(); + _o.value = this.value(); +} +} + +export class TechEntryT implements flatbuffers.IGeneratedObject { +constructor( + public key: string|Uint8Array|null = null, + public value: number = 0.0 +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + const key = (this.key !== null ? builder.createString(this.key!) : 0); + + return TechEntry.createTechEntry(builder, + key, + this.value + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/battle/uuid.ts b/ui/frontend/src/proto/galaxy/fbs/battle/uuid.ts new file mode 100644 index 0000000..70c1894 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/battle/uuid.ts @@ -0,0 +1,65 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + + + +export class UUID implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):UUID { + this.bb_pos = i; + this.bb = bb; + return this; +} + +hi():bigint { + return this.bb!.readUint64(this.bb_pos); +} + +lo():bigint { + return this.bb!.readUint64(this.bb_pos + 8); +} + +static sizeOf():number { + return 16; +} + +static createUUID(builder:flatbuffers.Builder, hi: bigint, lo: bigint):flatbuffers.Offset { + builder.prep(8, 16); + builder.writeInt64(BigInt(lo ?? 0)); + builder.writeInt64(BigInt(hi ?? 0)); + return builder.offset(); +} + + +unpack(): UUIDT { + return new UUIDT( + this.hi(), + this.lo() + ); +} + + +unpackTo(_o: UUIDT): void { + _o.hi = this.hi(); + _o.lo = this.lo(); +} +} + +export class UUIDT implements flatbuffers.IGeneratedObject { +constructor( + public hi: bigint = BigInt('0'), + public lo: bigint = BigInt('0') +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + return UUID.createUUID(builder, + this.hi, + this.lo + ); +} +}