ui/phase-14: rename planet end-to-end + order read-back

Wires the first end-to-end command through the full pipeline:
inspector rename action → local order draft → user.games.order
submit → optimistic overlay on map / inspector → server hydration
on cache miss via the new user.games.order.get message type.

Backend: GET /api/v1/user/games/{id}/orders forwards to engine
GET /api/v1/order. Gateway parses the engine PUT response into the
extended UserGamesOrderResponse FBS envelope and adds
executeUserGamesOrderGet for the read-back path. Frontend ports
ValidateTypeName to TS, lands the inline rename editor + Submit
button, and exposes a renderedReport context so consumers see the
overlay-applied snapshot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-09 11:50:09 +02:00
parent 381e41b325
commit f80c623a74
86 changed files with 7505 additions and 138 deletions
+40
View File
@@ -196,6 +196,46 @@ func (c *Client) PutOrders(ctx context.Context, baseURL string, payload json.Raw
return c.forwardPlayerWrite(ctx, baseURL, pathPlayerOrder, payload, "engine order")
}
// GetOrder calls `GET /api/v1/order?player=<raceName>&turn=<turn>` and
// returns the engine response body verbatim. A `204 No Content` body
// is signalled by `(nil, http.StatusNoContent, nil)` so callers can
// surface "no stored order" without parsing the empty payload.
// Other non-`200` statuses come back wrapped in `ErrEngineValidation`
// (4xx) or `ErrEngineUnreachable` (everything else), matching the
// existing player-write conventions.
func (c *Client) GetOrder(ctx context.Context, baseURL, raceName string, turn int) (json.RawMessage, int, error) {
if err := validateBaseURL(baseURL); err != nil {
return nil, 0, err
}
if strings.TrimSpace(raceName) == "" {
return nil, 0, errors.New("engineclient order get: race name must not be empty")
}
if turn < 0 {
return nil, 0, fmt.Errorf("engineclient order get: turn must not be negative, got %d", turn)
}
values := url.Values{}
values.Set("player", raceName)
values.Set("turn", strconv.Itoa(turn))
target := baseURL + pathPlayerOrder + "?" + values.Encode()
body, status, doErr := c.doRequest(ctx, http.MethodGet, target, nil, c.probeTimeout)
if doErr != nil {
return nil, 0, fmt.Errorf("%w: engine order get: %w", ErrEngineUnreachable, doErr)
}
switch status {
case http.StatusOK:
if len(body) == 0 {
return nil, status, fmt.Errorf("%w: engine order get: empty response body", ErrEngineProtocolViolation)
}
return json.RawMessage(body), status, nil
case http.StatusNoContent:
return nil, status, nil
case http.StatusBadRequest, http.StatusConflict:
return json.RawMessage(body), status, fmt.Errorf("%w: engine order get: %s", ErrEngineValidation, summariseEngineError(body, status))
default:
return nil, status, fmt.Errorf("%w: engine order get: %s", ErrEngineUnreachable, summariseEngineError(body, status))
}
}
// GetReport calls `GET /api/v1/report?player=<raceName>&turn=<turn>`
// and returns the engine response body verbatim.
func (c *Client) GetReport(ctx context.Context, baseURL, raceName string, turn int) (json.RawMessage, error) {
@@ -195,6 +195,68 @@ func TestClientReportsForwardsQuery(t *testing.T) {
}
}
func TestClientGetOrderForwardsQuery(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != pathPlayerOrder {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.Method != http.MethodGet {
t.Fatalf("unexpected method: %s", r.Method)
}
if r.URL.Query().Get("player") != "alpha" {
t.Fatalf("player = %q", r.URL.Query().Get("player"))
}
if r.URL.Query().Get("turn") != "3" {
t.Fatalf("turn = %q", r.URL.Query().Get("turn"))
}
_, _ = w.Write([]byte(`{"game_id":"abc","updatedAt":99,"cmd":[]}`))
}))
t.Cleanup(srv.Close)
cli := newTestClient(t, srv)
body, status, err := cli.GetOrder(context.Background(), srv.URL, "alpha", 3)
if err != nil {
t.Fatalf("GetOrder: %v", err)
}
if status != http.StatusOK {
t.Fatalf("status = %d", status)
}
if !strings.Contains(string(body), `"updatedAt":99`) {
t.Fatalf("body = %s", body)
}
}
func TestClientGetOrderNoContent(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(srv.Close)
cli := newTestClient(t, srv)
body, status, err := cli.GetOrder(context.Background(), srv.URL, "alpha", 3)
if err != nil {
t.Fatalf("GetOrder: %v", err)
}
if status != http.StatusNoContent {
t.Fatalf("status = %d", status)
}
if body != nil {
t.Fatalf("expected nil body on 204, got %s", body)
}
}
func TestClientGetOrderRejectsBadInput(t *testing.T) {
cli := newTestClient(t, httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("server must not be hit on bad input")
})))
if _, _, err := cli.GetOrder(context.Background(), "http://example.com", "", 0); err == nil {
t.Fatal("expected error on empty race name")
}
if _, _, err := cli.GetOrder(context.Background(), "http://example.com", "alpha", -1); err == nil {
t.Fatal("expected error on negative turn")
}
}
func TestClientHealthzSuccess(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != pathHealthz {
+36
View File
@@ -50,6 +50,14 @@ var pathParamStubs = map[string]string{
"turn": "42",
}
// queryParamStubs lists the deterministic substitutions used to fill
// query-string parameters declared in `openapi.yaml`. Every required
// query parameter must have an entry here; optional ones can stay
// blank (the contract test omits them when no stub is registered).
var queryParamStubs = map[string]string{
"turn": "42",
}
// requestBodyStubs lists the JSON request bodies the contract test sends for
// each operationId. Operations missing from the map default to an empty
// object `{}`, which is a valid placeholder thanks to `additionalProperties:
@@ -323,6 +331,9 @@ func buildRequest(t *testing.T, c contractOperation) *http.Request {
t.Helper()
target := substitutePathParams(t, c.path)
if query := buildQuery(t, c); query != "" {
target += "?" + query
}
url := "http://backend.internal" + target
body := bodyFor(t, c)
@@ -376,6 +387,31 @@ func bodyFor(t *testing.T, c contractOperation) requestBody {
}
}
func buildQuery(t *testing.T, c contractOperation) string {
t.Helper()
if c.op == nil {
return ""
}
values := make([]string, 0, len(c.op.Parameters))
for _, p := range c.op.Parameters {
if p == nil || p.Value == nil {
continue
}
if p.Value.In != "query" {
continue
}
stub, ok := queryParamStubs[p.Value.Name]
if !ok {
if p.Value.Required {
t.Fatalf("operation %q requires query parameter %q with no stub registered", c.operationID, p.Value.Name)
}
continue
}
values = append(values, p.Value.Name+"="+stub)
}
return strings.Join(values, "&")
}
func substitutePathParams(t *testing.T, templated string) string {
t.Helper()
@@ -136,6 +136,64 @@ func (h *UserGamesHandlers) Orders() gin.HandlerFunc {
}
}
// GetOrders handles GET /api/v1/user/games/{game_id}/orders?turn=N.
// Forwards to the engine's `GET /api/v1/order` with the player rebound
// from the runtime mapping. The query parameter `turn` is required
// and must be a non-negative integer; the engine itself enforces the
// same rule, but rejecting up-front saves a network hop.
//
// On `204 No Content` the handler answers `204` so the gateway can
// translate the FBS envelope to `found = false`. On `200` the
// engine's body is forwarded verbatim — the gateway re-encodes the
// JSON `UserGamesOrder` shape into FlatBuffers.
func (h *UserGamesHandlers) GetOrders() gin.HandlerFunc {
if h == nil || h.runtime == nil || h.engine == nil {
return handlers.NotImplemented("userGamesGetOrders")
}
return func(c *gin.Context) {
gameID, ok := parseGameIDParam(c)
if !ok {
return
}
turnRaw := c.Query("turn")
if turnRaw == "" {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "turn is required")
return
}
turn, err := strconv.Atoi(turnRaw)
if err != nil || turn < 0 {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "turn must be a non-negative integer")
return
}
userID, ok := userid.FromContext(c.Request.Context())
if !ok {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "user id missing")
return
}
ctx := c.Request.Context()
mapping, err := h.runtime.ResolvePlayerMapping(ctx, gameID, userID)
if err != nil {
respondGameProxyError(c, h.logger, "user games get orders", ctx, err)
return
}
endpoint, err := h.runtime.EngineEndpoint(ctx, gameID)
if err != nil {
respondGameProxyError(c, h.logger, "user games get orders", ctx, err)
return
}
body, status, err := h.engine.GetOrder(ctx, endpoint, mapping.RaceName, turn)
if err != nil {
respondEngineProxyError(c, h.logger, "user games get orders", ctx, body, err)
return
}
if status == http.StatusNoContent {
c.Status(http.StatusNoContent)
return
}
c.Data(http.StatusOK, "application/json", body)
}
}
// Report handles GET /api/v1/user/games/{game_id}/reports/{turn}.
func (h *UserGamesHandlers) Report() gin.HandlerFunc {
if h == nil || h.runtime == nil || h.engine == nil {
+1
View File
@@ -261,6 +261,7 @@ func registerUserRoutes(router *gin.Engine, instruments *metrics.Instruments, de
userGames := group.Group("/games")
userGames.POST("/:game_id/commands", deps.UserGames.Commands())
userGames.POST("/:game_id/orders", deps.UserGames.Orders())
userGames.GET("/:game_id/orders", deps.UserGames.GetOrders())
userGames.GET("/:game_id/reports/:turn", deps.UserGames.Report())
userSessions := group.Group("/sessions")
+45 -1
View File
@@ -1023,7 +1023,11 @@ paths:
$ref: "#/components/schemas/EngineOrder"
responses:
"200":
description: Engine order validation result passed through.
description: |
Engine order validation result passed through. Body is the
engine's `UserGamesOrder` shape — game_id, updatedAt, and
the per-command `cmd[]` list with `cmdApplied` /
`cmdErrorCode` populated by the engine.
content:
application/json:
schema:
@@ -1036,6 +1040,46 @@ paths:
$ref: "#/components/responses/NotImplementedError"
"500":
$ref: "#/components/responses/InternalError"
get:
tags: [User]
operationId: userGamesGetOrders
summary: Read the player's stored order for a turn
description: |
Forwards `GET /api/v1/order` against the engine container.
The caller always knows the current turn from the lobby
record at game boot, so `turn` is required.
security:
- UserHeader: []
parameters:
- $ref: "#/components/parameters/XUserID"
- $ref: "#/components/parameters/GameID"
- name: turn
in: query
required: true
description: Turn number whose stored order to fetch. Non-negative.
schema:
type: integer
format: int32
minimum: 0
responses:
"200":
description: |
Engine returned the stored order for this player + turn.
Body is the engine's `UserGamesOrder` shape.
content:
application/json:
schema:
$ref: "#/components/schemas/PassthroughObject"
"204":
description: No order has been stored for this player on this turn.
"400":
$ref: "#/components/responses/InvalidRequestError"
"404":
$ref: "#/components/responses/NotFoundError"
"501":
$ref: "#/components/responses/NotImplementedError"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/user/games/{game_id}/reports/{turn}:
get:
tags: [User]
+9 -5
View File
@@ -371,11 +371,15 @@ Authenticated client traffic for in-game operations crosses three
serialisation boundaries: signed-gRPC FlatBuffers (client ↔ gateway),
JSON over REST (gateway ↔ backend), and JSON over REST again
(backend ↔ engine). Gateway owns the FB ↔ JSON transcoding for the
three message types `user.games.command`, `user.games.order`,
`user.games.report` (FB schemas in `pkg/schema/fbs/{order,report}`,
encoders in `pkg/transcoder`). Backend never touches FlatBuffers and
never re-interprets the JSON beyond rebinding the actor field from
the runtime player mapping (clients never carry a trusted actor).
four message types `user.games.command`, `user.games.order`,
`user.games.order.get`, `user.games.report` (FB schemas in
`pkg/schema/fbs/{order,report}`, encoders in `pkg/transcoder`).
`user.games.order.get` reads back the player's stored order for a
given turn — paired with the POST `user.games.order` so the client
can hydrate its local draft after a cache loss without re-deriving
from the report. Backend never touches FlatBuffers and never
re-interprets the JSON beyond rebinding the actor field from the
runtime player mapping (clients never carry a trusted actor).
Container state is owned by `backend/internal/runtime`:
+9 -6
View File
@@ -606,13 +606,16 @@ not duplicated here.
### 6.2 Backend's role: pass-through with authorisation
The signed authenticated-edge pipeline for in-game traffic uses three
The signed authenticated-edge pipeline for in-game traffic uses four
message types on the authenticated surface — `user.games.command`,
`user.games.order`, `user.games.report` each with a typed
FlatBuffers payload. Gateway transcodes the FB request into the JSON
shape backend expects, forwards over plain REST to the corresponding
`/api/v1/user/games/{game_id}/*` endpoint, then transcodes the JSON
response back into FB before signing the reply.
`user.games.order`, `user.games.order.get`, `user.games.report`
each with a typed FlatBuffers payload. Gateway transcodes the FB
request into the JSON shape backend expects, forwards over plain
REST to the corresponding `/api/v1/user/games/{game_id}/*` endpoint,
then transcodes the JSON response back into FB before signing the
reply. `user.games.order.get` is the read-back companion to
`user.games.order`: clients use it to hydrate the local order draft
after a cache loss (fresh install, cleared storage, new device).
For every in-game endpoint the user surface acts as an authorised
pass-through to the engine container. Backend:
+9 -4
View File
@@ -624,12 +624,17 @@ Wire-формат команд, приказов и отчётов — собс
### 6.2 Роль backend: pass-through с авторизацией
Подписанный конвейер аутентифицированного edge для in-game-трафика
использует три message types на аутентифицированной поверхности —
`user.games.command`, `user.games.order`, `user.games.report`
у каждого типизированный FlatBuffers-payload. Gateway транскодирует FB-запрос в JSON-форму,
которую ждёт backend, форвардит её REST'ом в соответствующий
использует четыре message types на аутентифицированной поверхности —
`user.games.command`, `user.games.order`, `user.games.order.get`,
`user.games.report`у каждого типизированный FlatBuffers-payload.
Gateway транскодирует FB-запрос в JSON-форму, которую ждёт backend,
форвардит её REST'ом в соответствующий
`/api/v1/user/games/{game_id}/*` endpoint, после чего транскодирует
JSON-ответ обратно в FB перед подписью.
`user.games.order.get` — read-back-компаньон для `user.games.order`:
клиент использует его, чтобы восстановить локальный черновик приказа
после потери кэша (свежая установка, очищенное хранилище, новое
устройство).
Для каждого in-game-endpoint user-surface работает как
авторизующий pass-through к engine-контейнеру. Backend:
@@ -51,6 +51,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.executeUserGamesOrder(ctx, command.UserID, req)
case ordermodel.MessageTypeUserGamesOrderGet:
req, err := transcoder.PayloadToUserGamesOrderGet(command.PayloadBytes)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command %q: %w", command.MessageType, err)
}
return c.executeUserGamesOrderGet(ctx, command.UserID, req)
case reportmodel.MessageTypeUserGamesReport:
req, err := transcoder.PayloadToGameReportRequest(command.PayloadBytes)
if err != nil {
@@ -91,7 +97,22 @@ func (c *RESTClient) executeUserGamesOrder(ctx context.Context, userID string, r
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.order: %w", err)
}
return projectUserGamesAckResponse(status, respBody, transcoder.EmptyUserGamesOrderResponsePayload)
return projectUserGamesOrderResponse(status, respBody)
}
func (c *RESTClient) executeUserGamesOrderGet(ctx context.Context, userID string, req *ordermodel.UserGamesOrderGet) (downstream.UnaryResult, error) {
if req.GameID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.order.get: game_id must not be empty")
}
if req.Turn < 0 {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.order.get: turn must be non-negative, got %d", req.Turn)
}
target := fmt.Sprintf("%s/api/v1/user/games/%s/orders?turn=%d", c.baseURL, url.PathEscape(req.GameID.String()), req.Turn)
respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.order.get: %w", err)
}
return projectUserGamesOrderGetResponse(status, respBody)
}
func (c *RESTClient) executeUserGamesReport(ctx context.Context, userID string, req *reportmodel.GameReportRequest) (downstream.UnaryResult, error) {
@@ -122,10 +143,10 @@ func buildEngineCommandBody(commands []ordermodel.DecodableCommand) (gamerest.Co
return gamerest.Command{Actor: "", Commands: raw}, nil
}
// projectUserGamesAckResponse turns a backend response for command /
// order routes into a UnaryResult. Engine returns 204 on success, so
// any 2xx status is treated as ok and answered with the empty typed
// FB envelope produced by ackBuilder.
// projectUserGamesAckResponse turns a backend response for the
// `user.games.command` route into a UnaryResult. Engine returns 204
// on success, so any 2xx status is treated as ok and answered with
// the empty typed FB envelope produced by ackBuilder.
func projectUserGamesAckResponse(statusCode int, payload []byte, ackBuilder func() []byte) (downstream.UnaryResult, error) {
switch {
case statusCode >= 200 && statusCode < 300:
@@ -142,6 +163,79 @@ func projectUserGamesAckResponse(statusCode int, payload []byte, ackBuilder func
}
}
// projectUserGamesOrderResponse decodes the engine's `PUT /api/v1/order`
// JSON body (forwarded by backend) and re-encodes it as a FlatBuffers
// `UserGamesOrderResponse` envelope. The body carries per-command
// `cmdApplied` / `cmdErrorCode` plus the engine-assigned `updatedAt`,
// all of which round-trip into FB unchanged. An empty body falls back
// to a typed empty envelope so the gateway can ack a successful but
// unstructured 2xx without surfacing an error.
func projectUserGamesOrderResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
switch {
case statusCode >= 200 && statusCode < 300:
var parsed *ordermodel.UserGamesOrder
if len(payload) > 0 {
decoded, jsonErr := transcoder.JSONToUserGamesOrder(payload)
if jsonErr != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode engine order response: %w", jsonErr)
}
parsed = decoded
}
encoded, err := transcoder.UserGamesOrderResponseToPayload(parsed)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("encode order response 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)
}
}
// projectUserGamesOrderGetResponse decodes the engine's
// `GET /api/v1/order` JSON body and re-encodes it as a FlatBuffers
// `UserGamesOrderGetResponse` envelope. A `204 No Content` from the
// engine surfaces as `found = false` with no embedded order; `200`
// surfaces as `found = true` with the decoded order.
func projectUserGamesOrderGetResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
switch {
case statusCode == http.StatusNoContent:
encoded, err := transcoder.UserGamesOrderGetResponseToPayload(nil, false)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("encode order get response payload: %w", err)
}
return downstream.UnaryResult{
ResultCode: userCommandResultCodeOK,
PayloadBytes: encoded,
}, nil
case statusCode >= 200 && statusCode < 300:
decoded, err := transcoder.JSONToUserGamesOrder(payload)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode engine order get response: %w", err)
}
encoded, err := transcoder.UserGamesOrderGetResponseToPayload(decoded, true)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("encode order get response 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)
}
}
// projectUserGamesReportResponse decodes the engine's Report JSON
// payload (forwarded verbatim by backend) and re-encodes it as a
// FlatBuffers Report for the signed-gRPC client.
@@ -0,0 +1,187 @@
package backendclient_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"galaxy/gateway/internal/backendclient"
"galaxy/gateway/internal/downstream"
ordermodel "galaxy/model/order"
"galaxy/transcoder"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExecuteUserGamesOrderForwardsAndDecodesResponse(t *testing.T) {
t.Parallel()
gameID := uuid.MustParse("11111111-2222-3333-4444-555555555555")
applied := true
source := &ordermodel.UserGamesOrder{
GameID: gameID,
Commands: []ordermodel.DecodableCommand{
&ordermodel.CommandPlanetRename{
CommandMeta: ordermodel.CommandMeta{
CmdType: ordermodel.CommandTypePlanetRename,
CmdID: "00000000-0000-0000-0000-00000000aaaa",
},
Number: 7,
Name: "alpha",
},
},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/orders", r.URL.Path)
require.Equal(t, "user-1", r.Header.Get(backendclient.HeaderUserID))
writeJSON(t, w, http.StatusAccepted, map[string]any{
"game_id": gameID.String(),
"updatedAt": int64(99),
"cmd": []map[string]any{{
"@type": string(ordermodel.CommandTypePlanetRename),
"cmdId": "00000000-0000-0000-0000-00000000aaaa",
"cmdApplied": applied,
"planetNumber": 7,
"name": "alpha",
}},
})
}))
t.Cleanup(server.Close)
client := newRESTClient(t, server)
payload, err := transcoder.UserGamesOrderToPayload(source)
require.NoError(t, err)
cmd := newAuthCommand(t, ordermodel.MessageTypeUserGamesOrder, payload)
result, err := client.ExecuteGameCommand(context.Background(), cmd)
require.NoError(t, err)
assert.Equal(t, "ok", result.ResultCode)
decoded, err := transcoder.PayloadToUserGamesOrderResponse(result.PayloadBytes)
require.NoError(t, err)
require.NotNil(t, decoded)
assert.Equal(t, gameID, decoded.GameID)
assert.Equal(t, int64(99), decoded.UpdatedAt)
require.Len(t, decoded.Commands, 1)
rename, ok := ordermodel.AsCommand[*ordermodel.CommandPlanetRename](decoded.Commands[0])
require.True(t, ok)
assert.Equal(t, "00000000-0000-0000-0000-00000000aaaa", rename.CmdID)
require.NotNil(t, rename.CmdApplied)
assert.True(t, *rename.CmdApplied)
}
func TestExecuteUserGamesOrderGetReturnsStored(t *testing.T) {
t.Parallel()
gameID := uuid.MustParse("22222222-3333-4444-5555-666666666666")
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()+"/orders", r.URL.Path)
require.Equal(t, "5", r.URL.Query().Get("turn"))
writeJSON(t, w, http.StatusOK, map[string]any{
"game_id": gameID.String(),
"updatedAt": int64(42),
"cmd": []map[string]any{{
"@type": string(ordermodel.CommandTypePlanetRename),
"cmdId": "00000000-0000-0000-0000-00000000bbbb",
"planetNumber": 9,
"name": "stored",
}},
})
}))
t.Cleanup(server.Close)
client := newRESTClient(t, server)
payload, err := transcoder.UserGamesOrderGetToPayload(&ordermodel.UserGamesOrderGet{GameID: gameID, Turn: 5})
require.NoError(t, err)
result, err := client.ExecuteGameCommand(context.Background(), newAuthCommand(t, ordermodel.MessageTypeUserGamesOrderGet, payload))
require.NoError(t, err)
assert.Equal(t, "ok", result.ResultCode)
stored, found, err := transcoder.PayloadToUserGamesOrderGetResponse(result.PayloadBytes)
require.NoError(t, err)
require.True(t, found)
require.NotNil(t, stored)
assert.Equal(t, gameID, stored.GameID)
assert.Equal(t, int64(42), stored.UpdatedAt)
require.Len(t, stored.Commands, 1)
rename, ok := ordermodel.AsCommand[*ordermodel.CommandPlanetRename](stored.Commands[0])
require.True(t, ok)
assert.Equal(t, 9, rename.Number)
assert.Equal(t, "stored", rename.Name)
}
func TestExecuteUserGamesOrderGetMapsNoContent(t *testing.T) {
t.Parallel()
gameID := uuid.MustParse("33333333-4444-5555-6666-777777777777")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "11", r.URL.Query().Get("turn"))
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(server.Close)
client := newRESTClient(t, server)
payload, err := transcoder.UserGamesOrderGetToPayload(&ordermodel.UserGamesOrderGet{GameID: gameID, Turn: 11})
require.NoError(t, err)
result, err := client.ExecuteGameCommand(context.Background(), newAuthCommand(t, ordermodel.MessageTypeUserGamesOrderGet, payload))
require.NoError(t, err)
assert.Equal(t, "ok", result.ResultCode)
stored, found, err := transcoder.PayloadToUserGamesOrderGetResponse(result.PayloadBytes)
require.NoError(t, err)
assert.False(t, found)
assert.Nil(t, stored)
}
func TestExecuteUserGamesOrderGetRejectsNegativeTurn(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("server must not be hit on negative turn")
}))
t.Cleanup(server.Close)
client := newRESTClient(t, server)
gameID := uuid.MustParse("44444444-5555-6666-7777-888888888888")
// PayloadToUserGamesOrderGet rejects negative turns at decode
// time; force the negative case by hand-crafting a payload via
// the encoder set to 0 then mutating the buffer is fragile, so
// instead exercise the encoder's own non-negative check.
_, err := transcoder.UserGamesOrderGetToPayload(&ordermodel.UserGamesOrderGet{GameID: gameID, Turn: -1})
require.Error(t, err)
// And verify the dispatch path also surfaces the encoder error
// when wrapping a manually-signed envelope: the request payload
// is empty so the decoder reports "data is empty", which the
// dispatcher wraps with the message-type prefix.
_, err = client.ExecuteGameCommand(context.Background(), downstream.AuthenticatedCommand{
MessageType: ordermodel.MessageTypeUserGamesOrderGet,
PayloadBytes: nil,
UserID: "user-1",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "user.games.order.get")
}
// 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.
//
// TODO(phase14): collapse the two writeJSON copies once the package
// gains a shared `helpers_test.go`. Phase 14 keeps the duplicate to
// avoid touching unrelated tests.
var _ = json.Marshal // keep encoding/json import if writeJSON is hoisted
func init() {
// Sanity-check that the package-level writeJSON helper is
// declared by another _test.go file we depend on; if a future
// refactor removes it, this test file will not compile.
_ = strings.TrimSpace
}
+4 -3
View File
@@ -60,9 +60,10 @@ func GameRoutes(client *RESTClient) map[string]downstream.Client {
target = gameCommandClient{rest: client}
}
return map[string]downstream.Client{
ordermodel.MessageTypeUserGamesCommand: target,
ordermodel.MessageTypeUserGamesOrder: target,
reportmodel.MessageTypeUserGamesReport: target,
ordermodel.MessageTypeUserGamesCommand: target,
ordermodel.MessageTypeUserGamesOrder: target,
ordermodel.MessageTypeUserGamesOrderGet: target,
reportmodel.MessageTypeUserGamesReport: target,
}
}
+21
View File
@@ -18,6 +18,12 @@ const MessageTypeUserGamesCommand = "user.games.command"
// FlatBuffers `order.UserGamesOrder`.
const MessageTypeUserGamesOrder = "user.games.order"
// MessageTypeUserGamesOrderGet is the authenticated gateway message
// type used to read back the player's stored order for a given turn
// through `GET /api/v1/user/games/{game_id}/orders?turn=N`. The
// signed payload is a FlatBuffers `order.UserGamesOrderGet`.
const MessageTypeUserGamesOrderGet = "user.games.order.get"
// UserGamesCommand is the typed payload of MessageTypeUserGamesCommand.
// `GameID` selects the running engine container; `Commands` is the
// player command batch executed atomically by the engine. The `Actor`
@@ -54,6 +60,21 @@ func (o *UserGamesOrder) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, o)
}
// UserGamesOrderGet is the typed payload of
// MessageTypeUserGamesOrderGet. `Turn` is mandatory and must be
// non-negative; the caller pulls it from the lobby record at game
// boot. Backend rebinds the player from the runtime player mapping
// before forwarding to the engine.
type UserGamesOrderGet struct {
// GameID identifies the running game whose order is being
// read back.
GameID uuid.UUID `json:"game_id"`
// Turn selects the turn the stored order belongs to. Negative
// values are invalid.
Turn int `json:"turn"`
}
func AsCommand[E DecodableCommand](c DecodableCommand) (result E, ok bool) {
if v, ok := c.(E); ok {
return v, true
+9 -2
View File
@@ -2,8 +2,15 @@
## Generating sources
Given a `.fbs` file, source code can be generated using `flatc` command:
Given a `.fbs` file, source code can be generated using `flatc` from
this directory:
```shell
flatc --go {file}.fbs
flatc --go --go-module-name galaxy/schema/fbs {file}.fbs
```
The `--go-module-name` flag rewrites cross-namespace imports to the
fully-qualified module path (e.g. `common "galaxy/schema/fbs/common"`)
so the generated code links inside this Go module without local
replace directives. Omitting the flag yields imports such as
`common "common"` which fail to resolve.
+28 -3
View File
@@ -220,6 +220,31 @@ table UserGamesOrder {
// — kept as a typed envelope for future extension.
table UserGamesCommandResponse {}
// UserGamesOrderResponse is the success acknowledgement returned for
// `MessageTypeUserGamesOrder`. Mirrors `UserGamesCommandResponse`.
table UserGamesOrderResponse {}
// UserGamesOrderResponse mirrors the engine's `PUT /api/v1/order`
// success body: it echoes the stored order back to the caller with
// the engine-assigned `updated_at` timestamp and per-command
// `cmd_applied` / `cmd_error_code` populated on every entry.
table UserGamesOrderResponse {
game_id: common.UUID;
updated_at: int64;
commands: [CommandItem];
}
// UserGamesOrderGet is the signed-gRPC request payload for
// `MessageTypeUserGamesOrderGet`. Fetches the player's stored order
// for the given turn — the caller always knows the current turn from
// the lobby record so `turn` is required and must be non-negative.
table UserGamesOrderGet {
game_id: common.UUID (required);
turn: int64;
}
// UserGamesOrderGetResponse carries the result of
// `MessageTypeUserGamesOrderGet`. `found = false` is how the FBS
// envelope conveys the engine's `204 No Content` (no order stored
// for this player on this turn). When `found = true`, `order` is
// the engine's stored order for the turn.
table UserGamesOrderGetResponse {
found: bool;
order: UserGamesOrder;
}
+82
View File
@@ -0,0 +1,82 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package order
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type UserGamesOrderGet struct {
_tab flatbuffers.Table
}
func GetRootAsUserGamesOrderGet(buf []byte, offset flatbuffers.UOffsetT) *UserGamesOrderGet {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &UserGamesOrderGet{}
x.Init(buf, n+offset)
return x
}
func FinishUserGamesOrderGetBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsUserGamesOrderGet(buf []byte, offset flatbuffers.UOffsetT) *UserGamesOrderGet {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &UserGamesOrderGet{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedUserGamesOrderGetBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *UserGamesOrderGet) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *UserGamesOrderGet) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *UserGamesOrderGet) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *UserGamesOrderGet) Turn() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *UserGamesOrderGet) MutateTurn(n int64) bool {
return rcv._tab.MutateInt64Slot(6, n)
}
func UserGamesOrderGetStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func UserGamesOrderGetAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func UserGamesOrderGetAddTurn(builder *flatbuffers.Builder, turn int64) {
builder.PrependInt64Slot(1, turn, 0)
}
func UserGamesOrderGetEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -0,0 +1,80 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package order
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type UserGamesOrderGetResponse struct {
_tab flatbuffers.Table
}
func GetRootAsUserGamesOrderGetResponse(buf []byte, offset flatbuffers.UOffsetT) *UserGamesOrderGetResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &UserGamesOrderGetResponse{}
x.Init(buf, n+offset)
return x
}
func FinishUserGamesOrderGetResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsUserGamesOrderGetResponse(buf []byte, offset flatbuffers.UOffsetT) *UserGamesOrderGetResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &UserGamesOrderGetResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedUserGamesOrderGetResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *UserGamesOrderGetResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *UserGamesOrderGetResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *UserGamesOrderGetResponse) Found() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *UserGamesOrderGetResponse) MutateFound(n bool) bool {
return rcv._tab.MutateBoolSlot(4, n)
}
func (rcv *UserGamesOrderGetResponse) Order(obj *UserGamesOrder) *UserGamesOrder {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(UserGamesOrder)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func UserGamesOrderGetResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func UserGamesOrderGetResponseAddFound(builder *flatbuffers.Builder, found bool) {
builder.PrependBoolSlot(0, found, false)
}
func UserGamesOrderGetResponseAddOrder(builder *flatbuffers.Builder, order flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(order), 0)
}
func UserGamesOrderGetResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+60 -1
View File
@@ -4,6 +4,8 @@ package order
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type UserGamesOrderResponse struct {
@@ -41,8 +43,65 @@ func (rcv *UserGamesOrderResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *UserGamesOrderResponse) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *UserGamesOrderResponse) UpdatedAt() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *UserGamesOrderResponse) MutateUpdatedAt(n int64) bool {
return rcv._tab.MutateInt64Slot(6, n)
}
func (rcv *UserGamesOrderResponse) Commands(obj *CommandItem, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *UserGamesOrderResponse) CommandsLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func UserGamesOrderResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(0)
builder.StartObject(3)
}
func UserGamesOrderResponseAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func UserGamesOrderResponseAddUpdatedAt(builder *flatbuffers.Builder, updatedAt int64) {
builder.PrependInt64Slot(1, updatedAt, 0)
}
func UserGamesOrderResponseAddCommands(builder *flatbuffers.Builder, commands flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(commands), 0)
}
func UserGamesOrderResponseStartCommandsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func UserGamesOrderResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
+321 -5
View File
@@ -1,6 +1,7 @@
package transcoder
import (
"encoding/json"
"errors"
"fmt"
@@ -9,8 +10,117 @@ import (
fbs "galaxy/schema/fbs/order"
flatbuffers "github.com/google/flatbuffers/go"
"github.com/google/uuid"
)
// JSONToUserGamesOrder decodes the engine's JSON response body for
// `PUT /api/v1/order` and `GET /api/v1/order` into the typed
// `*model.UserGamesOrder`. The model's `Commands` field is an
// interface (`order.DecodableCommand`), so plain `json.Unmarshal`
// can't reach it — this helper performs the same per-`@type`
// dispatch as `game/internal/repo.ParseOrder`, but stays inside the
// shared transcoder so non-engine callers (the gateway, tests) can
// reuse it without crossing module boundaries.
func JSONToUserGamesOrder(payload []byte) (*model.UserGamesOrder, error) {
if len(payload) == 0 {
return nil, errors.New("decode user games order json: payload is empty")
}
var raw struct {
GameID string `json:"game_id"`
UpdatedAt int64 `json:"updatedAt"`
Commands []json.RawMessage `json:"cmd"`
}
if err := json.Unmarshal(payload, &raw); err != nil {
return nil, fmt.Errorf("decode user games order json: %w", err)
}
out := &model.UserGamesOrder{
UpdatedAt: raw.UpdatedAt,
}
if raw.GameID != "" {
gameID, err := uuid.Parse(raw.GameID)
if err != nil {
return nil, fmt.Errorf("decode user games order json: invalid game_id %q: %w", raw.GameID, err)
}
out.GameID = gameID
}
if len(raw.Commands) == 0 {
return out, nil
}
out.Commands = make([]model.DecodableCommand, len(raw.Commands))
for i, rawCmd := range raw.Commands {
cmd, err := decodeJSONCommand(rawCmd)
if err != nil {
return nil, fmt.Errorf("decode user games order json command %d: %w", i, err)
}
out.Commands[i] = cmd
}
return out, nil
}
func decodeJSONCommand(raw json.RawMessage) (model.DecodableCommand, error) {
meta := new(model.CommandMeta)
if err := json.Unmarshal(raw, meta); err != nil {
return nil, err
}
switch meta.CmdType {
case model.CommandTypeRaceQuit:
return unmarshalJSONCommand(raw, new(model.CommandRaceQuit))
case model.CommandTypeRaceVote:
return unmarshalJSONCommand(raw, new(model.CommandRaceVote))
case model.CommandTypeRaceRelation:
return unmarshalJSONCommand(raw, new(model.CommandRaceRelation))
case model.CommandTypeShipClassCreate:
return unmarshalJSONCommand(raw, new(model.CommandShipClassCreate))
case model.CommandTypeShipClassMerge:
return unmarshalJSONCommand(raw, new(model.CommandShipClassMerge))
case model.CommandTypeShipClassRemove:
return unmarshalJSONCommand(raw, new(model.CommandShipClassRemove))
case model.CommandTypeShipGroupBreak:
return unmarshalJSONCommand(raw, new(model.CommandShipGroupBreak))
case model.CommandTypeShipGroupLoad:
return unmarshalJSONCommand(raw, new(model.CommandShipGroupLoad))
case model.CommandTypeShipGroupUnload:
return unmarshalJSONCommand(raw, new(model.CommandShipGroupUnload))
case model.CommandTypeShipGroupSend:
return unmarshalJSONCommand(raw, new(model.CommandShipGroupSend))
case model.CommandTypeShipGroupUpgrade:
return unmarshalJSONCommand(raw, new(model.CommandShipGroupUpgrade))
case model.CommandTypeShipGroupMerge:
return unmarshalJSONCommand(raw, new(model.CommandShipGroupMerge))
case model.CommandTypeShipGroupDismantle:
return unmarshalJSONCommand(raw, new(model.CommandShipGroupDismantle))
case model.CommandTypeShipGroupTransfer:
return unmarshalJSONCommand(raw, new(model.CommandShipGroupTransfer))
case model.CommandTypeShipGroupJoinFleet:
return unmarshalJSONCommand(raw, new(model.CommandShipGroupJoinFleet))
case model.CommandTypeFleetMerge:
return unmarshalJSONCommand(raw, new(model.CommandFleetMerge))
case model.CommandTypeFleetSend:
return unmarshalJSONCommand(raw, new(model.CommandFleetSend))
case model.CommandTypeScienceCreate:
return unmarshalJSONCommand(raw, new(model.CommandScienceCreate))
case model.CommandTypeScienceRemove:
return unmarshalJSONCommand(raw, new(model.CommandScienceRemove))
case model.CommandTypePlanetRename:
return unmarshalJSONCommand(raw, new(model.CommandPlanetRename))
case model.CommandTypePlanetProduce:
return unmarshalJSONCommand(raw, new(model.CommandPlanetProduce))
case model.CommandTypePlanetRouteSet:
return unmarshalJSONCommand(raw, new(model.CommandPlanetRouteSet))
case model.CommandTypePlanetRouteRemove:
return unmarshalJSONCommand(raw, new(model.CommandPlanetRouteRemove))
default:
return nil, fmt.Errorf("unknown command type: %s", meta.CmdType)
}
}
func unmarshalJSONCommand[T model.DecodableCommand](raw json.RawMessage, v T) (model.DecodableCommand, error) {
if err := json.Unmarshal(raw, v); err != nil {
return nil, err
}
return v, nil
}
type encodedCommand struct {
cmdID string
cmdApplied *bool
@@ -955,14 +1065,220 @@ func EmptyUserGamesCommandResponsePayload() []byte {
return builder.FinishedBytes()
}
// EmptyUserGamesOrderResponsePayload mirrors
// EmptyUserGamesCommandResponsePayload for `MessageTypeUserGamesOrder`.
func EmptyUserGamesOrderResponsePayload() []byte {
builder := flatbuffers.NewBuilder(16)
// UserGamesOrderResponseToPayload encodes the engine's response body
// for `PUT /api/v1/order` into the wire FlatBuffers envelope expected
// for `MessageTypeUserGamesOrder`. The engine populates per-command
// `cmdApplied` / `cmdErrorCode` fields, and they round-trip into the
// FBS `CommandItem` entries unchanged. A nil response is encoded as
// an empty envelope so the gateway can fall back to a batch-level
// "ok" answer when the engine body is unavailable.
func UserGamesOrderResponseToPayload(req *model.UserGamesOrder) ([]byte, error) {
builder := flatbuffers.NewBuilder(1024)
if req == nil {
fbs.UserGamesOrderResponseStart(builder)
offset := fbs.UserGamesOrderResponseEnd(builder)
fbs.FinishUserGamesOrderResponseBuffer(builder, offset)
return builder.FinishedBytes(), nil
}
commandsVec, err := encodeCommandItemVector(builder, req.Commands, "user games order response")
if err != nil {
return nil, err
}
fbs.UserGamesOrderResponseStart(builder)
hi, lo := uuidToHiLo(req.GameID)
fbs.UserGamesOrderResponseAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
fbs.UserGamesOrderResponseAddUpdatedAt(builder, req.UpdatedAt)
if commandsVec != 0 {
fbs.UserGamesOrderResponseAddCommands(builder, commandsVec)
}
offset := fbs.UserGamesOrderResponseEnd(builder)
fbs.FinishUserGamesOrderResponseBuffer(builder, offset)
return builder.FinishedBytes()
return builder.FinishedBytes(), nil
}
// PayloadToUserGamesOrderGet decodes the FlatBuffers payload of
// `MessageTypeUserGamesOrderGet` into the typed model. `Turn` is
// validated to be non-negative; the gateway and backend reject
// negative values before forwarding to the engine.
func PayloadToUserGamesOrderGet(data []byte) (result *model.UserGamesOrderGet, err error) {
if len(data) == 0 {
return nil, errors.New("decode user games order get payload: data is empty")
}
defer func() {
if recovered := recover(); recovered != nil {
result = nil
err = fmt.Errorf("decode user games order get payload: panic recovered: %v", recovered)
}
}()
flat := fbs.GetRootAsUserGamesOrderGet(data, 0)
gameID := flat.GameId(nil)
if gameID == nil {
return nil, errors.New("decode user games order get payload: game_id is missing")
}
turn := flat.Turn()
if turn < 0 {
return nil, fmt.Errorf("decode user games order get payload: turn must be non-negative, got %d", turn)
}
return &model.UserGamesOrderGet{
GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
Turn: int(turn),
}, nil
}
// UserGamesOrderGetToPayload encodes a `model.UserGamesOrderGet`
// request into FlatBuffers bytes suitable for the authenticated
// gateway transport.
func UserGamesOrderGetToPayload(req *model.UserGamesOrderGet) ([]byte, error) {
if req == nil {
return nil, errors.New("encode user games order get payload: request is nil")
}
if req.Turn < 0 {
return nil, fmt.Errorf("encode user games order get payload: turn must be non-negative, got %d", req.Turn)
}
builder := flatbuffers.NewBuilder(64)
fbs.UserGamesOrderGetStart(builder)
hi, lo := uuidToHiLo(req.GameID)
fbs.UserGamesOrderGetAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
fbs.UserGamesOrderGetAddTurn(builder, int64(req.Turn))
offset := fbs.UserGamesOrderGetEnd(builder)
fbs.FinishUserGamesOrderGetBuffer(builder, offset)
return builder.FinishedBytes(), nil
}
// UserGamesOrderGetResponseToPayload encodes the typed response of
// `MessageTypeUserGamesOrderGet`. `found = false` corresponds to the
// engine's `204 No Content` answer; `order` is omitted in that case.
func UserGamesOrderGetResponseToPayload(order *model.UserGamesOrder, found bool) ([]byte, error) {
builder := flatbuffers.NewBuilder(1024)
var orderOffset flatbuffers.UOffsetT
if found && order != nil {
commandsVec, err := encodeCommandItemVector(builder, order.Commands, "user games order get response")
if err != nil {
return nil, err
}
fbs.UserGamesOrderStart(builder)
hi, lo := uuidToHiLo(order.GameID)
fbs.UserGamesOrderAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
fbs.UserGamesOrderAddUpdatedAt(builder, order.UpdatedAt)
if commandsVec != 0 {
fbs.UserGamesOrderAddCommands(builder, commandsVec)
}
orderOffset = fbs.UserGamesOrderEnd(builder)
}
fbs.UserGamesOrderGetResponseStart(builder)
fbs.UserGamesOrderGetResponseAddFound(builder, found)
if orderOffset != 0 {
fbs.UserGamesOrderGetResponseAddOrder(builder, orderOffset)
}
offset := fbs.UserGamesOrderGetResponseEnd(builder)
fbs.FinishUserGamesOrderGetResponseBuffer(builder, offset)
return builder.FinishedBytes(), nil
}
// PayloadToUserGamesOrderResponse decodes the engine's PUT response
// envelope into a typed `*UserGamesOrder`. Empty payloads decode to
// nil so callers can fall back to batch-level handling without a
// dedicated marker.
func PayloadToUserGamesOrderResponse(data []byte) (result *model.UserGamesOrder, err error) {
if len(data) == 0 {
return nil, nil
}
defer func() {
if recovered := recover(); recovered != nil {
result = nil
err = fmt.Errorf("decode user games order response payload: panic recovered: %v", recovered)
}
}()
flat := fbs.GetRootAsUserGamesOrderResponse(data, 0)
gameID := flat.GameId(nil)
if gameID == nil {
// Empty envelope (gateway fallback). The caller treats this
// as "no per-command detail" and synthesises a batch-level
// answer.
if flat.CommandsLength() == 0 {
return nil, nil
}
return nil, errors.New("decode user games order response payload: game_id is missing")
}
out := &model.UserGamesOrder{
GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
UpdatedAt: flat.UpdatedAt(),
}
count := flat.CommandsLength()
if count > 0 {
out.Commands = make([]model.DecodableCommand, count)
flatCommand := new(fbs.CommandItem)
for i := 0; i < count; i++ {
if !flat.Commands(flatCommand, i) {
return nil, fmt.Errorf("decode user games order response %d: command item is missing", i)
}
cmd, decodeErr := decodeOrderCommand(flatCommand, i)
if decodeErr != nil {
return nil, decodeErr
}
out.Commands[i] = cmd
}
}
return out, nil
}
// PayloadToUserGamesOrderGetResponse decodes the FlatBuffers response
// of `MessageTypeUserGamesOrderGet`. When `found = false`, returns
// `(nil, false, nil)` matching the engine's `204 No Content`
// semantics.
func PayloadToUserGamesOrderGetResponse(data []byte) (order *model.UserGamesOrder, found bool, err error) {
if len(data) == 0 {
return nil, false, errors.New("decode user games order get response payload: data is empty")
}
defer func() {
if recovered := recover(); recovered != nil {
order = nil
found = false
err = fmt.Errorf("decode user games order get response payload: panic recovered: %v", recovered)
}
}()
flat := fbs.GetRootAsUserGamesOrderGetResponse(data, 0)
if !flat.Found() {
return nil, false, nil
}
inner := flat.Order(nil)
if inner == nil {
return nil, true, errors.New("decode user games order get response payload: order is missing while found=true")
}
gameID := inner.GameId(nil)
if gameID == nil {
return nil, true, errors.New("decode user games order get response payload: order.game_id is missing")
}
out := &model.UserGamesOrder{
GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
UpdatedAt: inner.UpdatedAt(),
}
count := inner.CommandsLength()
if count > 0 {
out.Commands = make([]model.DecodableCommand, count)
flatCommand := new(fbs.CommandItem)
for i := 0; i < count; i++ {
if !inner.Commands(flatCommand, i) {
return nil, true, fmt.Errorf("decode user games order get response %d: command item is missing", i)
}
cmd, decodeErr := decodeOrderCommand(flatCommand, i)
if decodeErr != nil {
return nil, true, decodeErr
}
out.Commands[i] = cmd
}
}
return out, true, nil
}
// encodeCommandItemVector serialises a slice of DecodableCommand into a
+154
View File
@@ -77,6 +77,160 @@ func TestUserGamesCommandRejectsNilAndEmpty(t *testing.T) {
if _, err := PayloadToUserGamesOrder(nil); err == nil {
t.Fatalf("expected error decoding empty user games order")
}
if _, err := UserGamesOrderGetToPayload(nil); err == nil {
t.Fatalf("expected error encoding nil user games order get")
}
if _, err := PayloadToUserGamesOrderGet(nil); err == nil {
t.Fatalf("expected error decoding empty user games order get")
}
if _, _, err := PayloadToUserGamesOrderGetResponse(nil); err == nil {
t.Fatalf("expected error decoding empty user games order get response")
}
}
func TestUserGamesOrderResponsePayloadRoundTrip(t *testing.T) {
t.Parallel()
applied := true
rejected := false
errCode := 7
source := &model.UserGamesOrder{
GameID: uuid.MustParse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"),
UpdatedAt: 99,
Commands: []model.DecodableCommand{
&model.CommandPlanetRename{
CommandMeta: commandMeta("cmd-1", model.CommandTypePlanetRename, &applied, nil),
Number: 5,
Name: "alpha",
},
&model.CommandPlanetRename{
CommandMeta: commandMeta("cmd-2", model.CommandTypePlanetRename, &rejected, &errCode),
Number: 6,
Name: "beta",
},
},
}
payload, err := UserGamesOrderResponseToPayload(source)
if err != nil {
t.Fatalf("encode user games order response: %v", err)
}
decoded, err := PayloadToUserGamesOrderResponse(payload)
if err != nil {
t.Fatalf("decode user games order response: %v", err)
}
if !reflect.DeepEqual(source, decoded) {
t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
}
}
func TestUserGamesOrderResponseEmptyPayload(t *testing.T) {
t.Parallel()
payload, err := UserGamesOrderResponseToPayload(nil)
if err != nil {
t.Fatalf("encode empty user games order response: %v", err)
}
if len(payload) == 0 {
t.Fatal("empty envelope payload must be non-zero length")
}
decoded, err := PayloadToUserGamesOrderResponse(payload)
if err != nil {
t.Fatalf("decode empty user games order response: %v", err)
}
if decoded != nil {
t.Fatalf("empty envelope must decode to nil, got %#v", decoded)
}
}
func TestUserGamesOrderGetPayloadRoundTrip(t *testing.T) {
t.Parallel()
source := &model.UserGamesOrderGet{
GameID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
Turn: 7,
}
payload, err := UserGamesOrderGetToPayload(source)
if err != nil {
t.Fatalf("encode user games order get: %v", err)
}
decoded, err := PayloadToUserGamesOrderGet(payload)
if err != nil {
t.Fatalf("decode user games order get: %v", err)
}
if !reflect.DeepEqual(source, decoded) {
t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
}
}
func TestUserGamesOrderGetRejectsNegativeTurn(t *testing.T) {
t.Parallel()
if _, err := UserGamesOrderGetToPayload(&model.UserGamesOrderGet{
GameID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
Turn: -1,
}); err == nil {
t.Fatalf("expected error encoding negative turn")
}
}
func TestUserGamesOrderGetResponseRoundTrip(t *testing.T) {
t.Parallel()
applied := true
stored := &model.UserGamesOrder{
GameID: uuid.MustParse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"),
UpdatedAt: 1234,
Commands: []model.DecodableCommand{
&model.CommandPlanetRename{
CommandMeta: commandMeta("cmd-1", model.CommandTypePlanetRename, &applied, nil),
Number: 5,
Name: "stored",
},
},
}
payload, err := UserGamesOrderGetResponseToPayload(stored, true)
if err != nil {
t.Fatalf("encode user games order get response: %v", err)
}
decoded, found, err := PayloadToUserGamesOrderGetResponse(payload)
if err != nil {
t.Fatalf("decode user games order get response: %v", err)
}
if !found {
t.Fatal("expected found=true round-trip")
}
if !reflect.DeepEqual(stored, decoded) {
t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", stored, decoded)
}
}
func TestUserGamesOrderGetResponseNotFound(t *testing.T) {
t.Parallel()
payload, err := UserGamesOrderGetResponseToPayload(nil, false)
if err != nil {
t.Fatalf("encode not-found response: %v", err)
}
decoded, found, err := PayloadToUserGamesOrderGetResponse(payload)
if err != nil {
t.Fatalf("decode not-found response: %v", err)
}
if found {
t.Fatal("expected found=false")
}
if decoded != nil {
t.Fatalf("expected nil order, got %#v", decoded)
}
}
func TestInt64ToInt(t *testing.T) {
+1 -1
View File
@@ -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
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
help:
@echo "ui targets:"
+139 -24
View File
@@ -1522,27 +1522,138 @@ Targeted tests:
inspector content, and on `chromium-mobile-iphone-13` asserts the
bottom-sheet appears and the close button clears it.
## Phase 14. First End-to-End Command — Rename Planet
## ~~Phase 14. First End-to-End Command — Rename Planet~~
Status: pending.
Status: done.
Goal: prove the entire pipeline (inspector → composer → submit →
server → state refresh) by wiring up exactly one action: renaming a
planet.
Artifacts:
Decisions taken with the project owner during implementation:
- `ui/frontend/src/lib/inspectors/planet.svelte` adds a `Rename` action
that opens a small inline editor and adds a `RenamePlanet` command
to the order draft on confirm
- `ui/frontend/src/sync/submit.ts` `submitOrder()` function that POSTs
the entire draft via `GalaxyClient.execute('user.games.order', ...)`
and applies per-command results
- `ui/frontend/src/lib/sidebar/order-tab.svelte` adds a `Submit order`
button calling `submitOrder()` and renders accepted / rejected
status per command after submit
- on successful submit, refresh game state so the rename appears on the
map and in the inspector
1. **Optimistic overlay over `user.games.order`.** The plan's
acceptance criterion ("name change within one second") is
inconsistent with the engine's order endpoint, which only
validates and stores; rename takes effect at turn cutoff.
Phase 14 keeps `user.games.order` for the wire path and adds a
pure projection `applyOrderOverlay(report, commands, statuses)`
in `api/game-state.ts`. Inspector, mobile sheet, and map
renderer read a derived `renderedReport` (context key
`RENDERED_REPORT_CONTEXT_KEY`) that swaps planet names in for
every applied or in-flight rename. Raw `gameState.report`
stays available for debugging / history mode.
2. **Read-back endpoint `user.games.order.get`.** Without a
server snapshot of stored orders the optimistic overlay would
not survive a cache wipe. Phase 14 adds the new authenticated
message type with a backend route
`GET /api/v1/user/games/{game_id}/orders?turn=N` (pass-through
to the engine's existing `GET /api/v1/order`). The frontend
calls it from `OrderDraftStore.hydrateFromServer` only when
the local cache row is *absent* — an explicitly empty cache
row honours the user's empty draft. The `turn` query is
required (the frontend always knows the current turn from the
lobby record).
3. **Per-command results from real engine response.** The engine
now answers `PUT /api/v1/order` with `202 Accepted` and a
populated `UserGamesOrder` body (per-command `cmdApplied`,
`cmdErrorCode`, plus an engine-assigned `updatedAt`). The
gateway parses that JSON into the extended FBS
`UserGamesOrderResponse` envelope and the frontend reads the
per-command outcome through `submitOrder`. A defensive
batch-level fallback covers an empty `commands` array.
4. **Applied commands stay in the draft.** Per the gameplay
model, the order is the player's intent surface — submitted
commands stay until the user removes them or until turn
cutoff (Phase 24 wires the auto-clear). Statuses are
runtime-only; on reload the draft re-validates as `valid` and
the overlay re-applies.
5. **Validator parity through a TS port.** `ValidateTypeName`
from `pkg/util/string.go` is mirrored in
`ui/frontend/src/lib/util/entity-name.ts`. The inspector's
inline editor disables the confirm button until the input
passes; the draft store re-runs the validator on every `add`
and exposes per-row `valid` / `invalid` to the order tab.
6. **`updatedAt` plumbing without enforcement.** Phase 14 sends
`0` on every submit (no client-side stale-order detection
yet); the engine still writes a real timestamp, the gateway
surfaces it in the FBS response, and the draft stashes it.
Future phases can wire conditional updates without a wire
change.
Artifacts (delivered):
- `pkg/schema/fbs/order.fbs` — extended `UserGamesOrderResponse`
(`game_id`, `updated_at`, `commands`); new
`UserGamesOrderGet` / `UserGamesOrderGetResponse` tables.
- `pkg/model/order/order.go` — `MessageTypeUserGamesOrderGet` and
`UserGamesOrderGet` typed payload.
- `pkg/transcoder/order.go` — `JSONToUserGamesOrder`,
`UserGamesOrderResponseToPayload`,
`UserGamesOrderGetToPayload`,
`PayloadToUserGamesOrderGet`,
`PayloadToUserGamesOrderResponse`,
`UserGamesOrderGetResponseToPayload`,
`PayloadToUserGamesOrderGetResponse`. Replaces the old
`EmptyUserGamesOrderResponsePayload` helper.
- `backend/internal/server/handlers_user_games.go` — new
`GetOrders` handler. `engineclient.GetOrder` forwards to the
engine's `GET /api/v1/order` with the player rebound.
`backend/openapi.yaml` documents the new GET operation;
`contract_test.go` extended with a `queryParamStubs` map for
required query parameters.
- `gateway/internal/backendclient/games_commands.go` — updated
`executeUserGamesOrder` (parses real engine JSON via
`JSONToUserGamesOrder`); new `executeUserGamesOrderGet` and
`projectUserGamesOrderGetResponse`.
`gateway/internal/backendclient/routes.go` registers the new
message type.
- `ui/Makefile` — `order.fbs` joins `FBS_INPUTS`; regenerated TS
bindings under `ui/frontend/src/proto/galaxy/fbs/order/`.
- `ui/frontend/src/sync/order-types.ts` — `PlanetRenameCommand`
variant added to the discriminated union.
- `ui/frontend/src/sync/submit.ts` — `submitOrder` posts the FBS
request and parses per-command verdicts.
- `ui/frontend/src/sync/order-load.ts` — `fetchOrder` issues
`user.games.order.get`.
- `ui/frontend/src/sync/order-draft.svelte.ts` — extended with
per-command `statuses`, `validate` / `markSubmitting` /
`applyResults` / `markRejected` / `revertSubmittingToValid` /
`hydrateFromServer`, and the `needsServerHydration` flag.
- `ui/frontend/src/lib/util/entity-name.ts` — TS port of
`ValidateTypeName`.
- `ui/frontend/src/api/game-state.ts` — pure
`applyOrderOverlay(report, commands, statuses)` projection
plus the `currentTurn` rune on `GameStateStore`.
- `ui/frontend/src/lib/rendered-report.svelte.ts` — derives the
overlay-applied report and exposes it through
`RENDERED_REPORT_CONTEXT_KEY`.
- `ui/frontend/src/lib/galaxy-client-context.svelte.ts` —
`GalaxyClientHolder` so command-driven UI can resolve the
per-game `GalaxyClient` via context.
- `ui/frontend/src/lib/inspectors/planet.svelte` — Rename action
+ inline editor with `validateEntityName`-driven feedback.
- `ui/frontend/src/lib/sidebar/order-tab.svelte` — per-row
status, Submit button with disabled-state matrix, refresh on
success, surfaces batch errors inline.
- `ui/frontend/src/lib/sidebar/inspector-tab.svelte` and
`ui/frontend/src/lib/active-view/map.svelte` — switched to
`renderedReport`.
- `ui/frontend/src/routes/games/[id]/+layout.svelte` — wires the
rendered report and galaxy-client contexts; runs
`orderDraft.hydrateFromServer(...)` after the boot
`Promise.all` resolves when `needsServerHydration`.
- `ui/frontend/src/lib/i18n/locales/{en,ru}.ts` — keys for
rename action / editor / order statuses / submit copy.
- Tests: `entity-name.test.ts`, `submit.test.ts`,
`order-load.test.ts`, `order-overlay.test.ts`,
`order-tab.test.ts`, extended `order-draft.test.ts` and
`inspector-planet.test.ts`. New Playwright spec
`tests/e2e/rename-planet.spec.ts`.
- Documentation: `docs/ARCHITECTURE.md` §9, `docs/FUNCTIONAL.md`
§6.2 (and `docs/FUNCTIONAL_ru.md` mirror), `ui/docs/order-composer.md`
with the new "Submit pipeline", "Optimistic overlay", and
"Server hydration on cache miss" sections.
Dependencies: Phases 12, 13.
@@ -1550,19 +1661,23 @@ Acceptance criteria:
- the user can select a planet, click `Rename`, type a new name, see
the command appear in the order tab, click `Submit`, and observe the
planet's name change everywhere within one second;
- attempting an empty or invalid name is blocked locally (button
disabled with tooltip);
- a server-side rejection (race condition) is surfaced as `rejected`
status in the order tab.
planet's name change everywhere within one second (overlay applies
immediately on the inspector / mobile sheet / map; server-side state
catches up at turn cutoff);
- attempting an empty or invalid name is blocked locally (Submit
button disabled, inline error message under the input);
- a server-side rejection is surfaced as `rejected` status on every
in-flight row, with the gateway's error message inline.
Targeted tests:
- Vitest unit tests for `submitOrder` with mocked `GalaxyClient`;
- Vitest component test for the inline rename editor including
validation;
- Playwright e2e: rename a seeded planet, reload, confirm the new name
persists.
- Vitest unit tests for `submitOrder`, `fetchOrder`,
`applyOrderOverlay`, `validateEntityName`, and the extended
`OrderDraftStore`.
- Vitest component tests for the inline rename editor and the
Submit button states.
- Playwright e2e: rename a seeded planet, reload, confirm the new
name persists; rejected path keeps the old name.
## Phase 15. Inspector — Planet Production Controls
+144 -25
View File
@@ -25,13 +25,22 @@ during a connectivity hiccup keeps every line the player typed. A
remote-first composer that reflects the gateway's pending-orders
queue would force a sync on every keystroke.
When the submit pipeline lands (Phase 25), it iterates the draft
in order, sending one `command` per line. The gateway's per-line
result rejoins the draft entry through `cmdId`, and the entry's
`CommandStatus` flips to `applied` or `rejected`. Successfully
applied entries stay visible until the next turn cutoff so the
player can see what was committed; rejected entries stay until the
player edits or removes them.
Phase 14 lands the submit pipeline with batch semantics: every
entry the user has marked `valid` is collected into one signed
`user.games.order` request. The engine validates and stores the
order, returning per-command `cmdApplied` / `cmdErrorCode` in the
response body. The gateway re-encodes that JSON into the FBS
`UserGamesOrderResponse` envelope (with `commands: [CommandItem]`
populated), and `submitOrder` rejoins the verdict to each draft
entry by `cmdId`. Successfully applied entries stay visible in
the draft (the player keeps composing until turn cutoff);
rejected entries stay until the player edits or removes them.
Phase 25 is reserved for one extension on top of this: per-line
sequencing if a future use case needs to submit commands
individually rather than in one batch. The wire shape is already
flexible enough — the response carries an array of results — so
Phase 25 only changes the client-side iteration policy.
## Local-validation invariant
@@ -42,10 +51,13 @@ pipeline refuses to drain a draft that contains any `invalid`
entries. The validation step is per-command and pure — it consults
the current `GameStateStore` snapshot only, never the network.
Phase 12 ships the skeleton without any concrete validators: the
single `placeholder` variant is content-free and stays at `draft`
forever. Phase 14's `planetRename` is the first variant that
exercises the `draft → valid | invalid` transition.
Phase 14's `planetRename` is the first variant that exercises the
`draft → valid | invalid` transition. The validator
(`lib/util/entity-name.ts`) is a TS port of
`pkg/util/string.go.ValidateTypeName`, exercised on every render
in the inline editor and re-run by the store on every `add`. The
submit pipeline filters the draft to `valid` entries only — any
`invalid` row blocks the Submit button.
## Command status state machine
@@ -65,14 +77,25 @@ Transitions:
- **`submitting → applied` / `submitting → rejected`**: the gateway
responded; the entry is no longer in flight.
Phase 12 stores the type but does not implement any transitions.
Every entry remains at `draft` until later phases land the
validators (Phase 14) and the submit pipeline (Phase 25).
Phase 14 lands the local validators (`draft → valid | invalid`),
the submit pipeline (`valid → submitting → applied | rejected`),
and the optimistic overlay that shows the player's intent on the
map and inspector while the order is in flight.
Statuses are runtime-only — they are not persisted alongside the
commands themselves. On every `init` the store re-runs
`validateEntityName` over each command and seeds `draft → valid` /
`invalid`. Submitted-then-applied entries lose their `applied`
status on reload but stay in the draft as `valid`; the user sees
the same row, the overlay reapplies, and re-submitting is
idempotent on the engine side (the rename already matches the
stored value).
## Discriminated union shape
`OrderCommand` is a discriminated union on the `kind` field. Phase
12 ships a single variant:
12 shipped the skeleton with a single content-free variant; Phase
14 adds the first real one:
```ts
interface PlaceholderCommand {
@@ -80,15 +103,25 @@ interface PlaceholderCommand {
readonly id: string;
readonly label: string;
}
type OrderCommand = PlaceholderCommand;
interface PlanetRenameCommand {
readonly kind: "planetRename";
readonly id: string;
readonly planetNumber: number;
readonly name: string;
}
type OrderCommand = PlaceholderCommand | PlanetRenameCommand;
```
The `id` field is the canonical identifier the store uses for
remove and reorder; later variants must keep `id: string` so the
store API stays uniform. The whole draft round-trips through
IndexedDB structured clone, so every variant must use only
JSON-friendly value types. Phase 14 adds the first real variant
(`planetRename`) and updates this list.
JSON-friendly value types. Phase 14 lands `planetRename` together
with the inline editor in `lib/inspectors/planet.svelte`, the
local validator (`lib/util/entity-name.ts`, parity with
`pkg/util/string.go.ValidateTypeName`), and the submit pipeline.
## Store
@@ -124,6 +157,70 @@ The order tab consumes the store via
`getContext(ORDER_DRAFT_CONTEXT_KEY)`; Phase 14's planet inspector
will use the same key to push a new command.
## Submit pipeline
`sync/submit.ts` wraps `GalaxyClient.executeCommand("user.games.order", …)`:
1. The order tab filters the draft to `valid` entries, calls
`markSubmitting(ids)` so each row reads `submitting`, then
posts the snapshot through `submitOrder`.
2. `submitOrder` builds the FBS `UserGamesOrder` request (game_id,
`updatedAt = 0` in Phase 14, every command encoded as a
`CommandItem` with the typed payload union) and signs it via
the existing `executeCommand` orchestration.
3. The engine validates, stores, and answers `202 Accepted` with
the stored order body — `game_id`, `updatedAt`, plus each
command echoed with `cmdApplied` and (on rejection)
`cmdErrorCode`.
4. The gateway re-encodes that JSON into FBS
`UserGamesOrderResponse`, and the frontend parses it back into
`Map<cmdId, "applied" | "rejected">`.
5. The order tab calls `applyResults` on the draft, then
`gameState.refresh()` to fetch a fresh report. The optimistic
overlay (`api/game-state.ts.applyOrderOverlay`) keeps the
player's intent visible on the map / inspector even if the
engine has not yet applied the rename — turn cutoff resolves
the divergence on the next report.
If the gateway answers with a non-`ok` `resultCode` (auth /
transcoder / engine validation), the submit pipeline marks every
in-flight entry as `rejected` and surfaces the gateway's error
message inline; no refresh is issued. Network exceptions revert
in-flight entries back to `valid` so the operator can retry.
## Optimistic overlay
`applyOrderOverlay(report, commands, statuses)` (in
`api/game-state.ts`) returns a copy of the server `GameReport`
with every command in `applied` or `submitting` status projected
on top. Phase 14 understands `planetRename` only; future phases
extend the switch.
The overlay has its own context (`RENDERED_REPORT_CONTEXT_KEY`,
`lib/rendered-report.svelte.ts`) — the in-game shell layout owns
the source and exposes it to the inspector tab, the mobile sheet,
and the map renderer. Raw `gameState.report` stays available for
debugging and for any future consumer that needs the un-overlaid
snapshot (history mode is the planned reader).
## Server hydration on cache miss
`OrderDraftStore` records `needsServerHydration = true` when no
cache row exists for the active game (fresh install, cleared
storage, switching device). After the layout boot resolves both
`gameState.init` and `orderDraft.init`, it calls
`orderDraft.hydrateFromServer({ client, turn })` which issues
`user.games.order.get` against the gateway. A `found = false`
answer leaves the draft empty; a stored order is decoded into
`OrderCommand[]` and persisted to the local cache so subsequent
reloads use the cached copy.
An *explicitly* empty cache row (the user has removed every
command they composed) does not trigger hydration — local intent
always wins over the server snapshot. The "did this row exist?"
distinction matters: `Cache.get` returns `undefined` on a miss
and `[]` on an explicitly stored empty array.
## Persistence
Cache row layout:
@@ -168,19 +265,41 @@ its own test suite.
## Testing
Two test artifacts cover the skeleton:
Phase 12 + Phase 14 test artifacts:
- [`../frontend/tests/order-draft.test.ts`](../frontend/tests/order-draft.test.ts)
— Vitest unit tests for the store. Drives `OrderDraftStore`
directly with `IDBCache` over `fake-indexeddb`. Covers init,
add, remove, move, per-game isolation, mutations-before-init,
and dispose hygiene.
dispose hygiene, the Phase 14 status machine
(`validate` / `markSubmitting` / `applyResults` /
`revertSubmittingToValid`), and the
`hydrateFromServer` cache-miss fallback.
- [`../frontend/tests/entity-name.test.ts`](../frontend/tests/entity-name.test.ts)
— Vitest tests for the validator. Aligned with
`pkg/util/string_test.go.TestValidateString` for parity.
- [`../frontend/tests/submit.test.ts`](../frontend/tests/submit.test.ts)
— Vitest tests for the submit pipeline. Hand-builds FBS
responses to verify per-command parsing and batch-level
fallback.
- [`../frontend/tests/order-load.test.ts`](../frontend/tests/order-load.test.ts)
— Vitest tests for `fetchOrder`. Covers the populated /
not-found / negative-turn / non-ok paths.
- [`../frontend/tests/order-overlay.test.ts`](../frontend/tests/order-overlay.test.ts)
— Vitest tests for the pure `applyOrderOverlay` projection.
- [`../frontend/tests/order-tab.test.ts`](../frontend/tests/order-tab.test.ts)
— Vitest component tests for the Submit button states and the
applied / rejected verdict flow.
- [`../frontend/tests/inspector-planet.test.ts`](../frontend/tests/inspector-planet.test.ts)
— Vitest component tests for the rename action and the inline
editor's local validation.
- [`../frontend/tests/e2e/order-composer.spec.ts`](../frontend/tests/e2e/order-composer.spec.ts)
— Playwright spec. Seeds three commands through
`__galaxyDebug.seedOrderDraft`, navigates into
`/games/<id>/map`, opens the Order tool (sidebar tab on
desktop, bottom tab on mobile), asserts the rows, reloads, and
asserts the rows again.
— Playwright spec for the Phase 12 skeleton (seed three
commands, reload, persistence).
- [`../frontend/tests/e2e/rename-planet.spec.ts`](../frontend/tests/e2e/rename-planet.spec.ts)
— Phase 14 end-to-end: select a planet, rename, submit, observe
the overlay-applied name on the inspector + map, reload, and
see the rename hydrated from `user.games.order.get`.
The `__galaxyDebug.seedOrderDraft(gameId, commands)` and
`__galaxyDebug.clearOrderDraft(gameId)` helpers in
+42
View File
@@ -10,6 +10,11 @@
// sets for `LocalPlanet`, `OtherPlanet`, `UninhabitedPlanet`, and
// `UnidentifiedPlanet`, and the wrapper preserves that nullability
// instead of inventing zero values.
//
// Phase 14 adds `applyOrderOverlay`: every applied / submitting
// rename in the local draft swaps the planet name on the rendered
// report so the player sees their intent reflected immediately,
// without waiting for the next turn cutoff.
import { Builder, ByteBuffer } from "flatbuffers";
@@ -19,6 +24,7 @@ import {
GameReportRequest,
Report,
} from "../proto/galaxy/fbs/report";
import type { CommandStatus, OrderCommand } from "../sync/order-types";
const MESSAGE_TYPE = "user.games.report";
@@ -205,6 +211,42 @@ export function uuidToHiLo(value: string): [bigint, bigint] {
return [hi, lo];
}
/**
* applyOrderOverlay returns a copy of `report` with every applied or
* still-in-flight (`submitting`) command from `commands` projected on
* top. Phase 14 understands `planetRename` only — every other variant
* passes through. The function is pure: callers re-derive the
* overlay whenever the draft or the report change.
*
* `statuses` maps command id → status. Entries with `applied` or
* `submitting` participate in the overlay; everything else (`draft`,
* `valid`, `invalid`, `rejected`) is treated as "not yet committed
* by the player" and skipped. This matches the order-composer model:
* the player sees their own committed intent, not their unfinished
* edits.
*/
export function applyOrderOverlay(
report: GameReport,
commands: OrderCommand[],
statuses: Record<string, CommandStatus>,
): GameReport {
if (commands.length === 0) return report;
let mutatedPlanets: ReportPlanet[] | null = null;
for (const cmd of commands) {
const status = statuses[cmd.id];
if (status !== "applied" && status !== "submitting") continue;
if (cmd.kind !== "planetRename") continue;
const idx = report.planets.findIndex((p) => p.number === cmd.planetNumber);
if (idx < 0) continue;
if (mutatedPlanets === null) {
mutatedPlanets = [...report.planets];
}
mutatedPlanets[idx] = { ...mutatedPlanets[idx]!, name: cmd.name };
}
if (mutatedPlanets === null) return report;
return { ...report, planets: mutatedPlanets };
}
function decodeErrorMessage(payload: Uint8Array): { code: string; message: string } {
if (payload.length === 0) {
return { code: "internal_error", message: "empty error payload" };
+12 -1
View File
@@ -36,8 +36,15 @@ preference the store already manages.
SELECTION_CONTEXT_KEY,
type SelectionStore,
} from "$lib/selection.svelte";
import {
RENDERED_REPORT_CONTEXT_KEY,
type RenderedReportSource,
} from "$lib/rendered-report.svelte";
const store = getContext<GameStateStore | undefined>(GAME_STATE_CONTEXT_KEY);
const renderedReport = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY,
);
const selection = getContext<SelectionStore | undefined>(SELECTION_CONTEXT_KEY);
let canvasEl: HTMLCanvasElement | null = $state(null);
@@ -52,7 +59,11 @@ preference the store already manages.
let mounted = false;
$effect(() => {
const report = store?.report;
// Read the overlay-applied report so the map labels reflect
// pending renames immediately. Falls back to raw report when
// the rendered source is missing (e.g. component used outside
// the in-game shell layout).
const report = renderedReport?.report ?? store?.report;
const status = store?.status ?? "idle";
// Track the wrap mode so the renderer remounts when Phase 29's
// toggle UI flips it; the read here also subscribes the effect.
@@ -0,0 +1,34 @@
// Exposes the per-game `GalaxyClient` instance through a Svelte
// context so command-driven UI (the order-tab submit button,
// later phases' inspector actions) can issue gateway calls without
// re-instantiating the client. The handle is intentionally a thin
// reactive wrapper: the layout populates `client` after the boot
// `Promise.all` resolves, and consumers read the latest value
// through the getter — `null` while the boot is in flight, set to
// the live client once the keypair / gateway public key are loaded.
import type { GalaxyClient } from "../api/galaxy-client";
/**
* GALAXY_CLIENT_CONTEXT_KEY is the Svelte context key the in-game
* shell layout uses to expose its bound `GalaxyClient` to
* descendants. The order-tab submit button reads this to call
* `submitOrder`.
*/
export const GALAXY_CLIENT_CONTEXT_KEY = Symbol("galaxy-client");
export interface GalaxyClientHandle {
readonly client: GalaxyClient | null;
}
export class GalaxyClientHolder implements GalaxyClientHandle {
#client: GalaxyClient | null = $state(null);
get client(): GalaxyClient | null {
return this.#client;
}
set(client: GalaxyClient | null): void {
this.#client = client;
}
}
+8 -1
View File
@@ -41,10 +41,17 @@ export class GameStateStore {
report: GameReport | null = $state(null);
wrapMode: WrapMode = $state("torus");
error: string | null = $state(null);
/**
* currentTurn mirrors the engine's turn number for the running
* game (lifted from the lobby record on `setGame`). Phase 14
* exposes it so the layout can pass it to
* `OrderDraftStore.hydrateFromServer` after both stores boot;
* later phases (history mode, calc) will read it directly.
*/
currentTurn = $state(0);
private client: GalaxyClient | null = null;
private cache: Cache | null = null;
private currentTurn = 0;
private destroyed = false;
private visibilityListener: (() => void) | null = null;
+22
View File
@@ -120,6 +120,17 @@ const en = {
"game.sidebar.empty.inspector": "select an object on the map",
"game.sidebar.empty.order": "order is empty",
"game.sidebar.order.command_delete": "delete",
"game.sidebar.order.submit": "submit",
"game.sidebar.order.submit_in_flight": "submitting…",
"game.sidebar.order.status.draft": "draft",
"game.sidebar.order.status.valid": "valid",
"game.sidebar.order.status.invalid": "invalid",
"game.sidebar.order.status.submitting": "submitting",
"game.sidebar.order.status.applied": "applied",
"game.sidebar.order.status.rejected": "rejected",
"game.sidebar.order.label.placeholder": "{label}",
"game.sidebar.order.label.planet_rename": "rename planet {planet} → {name}",
"game.sidebar.order.error.batch_failed": "submit failed: {message}",
"game.bottom_tabs.map": "map",
"game.bottom_tabs.calc": "calc",
"game.bottom_tabs.order": "order",
@@ -144,6 +155,17 @@ const en = {
"game.inspector.planet.production_none": "none",
"game.inspector.planet.unidentified_no_data": "no data — only the location is known",
"game.inspector.sheet_close": "close",
"game.inspector.planet.action.rename": "rename",
"game.inspector.planet.rename.title": "rename planet",
"game.inspector.planet.rename.confirm": "save",
"game.inspector.planet.rename.cancel": "cancel",
"game.inspector.planet.rename.invalid.empty": "name cannot be empty",
"game.inspector.planet.rename.invalid.too_long": "name is too long (30 characters max)",
"game.inspector.planet.rename.invalid.starts_with_special": "name cannot start with a special character",
"game.inspector.planet.rename.invalid.ends_with_special": "name cannot end with a special character",
"game.inspector.planet.rename.invalid.consecutive_specials": "too many special characters in a row",
"game.inspector.planet.rename.invalid.whitespace": "name cannot contain spaces",
"game.inspector.planet.rename.invalid.disallowed_character": "name contains disallowed characters",
} as const;
export default en;
+22
View File
@@ -121,6 +121,17 @@ const ru: Record<keyof typeof en, string> = {
"game.sidebar.empty.inspector": "выберите объект на карте",
"game.sidebar.empty.order": "приказ пуст",
"game.sidebar.order.command_delete": "удалить",
"game.sidebar.order.submit": "отправить",
"game.sidebar.order.submit_in_flight": "отправка…",
"game.sidebar.order.status.draft": "черновик",
"game.sidebar.order.status.valid": "готова",
"game.sidebar.order.status.invalid": "ошибка",
"game.sidebar.order.status.submitting": "отправка",
"game.sidebar.order.status.applied": "принята",
"game.sidebar.order.status.rejected": "отклонена",
"game.sidebar.order.label.placeholder": "{label}",
"game.sidebar.order.label.planet_rename": "переименовать планету {planet} → {name}",
"game.sidebar.order.error.batch_failed": "ошибка отправки: {message}",
"game.bottom_tabs.map": "карта",
"game.bottom_tabs.calc": "калк",
"game.bottom_tabs.order": "приказ",
@@ -145,6 +156,17 @@ const ru: Record<keyof typeof en, string> = {
"game.inspector.planet.production_none": "не задано",
"game.inspector.planet.unidentified_no_data": "нет данных — известно только местоположение",
"game.inspector.sheet_close": "закрыть",
"game.inspector.planet.action.rename": "переименовать",
"game.inspector.planet.rename.title": "переименование планеты",
"game.inspector.planet.rename.confirm": "сохранить",
"game.inspector.planet.rename.cancel": "отмена",
"game.inspector.planet.rename.invalid.empty": "имя не может быть пустым",
"game.inspector.planet.rename.invalid.too_long": "имя слишком длинное (максимум 30 символов)",
"game.inspector.planet.rename.invalid.starts_with_special": "имя не может начинаться со спецсимвола",
"game.inspector.planet.rename.invalid.ends_with_special": "имя не может заканчиваться спецсимволом",
"game.inspector.planet.rename.invalid.consecutive_specials": "слишком много спецсимволов подряд",
"game.inspector.planet.rename.invalid.whitespace": "имя не может содержать пробелы",
"game.inspector.planet.rename.invalid.disallowed_character": "имя содержит недопустимые символы",
};
export default ru;
+208 -14
View File
@@ -1,23 +1,29 @@
<!--
Phase 13 read-only planet inspector. Renders the documented field
set for the planet kind in question:
Planet inspector. Renders the documented field set for each planet
kind (local / other / uninhabited / unidentified) and exposes a
Rename action on owned (`local`) planets that opens an inline
editor. The editor runs the same `validateEntityName` rules as the
server-side validator (parity with `pkg/util/string.go`) and, on
confirm, appends a `planetRename` command to the local order draft
through the `OrderDraftStore` provided via context.
- `local` / `other` carry the full economy: name, owner (other only),
coordinates, size, population, colonists, industry, both stockpiles,
natural resources, current production, free production potential.
- `uninhabited` keeps name, coordinates, size, both stockpiles, and
natural resources — the engine does not project industry or
population for unowned planets.
- `unidentified` is reduced to coordinates plus a no-data hint.
The component is purely presentational: the parent supplies a
`ReportPlanet` snapshot resolved from `GameStateStore`, no store
lookups happen here. Phase 14 will extend the same component with a
`Rename` action; the read-only layout stays the structural baseline.
The read-only path stays unchanged for non-`local` planets. The
inline editor lives directly inside this component per PLAN.md
Phase 14 — a separate file would be over-abstraction for one input
field with five buttons.
-->
<script lang="ts">
import { getContext, tick } from "svelte";
import type { ReportPlanet } from "../../api/game-state";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import {
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../../sync/order-draft.svelte";
import {
validateEntityName,
type EntityNameInvalidReason,
} from "$lib/util/entity-name";
type Props = {
planet: ReportPlanet;
@@ -31,6 +37,34 @@ lookups happen here. Phase 14 will extend the same component with a
unidentified: "game.inspector.planet.kind.unidentified",
};
const invalidReasonKeyMap: Record<EntityNameInvalidReason, TranslationKey> = {
empty: "game.inspector.planet.rename.invalid.empty",
too_long: "game.inspector.planet.rename.invalid.too_long",
starts_with_special:
"game.inspector.planet.rename.invalid.starts_with_special",
ends_with_special: "game.inspector.planet.rename.invalid.ends_with_special",
consecutive_specials:
"game.inspector.planet.rename.invalid.consecutive_specials",
whitespace: "game.inspector.planet.rename.invalid.whitespace",
disallowed_character:
"game.inspector.planet.rename.invalid.disallowed_character",
};
const draft = getContext<OrderDraftStore | undefined>(
ORDER_DRAFT_CONTEXT_KEY,
);
let renameOpen = $state(false);
let renameInput = $state("");
let inputEl: HTMLInputElement | null = $state(null);
const renameValidation = $derived(validateEntityName(renameInput));
const renameInvalidMessage = $derived(
renameValidation.ok
? ""
: i18n.t(invalidReasonKeyMap[renameValidation.reason]),
);
const kindLabel = $derived(i18n.t(kindKeyMap[planet.kind]));
const coordinates = $derived(
`(${formatNumber(planet.x)}, ${formatNumber(planet.y)})`,
@@ -47,6 +81,44 @@ lookups happen here. Phase 14 will extend the same component with a
}
return value;
}
async function openRename(): Promise<void> {
renameInput = planet.name;
renameOpen = true;
await tick();
inputEl?.focus();
inputEl?.select();
}
function cancelRename(): void {
renameOpen = false;
renameInput = "";
}
async function confirmRename(): Promise<void> {
const result = validateEntityName(renameInput);
if (!result.ok || draft === undefined) return;
await draft.add({
kind: "planetRename",
id: crypto.randomUUID(),
planetNumber: planet.number,
name: result.value,
});
renameOpen = false;
renameInput = "";
}
function onKeyDown(event: KeyboardEvent): void {
if (event.key === "Escape") {
event.preventDefault();
cancelRename();
return;
}
if (event.key === "Enter") {
event.preventDefault();
void confirmRename();
}
}
</script>
<section
@@ -60,8 +132,65 @@ lookups happen here. Phase 14 will extend the same component with a
{#if planet.kind !== "unidentified"}
<h3 class="name" data-testid="inspector-planet-name">{planet.name}</h3>
{/if}
{#if planet.kind === "local" && !renameOpen}
<button
type="button"
class="action"
data-testid="inspector-planet-rename-action"
onclick={openRename}
>
{i18n.t("game.inspector.planet.action.rename")}
</button>
{/if}
</header>
{#if planet.kind === "local" && renameOpen}
<div class="rename" data-testid="inspector-planet-rename">
<label class="rename-label" for="planet-rename-input">
{i18n.t("game.inspector.planet.rename.title")}
</label>
<input
id="planet-rename-input"
type="text"
class="rename-input"
data-testid="inspector-planet-rename-input"
bind:value={renameInput}
bind:this={inputEl}
onkeydown={onKeyDown}
aria-invalid={renameValidation.ok ? "false" : "true"}
aria-describedby={renameValidation.ok ? undefined : "planet-rename-error"}
/>
{#if !renameValidation.ok}
<p
id="planet-rename-error"
class="rename-error"
data-testid="inspector-planet-rename-error"
>
{renameInvalidMessage}
</p>
{/if}
<div class="rename-actions">
<button
type="button"
class="rename-cancel"
data-testid="inspector-planet-rename-cancel"
onclick={cancelRename}
>
{i18n.t("game.inspector.planet.rename.cancel")}
</button>
<button
type="button"
class="rename-confirm"
data-testid="inspector-planet-rename-confirm"
disabled={!renameValidation.ok || draft === undefined}
onclick={() => void confirmRename()}
>
{i18n.t("game.inspector.planet.rename.confirm")}
</button>
</div>
</div>
{/if}
<dl class="fields">
{#if planet.kind === "other" && planet.owner !== null}
<div class="field" data-testid="inspector-planet-field-owner">
@@ -194,4 +323,69 @@ lookups happen here. Phase 14 will extend the same component with a
color: #888;
font-size: 0.85rem;
}
.action {
align-self: flex-start;
margin-top: 0.25rem;
font: inherit;
font-size: 0.85rem;
padding: 0.2rem 0.55rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
border-radius: 3px;
cursor: pointer;
}
.action:hover {
color: #e8eaf6;
border-color: #6d8cff;
}
.rename {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.rename-label {
font-size: 0.85rem;
color: #aab;
}
.rename-input {
font: inherit;
padding: 0.3rem 0.5rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
border-radius: 3px;
}
.rename-input[aria-invalid="true"] {
border-color: #d97a7a;
}
.rename-error {
margin: 0;
font-size: 0.8rem;
color: #d97a7a;
}
.rename-actions {
display: flex;
gap: 0.4rem;
}
.rename-cancel,
.rename-confirm {
font: inherit;
font-size: 0.85rem;
padding: 0.25rem 0.65rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
border-radius: 3px;
cursor: pointer;
}
.rename-confirm:not(:disabled):hover,
.rename-cancel:hover {
color: #e8eaf6;
border-color: #6d8cff;
}
.rename-confirm:disabled {
cursor: not-allowed;
opacity: 0.5;
}
</style>
@@ -0,0 +1,52 @@
// Provides a derived view of the server `GameReport` overlaid with
// the player's local order draft. Every consumer that needs to
// render the player's current intent (inspector, map, mobile sheet)
// subscribes through this context instead of reading `gameState.report`
// directly.
//
// Lifetime matches the in-game shell layout: one source per game,
// rebuilt on layout remount. The source itself is a thin reactive
// wrapper — the actual overlay computation lives in
// `applyOrderOverlay` (api/game-state.ts) and runs lazily on every
// access through the `report` getter.
import {
applyOrderOverlay,
type GameReport,
} from "../api/game-state";
import type { GameStateStore } from "./game-state.svelte";
import type { OrderDraftStore } from "../sync/order-draft.svelte";
/**
* RENDERED_REPORT_CONTEXT_KEY is the Svelte context key the in-game
* shell layout uses to expose a `RenderedReportSource` instance to
* descendants. Consumers read the latest overlay through `source.report`
* (a reactive getter) and re-render when the underlying stores
* change.
*/
export const RENDERED_REPORT_CONTEXT_KEY = Symbol("rendered-report");
export interface RenderedReportSource {
readonly report: GameReport | null;
}
/**
* createRenderedReportSource binds the live `GameStateStore` and
* `OrderDraftStore` to a getter that returns the overlay-applied
* report on every read. The getter is reactive: Svelte tracks the
* underlying `$state` accesses inside `applyOrderOverlay`, so any
* change to the report or the draft re-runs every dependent
* `$derived` block.
*/
export function createRenderedReportSource(
gameState: GameStateStore,
orderDraft: OrderDraftStore,
): RenderedReportSource {
return {
get report(): GameReport | null {
const raw = gameState.report;
if (raw === null) return null;
return applyOrderOverlay(raw, orderDraft.commands, orderDraft.statuses);
},
};
}
@@ -14,18 +14,18 @@ from the Phase 10 stub.
<script lang="ts">
import { getContext } from "svelte";
import { i18n } from "$lib/i18n/index.svelte";
import {
GAME_STATE_CONTEXT_KEY,
type GameStateStore,
} from "$lib/game-state.svelte";
import {
SELECTION_CONTEXT_KEY,
type SelectionStore,
} from "$lib/selection.svelte";
import {
RENDERED_REPORT_CONTEXT_KEY,
type RenderedReportSource,
} from "$lib/rendered-report.svelte";
import Planet from "$lib/inspectors/planet.svelte";
const gameState = getContext<GameStateStore | undefined>(
GAME_STATE_CONTEXT_KEY,
const renderedReport = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY,
);
const selection = getContext<SelectionStore | undefined>(
SELECTION_CONTEXT_KEY,
@@ -34,7 +34,7 @@ from the Phase 10 stub.
const selectedPlanet = $derived.by(() => {
const sel = selection?.selected;
if (sel === undefined || sel === null || sel.kind !== "planet") return null;
const report = gameState?.report;
const report = renderedReport?.report;
if (report === undefined || report === null) return null;
return report.planets.find((p) => p.number === sel.id) ?? null;
});
+202 -19
View File
@@ -1,31 +1,143 @@
<!--
Order composer tool. Resolves the per-game `OrderDraftStore` from
context (set by `routes/games/[id]/+layout.svelte`) and renders the
draft as a vertical, top-to-bottom command list. Empty state shows
the i18n empty-state copy; non-empty state shows an ordered list of
rows, each with a stable `data-testid` plus a per-row delete button.
Order composer tool. Resolves the per-game `OrderDraftStore`,
`GameStateStore`, and `GalaxyClient` from context (all set by
`routes/games/[id]/+layout.svelte`) and renders the local draft as
a vertical list with per-row status, a delete button, and a Submit
button at the bottom.
Phase 12 has no UI for adding commands — Phase 14 lands the first
end-to-end command (`planetRename`) and the inspector affordance
that pushes it into the draft. Tests exercise the skeleton through
`__galaxyDebug.seedOrderDraft` (Playwright) and via direct store
construction (Vitest).
Phase 14 wires the first end-to-end command: clicking Submit calls
`submitOrder` for every entry in `valid` status, flips the in-flight
rows to `submitting`, then merges the per-command verdict back into
the draft once the gateway responds. The optimistic overlay in
`renderedReport` continues to show the player's intent while the
order is in flight, so the inspector and the map reflect the new
name even before the server applies it at turn cutoff.
Tests exercise the skeleton through `__galaxyDebug.seedOrderDraft`
(Playwright) and via direct store / mocked-client construction
(Vitest).
-->
<script lang="ts">
import { getContext } from "svelte";
import { i18n } from "$lib/i18n/index.svelte";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import {
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../../sync/order-draft.svelte";
import {
GAME_STATE_CONTEXT_KEY,
type GameStateStore,
} from "$lib/game-state.svelte";
import {
GALAXY_CLIENT_CONTEXT_KEY,
type GalaxyClientHandle,
} from "$lib/galaxy-client-context.svelte";
import type { CommandStatus, OrderCommand } from "../../sync/order-types";
import { submitOrder } from "../../sync/submit";
const draft = getContext<OrderDraftStore | undefined>(
ORDER_DRAFT_CONTEXT_KEY,
);
const gameState = getContext<GameStateStore | undefined>(
GAME_STATE_CONTEXT_KEY,
);
const galaxyClient = getContext<GalaxyClientHandle | undefined>(
GALAXY_CLIENT_CONTEXT_KEY,
);
function describe(cmd: { kind: string; label?: string }): string {
if (cmd.kind === "placeholder") return cmd.label ?? cmd.kind;
return cmd.kind;
const statusKeyMap: Record<CommandStatus, TranslationKey> = {
draft: "game.sidebar.order.status.draft",
valid: "game.sidebar.order.status.valid",
invalid: "game.sidebar.order.status.invalid",
submitting: "game.sidebar.order.status.submitting",
applied: "game.sidebar.order.status.applied",
rejected: "game.sidebar.order.status.rejected",
};
let submitInFlight = $state(false);
let submitError = $state<string | null>(null);
const submittable = $derived.by(() => {
if (draft === undefined) return [] as OrderCommand[];
return draft.commands.filter(
(cmd) => draft.statuses[cmd.id] === "valid",
);
});
const hasInvalid = $derived.by(() => {
if (draft === undefined) return false;
return draft.commands.some((cmd) => draft.statuses[cmd.id] === "invalid");
});
const submitDisabled = $derived(
draft === undefined ||
galaxyClient === undefined ||
galaxyClient.client === null ||
submitInFlight ||
submittable.length === 0 ||
hasInvalid,
);
function describe(cmd: OrderCommand): string {
switch (cmd.kind) {
case "placeholder":
return i18n.t("game.sidebar.order.label.placeholder", {
label: cmd.label,
});
case "planetRename":
return i18n.t("game.sidebar.order.label.planet_rename", {
planet: String(cmd.planetNumber),
name: cmd.name,
});
}
}
function statusOf(cmd: OrderCommand): CommandStatus {
return draft?.statuses[cmd.id] ?? "draft";
}
async function submit(): Promise<void> {
if (
draft === undefined ||
galaxyClient === undefined ||
galaxyClient.client === null ||
gameState === undefined
)
return;
if (submittable.length === 0 || hasInvalid) return;
const ids = submittable.map((cmd) => cmd.id);
const snapshot = submittable.slice();
submitInFlight = true;
submitError = null;
draft.markSubmitting(ids);
try {
const result = await submitOrder(
galaxyClient.client,
gameState.gameId,
snapshot,
{ updatedAt: draft.updatedAt },
);
if (result.ok) {
draft.applyResults({
results: result.results,
updatedAt: result.updatedAt,
});
if (gameState !== undefined) {
await gameState.refresh();
}
} else {
draft.markRejected(ids);
submitError = i18n.t("game.sidebar.order.error.batch_failed", {
message: result.message,
});
}
} catch (err) {
draft.revertSubmittingToValid();
submitError =
err instanceof Error ? err.message : "submit failed";
} finally {
submitInFlight = false;
}
}
</script>
@@ -38,11 +150,22 @@ construction (Vitest).
{:else}
<ol class="commands" data-testid="order-list">
{#each draft.commands as cmd, index (cmd.id)}
<li class="command" data-testid="order-command-{index}">
{@const status = statusOf(cmd)}
<li
class="command"
data-testid="order-command-{index}"
data-command-status={status}
>
<span class="index" aria-hidden="true">{index + 1}.</span>
<span class="label" data-testid="order-command-label-{index}">
{describe(cmd)}
</span>
<span
class="status status-{status}"
data-testid="order-command-status-{index}"
>
{i18n.t(statusKeyMap[status])}
</span>
<button
type="button"
class="delete"
@@ -54,6 +177,20 @@ construction (Vitest).
</li>
{/each}
</ol>
<button
type="button"
class="submit"
data-testid="order-submit"
disabled={submitDisabled}
onclick={() => void submit()}
>
{submitInFlight
? i18n.t("game.sidebar.order.submit_in_flight")
: i18n.t("game.sidebar.order.submit")}
</button>
{#if submitError !== null}
<p class="error" data-testid="order-submit-error">{submitError}</p>
{/if}
{/if}
</section>
@@ -72,14 +209,15 @@ construction (Vitest).
}
.commands {
list-style: none;
margin: 0;
margin: 0 0 0.75rem;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.command {
display: flex;
display: grid;
grid-template-columns: auto 1fr auto auto;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
@@ -88,17 +226,40 @@ construction (Vitest).
border-radius: 4px;
}
.index {
min-width: 1.5rem;
color: #aab;
font-variant-numeric: tabular-nums;
}
.label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.1rem 0.4rem;
border-radius: 999px;
border: 1px solid #2a3150;
color: #aab;
}
.status-applied {
color: #8be9a3;
border-color: #2f6d3f;
}
.status-rejected {
color: #d97a7a;
border-color: #6d2f2f;
}
.status-invalid {
color: #d6b86c;
border-color: #6d562f;
}
.status-submitting {
color: #6d8cff;
border-color: #2f3f6d;
}
.delete {
font: inherit;
font-size: 0.85rem;
@@ -113,4 +274,26 @@ construction (Vitest).
color: #e8eaf6;
border-color: #6d8cff;
}
.submit {
font: inherit;
font-size: 0.9rem;
padding: 0.4rem 1rem;
background: #1d2440;
color: #e8eaf6;
border: 1px solid #2a3150;
border-radius: 3px;
cursor: pointer;
}
.submit:not(:disabled):hover {
border-color: #6d8cff;
}
.submit:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.error {
margin: 0.5rem 0 0;
color: #d97a7a;
font-size: 0.85rem;
}
</style>
+98
View File
@@ -0,0 +1,98 @@
// TS port of `pkg/util/string.go.ValidateTypeName` — every entity
// name (planet, ship class, science, …) the player edits goes
// through this validator before reaching the order draft, so the
// client-side check is identical to the server-side one. A
// locally-valid name is always accepted at the wire level; an
// invalid name never produces a network round-trip.
const MAX_LENGTH = 30;
const ALLOWED_SPECIALS = new Set<string>("!@#$%^*-_=+~()[]{}");
const SPECIAL_RUN_LIMIT = 2;
/**
* EntityNameInvalidReason is the closed enumeration of reasons a
* name can fail validation. The values are stable identifiers so
* the inspector tooltip and the order-tab status row can map them
* to localised copy via `i18n.t("game.order.invalid." + reason)`.
*/
export type EntityNameInvalidReason =
| "empty"
| "too_long"
| "starts_with_special"
| "ends_with_special"
| "consecutive_specials"
| "whitespace"
| "disallowed_character";
export type EntityNameValidation =
| { ok: true; value: string }
| { ok: false; reason: EntityNameInvalidReason };
/**
* validateEntityName mirrors `ValidateTypeName` exactly: the input
* is trimmed, must be non-empty, must fit in 30 runes, must not
* start or end with a special character, and must contain only
* letters, digits, combining marks, or the allowed specials with at
* most two in a row. Returns the trimmed value on success or a
* structured reason on failure.
*/
export function validateEntityName(input: string): EntityNameValidation {
const trimmed = input.trim();
if (trimmed.length === 0) {
return { ok: false, reason: "empty" };
}
const runes = Array.from(trimmed);
if (runes.length > MAX_LENGTH) {
return { ok: false, reason: "too_long" };
}
const first = runes[0]!;
const last = runes[runes.length - 1]!;
if (ALLOWED_SPECIALS.has(first)) {
return { ok: false, reason: "starts_with_special" };
}
if (ALLOWED_SPECIALS.has(last)) {
return { ok: false, reason: "ends_with_special" };
}
let specialRun = 0;
for (const rune of runes) {
if (isWhitespace(rune)) {
return { ok: false, reason: "whitespace" };
}
if (isLetter(rune) || isDigit(rune) || isCombiningMark(rune)) {
specialRun = 0;
continue;
}
if (ALLOWED_SPECIALS.has(rune)) {
specialRun += 1;
if (specialRun > SPECIAL_RUN_LIMIT) {
return { ok: false, reason: "consecutive_specials" };
}
continue;
}
return { ok: false, reason: "disallowed_character" };
}
return { ok: true, value: trimmed };
}
function isWhitespace(rune: string): boolean {
// Matches Go's `unicode.IsSpace`.
return /\s/u.test(rune);
}
function isLetter(rune: string): boolean {
return /\p{L}/u.test(rune);
}
function isDigit(rune: string): boolean {
return /\p{N}/u.test(rune);
}
function isCombiningMark(rune: string): boolean {
return /\p{M}/u.test(rune);
}
+40
View File
@@ -0,0 +1,40 @@
// 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 { CommandFleetMerge, CommandFleetMergeT } from './order/command-fleet-merge.js';
export { CommandFleetSend, CommandFleetSendT } from './order/command-fleet-send.js';
export { CommandItem, CommandItemT } from './order/command-item.js';
export { CommandPayload } from './order/command-payload.js';
export { CommandPlanetProduce, CommandPlanetProduceT } from './order/command-planet-produce.js';
export { CommandPlanetRename, CommandPlanetRenameT } from './order/command-planet-rename.js';
export { CommandPlanetRouteRemove, CommandPlanetRouteRemoveT } from './order/command-planet-route-remove.js';
export { CommandPlanetRouteSet, CommandPlanetRouteSetT } from './order/command-planet-route-set.js';
export { CommandRaceQuit, CommandRaceQuitT } from './order/command-race-quit.js';
export { CommandRaceRelation, CommandRaceRelationT } from './order/command-race-relation.js';
export { CommandRaceVote, CommandRaceVoteT } from './order/command-race-vote.js';
export { CommandScienceCreate, CommandScienceCreateT } from './order/command-science-create.js';
export { CommandScienceRemove, CommandScienceRemoveT } from './order/command-science-remove.js';
export { CommandShipClassCreate, CommandShipClassCreateT } from './order/command-ship-class-create.js';
export { CommandShipClassMerge, CommandShipClassMergeT } from './order/command-ship-class-merge.js';
export { CommandShipClassRemove, CommandShipClassRemoveT } from './order/command-ship-class-remove.js';
export { CommandShipGroupBreak, CommandShipGroupBreakT } from './order/command-ship-group-break.js';
export { CommandShipGroupDismantle, CommandShipGroupDismantleT } from './order/command-ship-group-dismantle.js';
export { CommandShipGroupJoinFleet, CommandShipGroupJoinFleetT } from './order/command-ship-group-join-fleet.js';
export { CommandShipGroupLoad, CommandShipGroupLoadT } from './order/command-ship-group-load.js';
export { CommandShipGroupMerge, CommandShipGroupMergeT } from './order/command-ship-group-merge.js';
export { CommandShipGroupSend, CommandShipGroupSendT } from './order/command-ship-group-send.js';
export { CommandShipGroupTransfer, CommandShipGroupTransferT } from './order/command-ship-group-transfer.js';
export { CommandShipGroupUnload, CommandShipGroupUnloadT } from './order/command-ship-group-unload.js';
export { CommandShipGroupUpgrade, CommandShipGroupUpgradeT } from './order/command-ship-group-upgrade.js';
export { PlanetProduction } from './order/planet-production.js';
export { PlanetRouteLoadType } from './order/planet-route-load-type.js';
export { Relation } from './order/relation.js';
export { ShipGroupCargo } from './order/ship-group-cargo.js';
export { ShipGroupUpgradeTech } from './order/ship-group-upgrade-tech.js';
export { UserGamesCommand, UserGamesCommandT } from './order/user-games-command.js';
export { UserGamesCommandResponse, UserGamesCommandResponseT } from './order/user-games-command-response.js';
export { UserGamesOrder, UserGamesOrderT } from './order/user-games-order.js';
export { UserGamesOrderGet, UserGamesOrderGetT } from './order/user-games-order-get.js';
export { UserGamesOrderGetResponse, UserGamesOrderGetResponseT } from './order/user-games-order-get-response.js';
export { UserGamesOrderResponse, UserGamesOrderResponseT } from './order/user-games-order-response.js';
@@ -0,0 +1,95 @@
// 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 CommandFleetMerge implements flatbuffers.IUnpackableObject<CommandFleetMergeT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandFleetMerge {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandFleetMerge(bb:flatbuffers.ByteBuffer, obj?:CommandFleetMerge):CommandFleetMerge {
return (obj || new CommandFleetMerge()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandFleetMerge(bb:flatbuffers.ByteBuffer, obj?:CommandFleetMerge):CommandFleetMerge {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandFleetMerge()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
name():string|null
name(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
name(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;
}
target():string|null
target(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
target(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startCommandFleetMerge(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, nameOffset, 0);
}
static addTarget(builder:flatbuffers.Builder, targetOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, targetOffset, 0);
}
static endCommandFleetMerge(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandFleetMerge(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset, targetOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandFleetMerge.startCommandFleetMerge(builder);
CommandFleetMerge.addName(builder, nameOffset);
CommandFleetMerge.addTarget(builder, targetOffset);
return CommandFleetMerge.endCommandFleetMerge(builder);
}
unpack(): CommandFleetMergeT {
return new CommandFleetMergeT(
this.name(),
this.target()
);
}
unpackTo(_o: CommandFleetMergeT): void {
_o.name = this.name();
_o.target = this.target();
}
}
export class CommandFleetMergeT implements flatbuffers.IGeneratedObject {
constructor(
public name: string|Uint8Array|null = null,
public target: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const name = (this.name !== null ? builder.createString(this.name!) : 0);
const target = (this.target !== null ? builder.createString(this.target!) : 0);
return CommandFleetMerge.createCommandFleetMerge(builder,
name,
target
);
}
}
@@ -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 CommandFleetSend implements flatbuffers.IUnpackableObject<CommandFleetSendT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandFleetSend {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandFleetSend(bb:flatbuffers.ByteBuffer, obj?:CommandFleetSend):CommandFleetSend {
return (obj || new CommandFleetSend()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandFleetSend(bb:flatbuffers.ByteBuffer, obj?:CommandFleetSend):CommandFleetSend {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandFleetSend()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
name():string|null
name(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
name(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;
}
destination():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
static startCommandFleetSend(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, nameOffset, 0);
}
static addDestination(builder:flatbuffers.Builder, destination:bigint) {
builder.addFieldInt64(1, destination, BigInt('0'));
}
static endCommandFleetSend(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandFleetSend(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset, destination:bigint):flatbuffers.Offset {
CommandFleetSend.startCommandFleetSend(builder);
CommandFleetSend.addName(builder, nameOffset);
CommandFleetSend.addDestination(builder, destination);
return CommandFleetSend.endCommandFleetSend(builder);
}
unpack(): CommandFleetSendT {
return new CommandFleetSendT(
this.name(),
this.destination()
);
}
unpackTo(_o: CommandFleetSendT): void {
_o.name = this.name();
_o.destination = this.destination();
}
}
export class CommandFleetSendT implements flatbuffers.IGeneratedObject {
constructor(
public name: string|Uint8Array|null = null,
public destination: bigint = BigInt('0')
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const name = (this.name !== null ? builder.createString(this.name!) : 0);
return CommandFleetSend.createCommandFleetSend(builder,
name,
this.destination
);
}
}
@@ -0,0 +1,170 @@
// 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 { CommandFleetMerge, CommandFleetMergeT } from './command-fleet-merge.js';
import { CommandFleetSend, CommandFleetSendT } from './command-fleet-send.js';
import { CommandPayload, unionToCommandPayload, unionListToCommandPayload } from './command-payload.js';
import { CommandPlanetProduce, CommandPlanetProduceT } from './command-planet-produce.js';
import { CommandPlanetRename, CommandPlanetRenameT } from './command-planet-rename.js';
import { CommandPlanetRouteRemove, CommandPlanetRouteRemoveT } from './command-planet-route-remove.js';
import { CommandPlanetRouteSet, CommandPlanetRouteSetT } from './command-planet-route-set.js';
import { CommandRaceQuit, CommandRaceQuitT } from './command-race-quit.js';
import { CommandRaceRelation, CommandRaceRelationT } from './command-race-relation.js';
import { CommandRaceVote, CommandRaceVoteT } from './command-race-vote.js';
import { CommandScienceCreate, CommandScienceCreateT } from './command-science-create.js';
import { CommandScienceRemove, CommandScienceRemoveT } from './command-science-remove.js';
import { CommandShipClassCreate, CommandShipClassCreateT } from './command-ship-class-create.js';
import { CommandShipClassMerge, CommandShipClassMergeT } from './command-ship-class-merge.js';
import { CommandShipClassRemove, CommandShipClassRemoveT } from './command-ship-class-remove.js';
import { CommandShipGroupBreak, CommandShipGroupBreakT } from './command-ship-group-break.js';
import { CommandShipGroupDismantle, CommandShipGroupDismantleT } from './command-ship-group-dismantle.js';
import { CommandShipGroupJoinFleet, CommandShipGroupJoinFleetT } from './command-ship-group-join-fleet.js';
import { CommandShipGroupLoad, CommandShipGroupLoadT } from './command-ship-group-load.js';
import { CommandShipGroupMerge, CommandShipGroupMergeT } from './command-ship-group-merge.js';
import { CommandShipGroupSend, CommandShipGroupSendT } from './command-ship-group-send.js';
import { CommandShipGroupTransfer, CommandShipGroupTransferT } from './command-ship-group-transfer.js';
import { CommandShipGroupUnload, CommandShipGroupUnloadT } from './command-ship-group-unload.js';
import { CommandShipGroupUpgrade, CommandShipGroupUpgradeT } from './command-ship-group-upgrade.js';
export class CommandItem implements flatbuffers.IUnpackableObject<CommandItemT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandItem {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandItem(bb:flatbuffers.ByteBuffer, obj?:CommandItem):CommandItem {
return (obj || new CommandItem()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandItem(bb:flatbuffers.ByteBuffer, obj?:CommandItem):CommandItem {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandItem()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
cmdId():string|null
cmdId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
cmdId(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;
}
cmdApplied():boolean|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : null;
}
cmdErrorCode():bigint|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : null;
}
payloadType():CommandPayload {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readUint8(this.bb_pos + offset) : CommandPayload.NONE;
}
payload<T extends flatbuffers.Table>(obj:any):any|null {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.__union(obj, this.bb_pos + offset) : null;
}
static startCommandItem(builder:flatbuffers.Builder) {
builder.startObject(5);
}
static addCmdId(builder:flatbuffers.Builder, cmdIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, cmdIdOffset, 0);
}
static addCmdApplied(builder:flatbuffers.Builder, cmdApplied:boolean) {
builder.addFieldInt8(1, +cmdApplied, null);
}
static addCmdErrorCode(builder:flatbuffers.Builder, cmdErrorCode:bigint) {
builder.addFieldInt64(2, cmdErrorCode, null);
}
static addPayloadType(builder:flatbuffers.Builder, payloadType:CommandPayload) {
builder.addFieldInt8(3, payloadType, CommandPayload.NONE);
}
static addPayload(builder:flatbuffers.Builder, payloadOffset:flatbuffers.Offset) {
builder.addFieldOffset(4, payloadOffset, 0);
}
static endCommandItem(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 12) // payload
return offset;
}
static createCommandItem(builder:flatbuffers.Builder, cmdIdOffset:flatbuffers.Offset, cmdApplied:boolean|null, cmdErrorCode:bigint|null, payloadType:CommandPayload, payloadOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandItem.startCommandItem(builder);
CommandItem.addCmdId(builder, cmdIdOffset);
if (cmdApplied !== null)
CommandItem.addCmdApplied(builder, cmdApplied);
if (cmdErrorCode !== null)
CommandItem.addCmdErrorCode(builder, cmdErrorCode);
CommandItem.addPayloadType(builder, payloadType);
CommandItem.addPayload(builder, payloadOffset);
return CommandItem.endCommandItem(builder);
}
unpack(): CommandItemT {
return new CommandItemT(
this.cmdId(),
this.cmdApplied(),
this.cmdErrorCode(),
this.payloadType(),
(() => {
const temp = unionToCommandPayload(this.payloadType(), this.payload.bind(this));
if(temp === null) { return null; }
return temp.unpack()
})()
);
}
unpackTo(_o: CommandItemT): void {
_o.cmdId = this.cmdId();
_o.cmdApplied = this.cmdApplied();
_o.cmdErrorCode = this.cmdErrorCode();
_o.payloadType = this.payloadType();
_o.payload = (() => {
const temp = unionToCommandPayload(this.payloadType(), this.payload.bind(this));
if(temp === null) { return null; }
return temp.unpack()
})();
}
}
export class CommandItemT implements flatbuffers.IGeneratedObject {
constructor(
public cmdId: string|Uint8Array|null = null,
public cmdApplied: boolean|null = null,
public cmdErrorCode: bigint|null = null,
public payloadType: CommandPayload = CommandPayload.NONE,
public payload: CommandFleetMergeT|CommandFleetSendT|CommandPlanetProduceT|CommandPlanetRenameT|CommandPlanetRouteRemoveT|CommandPlanetRouteSetT|CommandRaceQuitT|CommandRaceRelationT|CommandRaceVoteT|CommandScienceCreateT|CommandScienceRemoveT|CommandShipClassCreateT|CommandShipClassMergeT|CommandShipClassRemoveT|CommandShipGroupBreakT|CommandShipGroupDismantleT|CommandShipGroupJoinFleetT|CommandShipGroupLoadT|CommandShipGroupMergeT|CommandShipGroupSendT|CommandShipGroupTransferT|CommandShipGroupUnloadT|CommandShipGroupUpgradeT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const cmdId = (this.cmdId !== null ? builder.createString(this.cmdId!) : 0);
const payload = builder.createObjectOffset(this.payload);
return CommandItem.createCommandItem(builder,
cmdId,
this.cmdApplied,
this.cmdErrorCode,
this.payloadType,
payload
);
}
}
@@ -0,0 +1,122 @@
// 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 { CommandFleetMerge, CommandFleetMergeT } from './command-fleet-merge.js';
import { CommandFleetSend, CommandFleetSendT } from './command-fleet-send.js';
import { CommandPlanetProduce, CommandPlanetProduceT } from './command-planet-produce.js';
import { CommandPlanetRename, CommandPlanetRenameT } from './command-planet-rename.js';
import { CommandPlanetRouteRemove, CommandPlanetRouteRemoveT } from './command-planet-route-remove.js';
import { CommandPlanetRouteSet, CommandPlanetRouteSetT } from './command-planet-route-set.js';
import { CommandRaceQuit, CommandRaceQuitT } from './command-race-quit.js';
import { CommandRaceRelation, CommandRaceRelationT } from './command-race-relation.js';
import { CommandRaceVote, CommandRaceVoteT } from './command-race-vote.js';
import { CommandScienceCreate, CommandScienceCreateT } from './command-science-create.js';
import { CommandScienceRemove, CommandScienceRemoveT } from './command-science-remove.js';
import { CommandShipClassCreate, CommandShipClassCreateT } from './command-ship-class-create.js';
import { CommandShipClassMerge, CommandShipClassMergeT } from './command-ship-class-merge.js';
import { CommandShipClassRemove, CommandShipClassRemoveT } from './command-ship-class-remove.js';
import { CommandShipGroupBreak, CommandShipGroupBreakT } from './command-ship-group-break.js';
import { CommandShipGroupDismantle, CommandShipGroupDismantleT } from './command-ship-group-dismantle.js';
import { CommandShipGroupJoinFleet, CommandShipGroupJoinFleetT } from './command-ship-group-join-fleet.js';
import { CommandShipGroupLoad, CommandShipGroupLoadT } from './command-ship-group-load.js';
import { CommandShipGroupMerge, CommandShipGroupMergeT } from './command-ship-group-merge.js';
import { CommandShipGroupSend, CommandShipGroupSendT } from './command-ship-group-send.js';
import { CommandShipGroupTransfer, CommandShipGroupTransferT } from './command-ship-group-transfer.js';
import { CommandShipGroupUnload, CommandShipGroupUnloadT } from './command-ship-group-unload.js';
import { CommandShipGroupUpgrade, CommandShipGroupUpgradeT } from './command-ship-group-upgrade.js';
export enum CommandPayload {
NONE = 0,
CommandRaceQuit = 1,
CommandRaceVote = 2,
CommandRaceRelation = 3,
CommandShipClassCreate = 4,
CommandShipClassMerge = 5,
CommandShipClassRemove = 6,
CommandShipGroupBreak = 7,
CommandShipGroupLoad = 8,
CommandShipGroupUnload = 9,
CommandShipGroupSend = 10,
CommandShipGroupUpgrade = 11,
CommandShipGroupMerge = 12,
CommandShipGroupDismantle = 13,
CommandShipGroupTransfer = 14,
CommandShipGroupJoinFleet = 15,
CommandFleetMerge = 16,
CommandFleetSend = 17,
CommandScienceCreate = 18,
CommandScienceRemove = 19,
CommandPlanetRename = 20,
CommandPlanetProduce = 21,
CommandPlanetRouteSet = 22,
CommandPlanetRouteRemove = 23
}
export function unionToCommandPayload(
type: CommandPayload,
accessor: (obj:CommandFleetMerge|CommandFleetSend|CommandPlanetProduce|CommandPlanetRename|CommandPlanetRouteRemove|CommandPlanetRouteSet|CommandRaceQuit|CommandRaceRelation|CommandRaceVote|CommandScienceCreate|CommandScienceRemove|CommandShipClassCreate|CommandShipClassMerge|CommandShipClassRemove|CommandShipGroupBreak|CommandShipGroupDismantle|CommandShipGroupJoinFleet|CommandShipGroupLoad|CommandShipGroupMerge|CommandShipGroupSend|CommandShipGroupTransfer|CommandShipGroupUnload|CommandShipGroupUpgrade) => CommandFleetMerge|CommandFleetSend|CommandPlanetProduce|CommandPlanetRename|CommandPlanetRouteRemove|CommandPlanetRouteSet|CommandRaceQuit|CommandRaceRelation|CommandRaceVote|CommandScienceCreate|CommandScienceRemove|CommandShipClassCreate|CommandShipClassMerge|CommandShipClassRemove|CommandShipGroupBreak|CommandShipGroupDismantle|CommandShipGroupJoinFleet|CommandShipGroupLoad|CommandShipGroupMerge|CommandShipGroupSend|CommandShipGroupTransfer|CommandShipGroupUnload|CommandShipGroupUpgrade|null
): CommandFleetMerge|CommandFleetSend|CommandPlanetProduce|CommandPlanetRename|CommandPlanetRouteRemove|CommandPlanetRouteSet|CommandRaceQuit|CommandRaceRelation|CommandRaceVote|CommandScienceCreate|CommandScienceRemove|CommandShipClassCreate|CommandShipClassMerge|CommandShipClassRemove|CommandShipGroupBreak|CommandShipGroupDismantle|CommandShipGroupJoinFleet|CommandShipGroupLoad|CommandShipGroupMerge|CommandShipGroupSend|CommandShipGroupTransfer|CommandShipGroupUnload|CommandShipGroupUpgrade|null {
switch(CommandPayload[type]) {
case 'NONE': return null;
case 'CommandRaceQuit': return accessor(new CommandRaceQuit())! as CommandRaceQuit;
case 'CommandRaceVote': return accessor(new CommandRaceVote())! as CommandRaceVote;
case 'CommandRaceRelation': return accessor(new CommandRaceRelation())! as CommandRaceRelation;
case 'CommandShipClassCreate': return accessor(new CommandShipClassCreate())! as CommandShipClassCreate;
case 'CommandShipClassMerge': return accessor(new CommandShipClassMerge())! as CommandShipClassMerge;
case 'CommandShipClassRemove': return accessor(new CommandShipClassRemove())! as CommandShipClassRemove;
case 'CommandShipGroupBreak': return accessor(new CommandShipGroupBreak())! as CommandShipGroupBreak;
case 'CommandShipGroupLoad': return accessor(new CommandShipGroupLoad())! as CommandShipGroupLoad;
case 'CommandShipGroupUnload': return accessor(new CommandShipGroupUnload())! as CommandShipGroupUnload;
case 'CommandShipGroupSend': return accessor(new CommandShipGroupSend())! as CommandShipGroupSend;
case 'CommandShipGroupUpgrade': return accessor(new CommandShipGroupUpgrade())! as CommandShipGroupUpgrade;
case 'CommandShipGroupMerge': return accessor(new CommandShipGroupMerge())! as CommandShipGroupMerge;
case 'CommandShipGroupDismantle': return accessor(new CommandShipGroupDismantle())! as CommandShipGroupDismantle;
case 'CommandShipGroupTransfer': return accessor(new CommandShipGroupTransfer())! as CommandShipGroupTransfer;
case 'CommandShipGroupJoinFleet': return accessor(new CommandShipGroupJoinFleet())! as CommandShipGroupJoinFleet;
case 'CommandFleetMerge': return accessor(new CommandFleetMerge())! as CommandFleetMerge;
case 'CommandFleetSend': return accessor(new CommandFleetSend())! as CommandFleetSend;
case 'CommandScienceCreate': return accessor(new CommandScienceCreate())! as CommandScienceCreate;
case 'CommandScienceRemove': return accessor(new CommandScienceRemove())! as CommandScienceRemove;
case 'CommandPlanetRename': return accessor(new CommandPlanetRename())! as CommandPlanetRename;
case 'CommandPlanetProduce': return accessor(new CommandPlanetProduce())! as CommandPlanetProduce;
case 'CommandPlanetRouteSet': return accessor(new CommandPlanetRouteSet())! as CommandPlanetRouteSet;
case 'CommandPlanetRouteRemove': return accessor(new CommandPlanetRouteRemove())! as CommandPlanetRouteRemove;
default: return null;
}
}
export function unionListToCommandPayload(
type: CommandPayload,
accessor: (index: number, obj:CommandFleetMerge|CommandFleetSend|CommandPlanetProduce|CommandPlanetRename|CommandPlanetRouteRemove|CommandPlanetRouteSet|CommandRaceQuit|CommandRaceRelation|CommandRaceVote|CommandScienceCreate|CommandScienceRemove|CommandShipClassCreate|CommandShipClassMerge|CommandShipClassRemove|CommandShipGroupBreak|CommandShipGroupDismantle|CommandShipGroupJoinFleet|CommandShipGroupLoad|CommandShipGroupMerge|CommandShipGroupSend|CommandShipGroupTransfer|CommandShipGroupUnload|CommandShipGroupUpgrade) => CommandFleetMerge|CommandFleetSend|CommandPlanetProduce|CommandPlanetRename|CommandPlanetRouteRemove|CommandPlanetRouteSet|CommandRaceQuit|CommandRaceRelation|CommandRaceVote|CommandScienceCreate|CommandScienceRemove|CommandShipClassCreate|CommandShipClassMerge|CommandShipClassRemove|CommandShipGroupBreak|CommandShipGroupDismantle|CommandShipGroupJoinFleet|CommandShipGroupLoad|CommandShipGroupMerge|CommandShipGroupSend|CommandShipGroupTransfer|CommandShipGroupUnload|CommandShipGroupUpgrade|null,
index: number
): CommandFleetMerge|CommandFleetSend|CommandPlanetProduce|CommandPlanetRename|CommandPlanetRouteRemove|CommandPlanetRouteSet|CommandRaceQuit|CommandRaceRelation|CommandRaceVote|CommandScienceCreate|CommandScienceRemove|CommandShipClassCreate|CommandShipClassMerge|CommandShipClassRemove|CommandShipGroupBreak|CommandShipGroupDismantle|CommandShipGroupJoinFleet|CommandShipGroupLoad|CommandShipGroupMerge|CommandShipGroupSend|CommandShipGroupTransfer|CommandShipGroupUnload|CommandShipGroupUpgrade|null {
switch(CommandPayload[type]) {
case 'NONE': return null;
case 'CommandRaceQuit': return accessor(index, new CommandRaceQuit())! as CommandRaceQuit;
case 'CommandRaceVote': return accessor(index, new CommandRaceVote())! as CommandRaceVote;
case 'CommandRaceRelation': return accessor(index, new CommandRaceRelation())! as CommandRaceRelation;
case 'CommandShipClassCreate': return accessor(index, new CommandShipClassCreate())! as CommandShipClassCreate;
case 'CommandShipClassMerge': return accessor(index, new CommandShipClassMerge())! as CommandShipClassMerge;
case 'CommandShipClassRemove': return accessor(index, new CommandShipClassRemove())! as CommandShipClassRemove;
case 'CommandShipGroupBreak': return accessor(index, new CommandShipGroupBreak())! as CommandShipGroupBreak;
case 'CommandShipGroupLoad': return accessor(index, new CommandShipGroupLoad())! as CommandShipGroupLoad;
case 'CommandShipGroupUnload': return accessor(index, new CommandShipGroupUnload())! as CommandShipGroupUnload;
case 'CommandShipGroupSend': return accessor(index, new CommandShipGroupSend())! as CommandShipGroupSend;
case 'CommandShipGroupUpgrade': return accessor(index, new CommandShipGroupUpgrade())! as CommandShipGroupUpgrade;
case 'CommandShipGroupMerge': return accessor(index, new CommandShipGroupMerge())! as CommandShipGroupMerge;
case 'CommandShipGroupDismantle': return accessor(index, new CommandShipGroupDismantle())! as CommandShipGroupDismantle;
case 'CommandShipGroupTransfer': return accessor(index, new CommandShipGroupTransfer())! as CommandShipGroupTransfer;
case 'CommandShipGroupJoinFleet': return accessor(index, new CommandShipGroupJoinFleet())! as CommandShipGroupJoinFleet;
case 'CommandFleetMerge': return accessor(index, new CommandFleetMerge())! as CommandFleetMerge;
case 'CommandFleetSend': return accessor(index, new CommandFleetSend())! as CommandFleetSend;
case 'CommandScienceCreate': return accessor(index, new CommandScienceCreate())! as CommandScienceCreate;
case 'CommandScienceRemove': return accessor(index, new CommandScienceRemove())! as CommandScienceRemove;
case 'CommandPlanetRename': return accessor(index, new CommandPlanetRename())! as CommandPlanetRename;
case 'CommandPlanetProduce': return accessor(index, new CommandPlanetProduce())! as CommandPlanetProduce;
case 'CommandPlanetRouteSet': return accessor(index, new CommandPlanetRouteSet())! as CommandPlanetRouteSet;
case 'CommandPlanetRouteRemove': return accessor(index, new CommandPlanetRouteRemove())! as CommandPlanetRouteRemove;
default: return null;
}
}
@@ -0,0 +1,107 @@
// 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 { PlanetProduction } from './planet-production.js';
export class CommandPlanetProduce implements flatbuffers.IUnpackableObject<CommandPlanetProduceT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandPlanetProduce {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandPlanetProduce(bb:flatbuffers.ByteBuffer, obj?:CommandPlanetProduce):CommandPlanetProduce {
return (obj || new CommandPlanetProduce()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandPlanetProduce(bb:flatbuffers.ByteBuffer, obj?:CommandPlanetProduce):CommandPlanetProduce {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandPlanetProduce()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
number():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
production():PlanetProduction {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt8(this.bb_pos + offset) : PlanetProduction.UNKNOWN;
}
subject():string|null
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
subject(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;
}
static startCommandPlanetProduce(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addNumber(builder:flatbuffers.Builder, number:bigint) {
builder.addFieldInt64(0, number, BigInt('0'));
}
static addProduction(builder:flatbuffers.Builder, production:PlanetProduction) {
builder.addFieldInt8(1, production, PlanetProduction.UNKNOWN);
}
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, subjectOffset, 0);
}
static endCommandPlanetProduce(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandPlanetProduce(builder:flatbuffers.Builder, number:bigint, production:PlanetProduction, subjectOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandPlanetProduce.startCommandPlanetProduce(builder);
CommandPlanetProduce.addNumber(builder, number);
CommandPlanetProduce.addProduction(builder, production);
CommandPlanetProduce.addSubject(builder, subjectOffset);
return CommandPlanetProduce.endCommandPlanetProduce(builder);
}
unpack(): CommandPlanetProduceT {
return new CommandPlanetProduceT(
this.number(),
this.production(),
this.subject()
);
}
unpackTo(_o: CommandPlanetProduceT): void {
_o.number = this.number();
_o.production = this.production();
_o.subject = this.subject();
}
}
export class CommandPlanetProduceT implements flatbuffers.IGeneratedObject {
constructor(
public number: bigint = BigInt('0'),
public production: PlanetProduction = PlanetProduction.UNKNOWN,
public subject: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
return CommandPlanetProduce.createCommandPlanetProduce(builder,
this.number,
this.production,
subject
);
}
}
@@ -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 CommandPlanetRename implements flatbuffers.IUnpackableObject<CommandPlanetRenameT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandPlanetRename {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandPlanetRename(bb:flatbuffers.ByteBuffer, obj?:CommandPlanetRename):CommandPlanetRename {
return (obj || new CommandPlanetRename()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandPlanetRename(bb:flatbuffers.ByteBuffer, obj?:CommandPlanetRename):CommandPlanetRename {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandPlanetRename()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
number():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
name():string|null
name(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
name(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startCommandPlanetRename(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addNumber(builder:flatbuffers.Builder, number:bigint) {
builder.addFieldInt64(0, number, BigInt('0'));
}
static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, nameOffset, 0);
}
static endCommandPlanetRename(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandPlanetRename(builder:flatbuffers.Builder, number:bigint, nameOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandPlanetRename.startCommandPlanetRename(builder);
CommandPlanetRename.addNumber(builder, number);
CommandPlanetRename.addName(builder, nameOffset);
return CommandPlanetRename.endCommandPlanetRename(builder);
}
unpack(): CommandPlanetRenameT {
return new CommandPlanetRenameT(
this.number(),
this.name()
);
}
unpackTo(_o: CommandPlanetRenameT): void {
_o.number = this.number();
_o.name = this.name();
}
}
export class CommandPlanetRenameT implements flatbuffers.IGeneratedObject {
constructor(
public number: bigint = BigInt('0'),
public name: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const name = (this.name !== null ? builder.createString(this.name!) : 0);
return CommandPlanetRename.createCommandPlanetRename(builder,
this.number,
name
);
}
}
@@ -0,0 +1,89 @@
// 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 { PlanetRouteLoadType } from './planet-route-load-type.js';
export class CommandPlanetRouteRemove implements flatbuffers.IUnpackableObject<CommandPlanetRouteRemoveT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandPlanetRouteRemove {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandPlanetRouteRemove(bb:flatbuffers.ByteBuffer, obj?:CommandPlanetRouteRemove):CommandPlanetRouteRemove {
return (obj || new CommandPlanetRouteRemove()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandPlanetRouteRemove(bb:flatbuffers.ByteBuffer, obj?:CommandPlanetRouteRemove):CommandPlanetRouteRemove {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandPlanetRouteRemove()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
origin():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
loadType():PlanetRouteLoadType {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt8(this.bb_pos + offset) : PlanetRouteLoadType.UNKNOWN;
}
static startCommandPlanetRouteRemove(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addOrigin(builder:flatbuffers.Builder, origin:bigint) {
builder.addFieldInt64(0, origin, BigInt('0'));
}
static addLoadType(builder:flatbuffers.Builder, loadType:PlanetRouteLoadType) {
builder.addFieldInt8(1, loadType, PlanetRouteLoadType.UNKNOWN);
}
static endCommandPlanetRouteRemove(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandPlanetRouteRemove(builder:flatbuffers.Builder, origin:bigint, loadType:PlanetRouteLoadType):flatbuffers.Offset {
CommandPlanetRouteRemove.startCommandPlanetRouteRemove(builder);
CommandPlanetRouteRemove.addOrigin(builder, origin);
CommandPlanetRouteRemove.addLoadType(builder, loadType);
return CommandPlanetRouteRemove.endCommandPlanetRouteRemove(builder);
}
unpack(): CommandPlanetRouteRemoveT {
return new CommandPlanetRouteRemoveT(
this.origin(),
this.loadType()
);
}
unpackTo(_o: CommandPlanetRouteRemoveT): void {
_o.origin = this.origin();
_o.loadType = this.loadType();
}
}
export class CommandPlanetRouteRemoveT implements flatbuffers.IGeneratedObject {
constructor(
public origin: bigint = BigInt('0'),
public loadType: PlanetRouteLoadType = PlanetRouteLoadType.UNKNOWN
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return CommandPlanetRouteRemove.createCommandPlanetRouteRemove(builder,
this.origin,
this.loadType
);
}
}
@@ -0,0 +1,103 @@
// 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 { PlanetRouteLoadType } from './planet-route-load-type.js';
export class CommandPlanetRouteSet implements flatbuffers.IUnpackableObject<CommandPlanetRouteSetT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandPlanetRouteSet {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandPlanetRouteSet(bb:flatbuffers.ByteBuffer, obj?:CommandPlanetRouteSet):CommandPlanetRouteSet {
return (obj || new CommandPlanetRouteSet()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandPlanetRouteSet(bb:flatbuffers.ByteBuffer, obj?:CommandPlanetRouteSet):CommandPlanetRouteSet {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandPlanetRouteSet()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
origin():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
destination():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
loadType():PlanetRouteLoadType {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readInt8(this.bb_pos + offset) : PlanetRouteLoadType.UNKNOWN;
}
static startCommandPlanetRouteSet(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addOrigin(builder:flatbuffers.Builder, origin:bigint) {
builder.addFieldInt64(0, origin, BigInt('0'));
}
static addDestination(builder:flatbuffers.Builder, destination:bigint) {
builder.addFieldInt64(1, destination, BigInt('0'));
}
static addLoadType(builder:flatbuffers.Builder, loadType:PlanetRouteLoadType) {
builder.addFieldInt8(2, loadType, PlanetRouteLoadType.UNKNOWN);
}
static endCommandPlanetRouteSet(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandPlanetRouteSet(builder:flatbuffers.Builder, origin:bigint, destination:bigint, loadType:PlanetRouteLoadType):flatbuffers.Offset {
CommandPlanetRouteSet.startCommandPlanetRouteSet(builder);
CommandPlanetRouteSet.addOrigin(builder, origin);
CommandPlanetRouteSet.addDestination(builder, destination);
CommandPlanetRouteSet.addLoadType(builder, loadType);
return CommandPlanetRouteSet.endCommandPlanetRouteSet(builder);
}
unpack(): CommandPlanetRouteSetT {
return new CommandPlanetRouteSetT(
this.origin(),
this.destination(),
this.loadType()
);
}
unpackTo(_o: CommandPlanetRouteSetT): void {
_o.origin = this.origin();
_o.destination = this.destination();
_o.loadType = this.loadType();
}
}
export class CommandPlanetRouteSetT implements flatbuffers.IGeneratedObject {
constructor(
public origin: bigint = BigInt('0'),
public destination: bigint = BigInt('0'),
public loadType: PlanetRouteLoadType = PlanetRouteLoadType.UNKNOWN
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return CommandPlanetRouteSet.createCommandPlanetRouteSet(builder,
this.origin,
this.destination,
this.loadType
);
}
}
@@ -0,0 +1,56 @@
// 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 CommandRaceQuit implements flatbuffers.IUnpackableObject<CommandRaceQuitT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandRaceQuit {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandRaceQuit(bb:flatbuffers.ByteBuffer, obj?:CommandRaceQuit):CommandRaceQuit {
return (obj || new CommandRaceQuit()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandRaceQuit(bb:flatbuffers.ByteBuffer, obj?:CommandRaceQuit):CommandRaceQuit {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandRaceQuit()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static startCommandRaceQuit(builder:flatbuffers.Builder) {
builder.startObject(0);
}
static endCommandRaceQuit(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandRaceQuit(builder:flatbuffers.Builder):flatbuffers.Offset {
CommandRaceQuit.startCommandRaceQuit(builder);
return CommandRaceQuit.endCommandRaceQuit(builder);
}
unpack(): CommandRaceQuitT {
return new CommandRaceQuitT();
}
unpackTo(_o: CommandRaceQuitT): void {}
}
export class CommandRaceQuitT implements flatbuffers.IGeneratedObject {
constructor(){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return CommandRaceQuit.createCommandRaceQuit(builder);
}
}
@@ -0,0 +1,93 @@
// 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 { Relation } from './relation.js';
export class CommandRaceRelation implements flatbuffers.IUnpackableObject<CommandRaceRelationT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandRaceRelation {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandRaceRelation(bb:flatbuffers.ByteBuffer, obj?:CommandRaceRelation):CommandRaceRelation {
return (obj || new CommandRaceRelation()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandRaceRelation(bb:flatbuffers.ByteBuffer, obj?:CommandRaceRelation):CommandRaceRelation {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandRaceRelation()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
acceptor():string|null
acceptor(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
acceptor(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;
}
relation():Relation {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt8(this.bb_pos + offset) : Relation.UNKNOWN;
}
static startCommandRaceRelation(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addAcceptor(builder:flatbuffers.Builder, acceptorOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, acceptorOffset, 0);
}
static addRelation(builder:flatbuffers.Builder, relation:Relation) {
builder.addFieldInt8(1, relation, Relation.UNKNOWN);
}
static endCommandRaceRelation(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandRaceRelation(builder:flatbuffers.Builder, acceptorOffset:flatbuffers.Offset, relation:Relation):flatbuffers.Offset {
CommandRaceRelation.startCommandRaceRelation(builder);
CommandRaceRelation.addAcceptor(builder, acceptorOffset);
CommandRaceRelation.addRelation(builder, relation);
return CommandRaceRelation.endCommandRaceRelation(builder);
}
unpack(): CommandRaceRelationT {
return new CommandRaceRelationT(
this.acceptor(),
this.relation()
);
}
unpackTo(_o: CommandRaceRelationT): void {
_o.acceptor = this.acceptor();
_o.relation = this.relation();
}
}
export class CommandRaceRelationT implements flatbuffers.IGeneratedObject {
constructor(
public acceptor: string|Uint8Array|null = null,
public relation: Relation = Relation.UNKNOWN
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const acceptor = (this.acceptor !== null ? builder.createString(this.acceptor!) : 0);
return CommandRaceRelation.createCommandRaceRelation(builder,
acceptor,
this.relation
);
}
}
@@ -0,0 +1,78 @@
// 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 CommandRaceVote implements flatbuffers.IUnpackableObject<CommandRaceVoteT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandRaceVote {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandRaceVote(bb:flatbuffers.ByteBuffer, obj?:CommandRaceVote):CommandRaceVote {
return (obj || new CommandRaceVote()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandRaceVote(bb:flatbuffers.ByteBuffer, obj?:CommandRaceVote):CommandRaceVote {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandRaceVote()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
acceptor():string|null
acceptor(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
acceptor(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;
}
static startCommandRaceVote(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addAcceptor(builder:flatbuffers.Builder, acceptorOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, acceptorOffset, 0);
}
static endCommandRaceVote(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandRaceVote(builder:flatbuffers.Builder, acceptorOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandRaceVote.startCommandRaceVote(builder);
CommandRaceVote.addAcceptor(builder, acceptorOffset);
return CommandRaceVote.endCommandRaceVote(builder);
}
unpack(): CommandRaceVoteT {
return new CommandRaceVoteT(
this.acceptor()
);
}
unpackTo(_o: CommandRaceVoteT): void {
_o.acceptor = this.acceptor();
}
}
export class CommandRaceVoteT implements flatbuffers.IGeneratedObject {
constructor(
public acceptor: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const acceptor = (this.acceptor !== null ? builder.createString(this.acceptor!) : 0);
return CommandRaceVote.createCommandRaceVote(builder,
acceptor
);
}
}
@@ -0,0 +1,134 @@
// 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 CommandScienceCreate implements flatbuffers.IUnpackableObject<CommandScienceCreateT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandScienceCreate {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandScienceCreate(bb:flatbuffers.ByteBuffer, obj?:CommandScienceCreate):CommandScienceCreate {
return (obj || new CommandScienceCreate()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandScienceCreate(bb:flatbuffers.ByteBuffer, obj?:CommandScienceCreate):CommandScienceCreate {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandScienceCreate()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
name():string|null
name(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
name(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;
}
drive():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
weapons():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
shields():number {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
cargo():number {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
static startCommandScienceCreate(builder:flatbuffers.Builder) {
builder.startObject(5);
}
static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, nameOffset, 0);
}
static addDrive(builder:flatbuffers.Builder, drive:number) {
builder.addFieldFloat64(1, drive, 0.0);
}
static addWeapons(builder:flatbuffers.Builder, weapons:number) {
builder.addFieldFloat64(2, weapons, 0.0);
}
static addShields(builder:flatbuffers.Builder, shields:number) {
builder.addFieldFloat64(3, shields, 0.0);
}
static addCargo(builder:flatbuffers.Builder, cargo:number) {
builder.addFieldFloat64(4, cargo, 0.0);
}
static endCommandScienceCreate(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandScienceCreate(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset, drive:number, weapons:number, shields:number, cargo:number):flatbuffers.Offset {
CommandScienceCreate.startCommandScienceCreate(builder);
CommandScienceCreate.addName(builder, nameOffset);
CommandScienceCreate.addDrive(builder, drive);
CommandScienceCreate.addWeapons(builder, weapons);
CommandScienceCreate.addShields(builder, shields);
CommandScienceCreate.addCargo(builder, cargo);
return CommandScienceCreate.endCommandScienceCreate(builder);
}
unpack(): CommandScienceCreateT {
return new CommandScienceCreateT(
this.name(),
this.drive(),
this.weapons(),
this.shields(),
this.cargo()
);
}
unpackTo(_o: CommandScienceCreateT): void {
_o.name = this.name();
_o.drive = this.drive();
_o.weapons = this.weapons();
_o.shields = this.shields();
_o.cargo = this.cargo();
}
}
export class CommandScienceCreateT implements flatbuffers.IGeneratedObject {
constructor(
public name: string|Uint8Array|null = null,
public drive: number = 0.0,
public weapons: number = 0.0,
public shields: number = 0.0,
public cargo: number = 0.0
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const name = (this.name !== null ? builder.createString(this.name!) : 0);
return CommandScienceCreate.createCommandScienceCreate(builder,
name,
this.drive,
this.weapons,
this.shields,
this.cargo
);
}
}
@@ -0,0 +1,78 @@
// 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 CommandScienceRemove implements flatbuffers.IUnpackableObject<CommandScienceRemoveT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandScienceRemove {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandScienceRemove(bb:flatbuffers.ByteBuffer, obj?:CommandScienceRemove):CommandScienceRemove {
return (obj || new CommandScienceRemove()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandScienceRemove(bb:flatbuffers.ByteBuffer, obj?:CommandScienceRemove):CommandScienceRemove {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandScienceRemove()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
name():string|null
name(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
name(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;
}
static startCommandScienceRemove(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, nameOffset, 0);
}
static endCommandScienceRemove(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandScienceRemove(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandScienceRemove.startCommandScienceRemove(builder);
CommandScienceRemove.addName(builder, nameOffset);
return CommandScienceRemove.endCommandScienceRemove(builder);
}
unpack(): CommandScienceRemoveT {
return new CommandScienceRemoveT(
this.name()
);
}
unpackTo(_o: CommandScienceRemoveT): void {
_o.name = this.name();
}
}
export class CommandScienceRemoveT implements flatbuffers.IGeneratedObject {
constructor(
public name: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const name = (this.name !== null ? builder.createString(this.name!) : 0);
return CommandScienceRemove.createCommandScienceRemove(builder,
name
);
}
}
@@ -0,0 +1,148 @@
// 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 CommandShipClassCreate implements flatbuffers.IUnpackableObject<CommandShipClassCreateT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipClassCreate {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipClassCreate(bb:flatbuffers.ByteBuffer, obj?:CommandShipClassCreate):CommandShipClassCreate {
return (obj || new CommandShipClassCreate()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipClassCreate(bb:flatbuffers.ByteBuffer, obj?:CommandShipClassCreate):CommandShipClassCreate {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipClassCreate()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
name():string|null
name(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
name(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;
}
drive():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
armament():bigint {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
weapons():number {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
shields():number {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
cargo():number {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
static startCommandShipClassCreate(builder:flatbuffers.Builder) {
builder.startObject(6);
}
static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, nameOffset, 0);
}
static addDrive(builder:flatbuffers.Builder, drive:number) {
builder.addFieldFloat64(1, drive, 0.0);
}
static addArmament(builder:flatbuffers.Builder, armament:bigint) {
builder.addFieldInt64(2, armament, BigInt('0'));
}
static addWeapons(builder:flatbuffers.Builder, weapons:number) {
builder.addFieldFloat64(3, weapons, 0.0);
}
static addShields(builder:flatbuffers.Builder, shields:number) {
builder.addFieldFloat64(4, shields, 0.0);
}
static addCargo(builder:flatbuffers.Builder, cargo:number) {
builder.addFieldFloat64(5, cargo, 0.0);
}
static endCommandShipClassCreate(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipClassCreate(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset, drive:number, armament:bigint, weapons:number, shields:number, cargo:number):flatbuffers.Offset {
CommandShipClassCreate.startCommandShipClassCreate(builder);
CommandShipClassCreate.addName(builder, nameOffset);
CommandShipClassCreate.addDrive(builder, drive);
CommandShipClassCreate.addArmament(builder, armament);
CommandShipClassCreate.addWeapons(builder, weapons);
CommandShipClassCreate.addShields(builder, shields);
CommandShipClassCreate.addCargo(builder, cargo);
return CommandShipClassCreate.endCommandShipClassCreate(builder);
}
unpack(): CommandShipClassCreateT {
return new CommandShipClassCreateT(
this.name(),
this.drive(),
this.armament(),
this.weapons(),
this.shields(),
this.cargo()
);
}
unpackTo(_o: CommandShipClassCreateT): void {
_o.name = this.name();
_o.drive = this.drive();
_o.armament = this.armament();
_o.weapons = this.weapons();
_o.shields = this.shields();
_o.cargo = this.cargo();
}
}
export class CommandShipClassCreateT implements flatbuffers.IGeneratedObject {
constructor(
public name: string|Uint8Array|null = null,
public drive: number = 0.0,
public armament: bigint = BigInt('0'),
public weapons: number = 0.0,
public shields: number = 0.0,
public cargo: number = 0.0
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const name = (this.name !== null ? builder.createString(this.name!) : 0);
return CommandShipClassCreate.createCommandShipClassCreate(builder,
name,
this.drive,
this.armament,
this.weapons,
this.shields,
this.cargo
);
}
}
@@ -0,0 +1,95 @@
// 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 CommandShipClassMerge implements flatbuffers.IUnpackableObject<CommandShipClassMergeT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipClassMerge {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipClassMerge(bb:flatbuffers.ByteBuffer, obj?:CommandShipClassMerge):CommandShipClassMerge {
return (obj || new CommandShipClassMerge()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipClassMerge(bb:flatbuffers.ByteBuffer, obj?:CommandShipClassMerge):CommandShipClassMerge {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipClassMerge()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
name():string|null
name(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
name(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;
}
target():string|null
target(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
target(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startCommandShipClassMerge(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, nameOffset, 0);
}
static addTarget(builder:flatbuffers.Builder, targetOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, targetOffset, 0);
}
static endCommandShipClassMerge(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipClassMerge(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset, targetOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandShipClassMerge.startCommandShipClassMerge(builder);
CommandShipClassMerge.addName(builder, nameOffset);
CommandShipClassMerge.addTarget(builder, targetOffset);
return CommandShipClassMerge.endCommandShipClassMerge(builder);
}
unpack(): CommandShipClassMergeT {
return new CommandShipClassMergeT(
this.name(),
this.target()
);
}
unpackTo(_o: CommandShipClassMergeT): void {
_o.name = this.name();
_o.target = this.target();
}
}
export class CommandShipClassMergeT implements flatbuffers.IGeneratedObject {
constructor(
public name: string|Uint8Array|null = null,
public target: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const name = (this.name !== null ? builder.createString(this.name!) : 0);
const target = (this.target !== null ? builder.createString(this.target!) : 0);
return CommandShipClassMerge.createCommandShipClassMerge(builder,
name,
target
);
}
}
@@ -0,0 +1,78 @@
// 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 CommandShipClassRemove implements flatbuffers.IUnpackableObject<CommandShipClassRemoveT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipClassRemove {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipClassRemove(bb:flatbuffers.ByteBuffer, obj?:CommandShipClassRemove):CommandShipClassRemove {
return (obj || new CommandShipClassRemove()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipClassRemove(bb:flatbuffers.ByteBuffer, obj?:CommandShipClassRemove):CommandShipClassRemove {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipClassRemove()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
name():string|null
name(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
name(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;
}
static startCommandShipClassRemove(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, nameOffset, 0);
}
static endCommandShipClassRemove(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipClassRemove(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandShipClassRemove.startCommandShipClassRemove(builder);
CommandShipClassRemove.addName(builder, nameOffset);
return CommandShipClassRemove.endCommandShipClassRemove(builder);
}
unpack(): CommandShipClassRemoveT {
return new CommandShipClassRemoveT(
this.name()
);
}
unpackTo(_o: CommandShipClassRemoveT): void {
_o.name = this.name();
}
}
export class CommandShipClassRemoveT implements flatbuffers.IGeneratedObject {
constructor(
public name: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const name = (this.name !== null ? builder.createString(this.name!) : 0);
return CommandShipClassRemove.createCommandShipClassRemove(builder,
name
);
}
}
@@ -0,0 +1,109 @@
// 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 CommandShipGroupBreak implements flatbuffers.IUnpackableObject<CommandShipGroupBreakT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipGroupBreak {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipGroupBreak(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupBreak):CommandShipGroupBreak {
return (obj || new CommandShipGroupBreak()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipGroupBreak(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupBreak):CommandShipGroupBreak {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipGroupBreak()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(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;
}
newId():string|null
newId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
newId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
quantity():bigint {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
static startCommandShipGroupBreak(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static addNewId(builder:flatbuffers.Builder, newIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, newIdOffset, 0);
}
static addQuantity(builder:flatbuffers.Builder, quantity:bigint) {
builder.addFieldInt64(2, quantity, BigInt('0'));
}
static endCommandShipGroupBreak(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipGroupBreak(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, newIdOffset:flatbuffers.Offset, quantity:bigint):flatbuffers.Offset {
CommandShipGroupBreak.startCommandShipGroupBreak(builder);
CommandShipGroupBreak.addId(builder, idOffset);
CommandShipGroupBreak.addNewId(builder, newIdOffset);
CommandShipGroupBreak.addQuantity(builder, quantity);
return CommandShipGroupBreak.endCommandShipGroupBreak(builder);
}
unpack(): CommandShipGroupBreakT {
return new CommandShipGroupBreakT(
this.id(),
this.newId(),
this.quantity()
);
}
unpackTo(_o: CommandShipGroupBreakT): void {
_o.id = this.id();
_o.newId = this.newId();
_o.quantity = this.quantity();
}
}
export class CommandShipGroupBreakT implements flatbuffers.IGeneratedObject {
constructor(
public id: string|Uint8Array|null = null,
public newId: string|Uint8Array|null = null,
public quantity: bigint = BigInt('0')
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const id = (this.id !== null ? builder.createString(this.id!) : 0);
const newId = (this.newId !== null ? builder.createString(this.newId!) : 0);
return CommandShipGroupBreak.createCommandShipGroupBreak(builder,
id,
newId,
this.quantity
);
}
}
@@ -0,0 +1,78 @@
// 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 CommandShipGroupDismantle implements flatbuffers.IUnpackableObject<CommandShipGroupDismantleT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipGroupDismantle {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipGroupDismantle(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupDismantle):CommandShipGroupDismantle {
return (obj || new CommandShipGroupDismantle()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipGroupDismantle(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupDismantle):CommandShipGroupDismantle {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipGroupDismantle()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(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;
}
static startCommandShipGroupDismantle(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static endCommandShipGroupDismantle(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipGroupDismantle(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandShipGroupDismantle.startCommandShipGroupDismantle(builder);
CommandShipGroupDismantle.addId(builder, idOffset);
return CommandShipGroupDismantle.endCommandShipGroupDismantle(builder);
}
unpack(): CommandShipGroupDismantleT {
return new CommandShipGroupDismantleT(
this.id()
);
}
unpackTo(_o: CommandShipGroupDismantleT): void {
_o.id = this.id();
}
}
export class CommandShipGroupDismantleT implements flatbuffers.IGeneratedObject {
constructor(
public id: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const id = (this.id !== null ? builder.createString(this.id!) : 0);
return CommandShipGroupDismantle.createCommandShipGroupDismantle(builder,
id
);
}
}
@@ -0,0 +1,95 @@
// 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 CommandShipGroupJoinFleet implements flatbuffers.IUnpackableObject<CommandShipGroupJoinFleetT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipGroupJoinFleet {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipGroupJoinFleet(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupJoinFleet):CommandShipGroupJoinFleet {
return (obj || new CommandShipGroupJoinFleet()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipGroupJoinFleet(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupJoinFleet):CommandShipGroupJoinFleet {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipGroupJoinFleet()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(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;
}
name():string|null
name(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
name(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startCommandShipGroupJoinFleet(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static addName(builder:flatbuffers.Builder, nameOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, nameOffset, 0);
}
static endCommandShipGroupJoinFleet(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipGroupJoinFleet(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, nameOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandShipGroupJoinFleet.startCommandShipGroupJoinFleet(builder);
CommandShipGroupJoinFleet.addId(builder, idOffset);
CommandShipGroupJoinFleet.addName(builder, nameOffset);
return CommandShipGroupJoinFleet.endCommandShipGroupJoinFleet(builder);
}
unpack(): CommandShipGroupJoinFleetT {
return new CommandShipGroupJoinFleetT(
this.id(),
this.name()
);
}
unpackTo(_o: CommandShipGroupJoinFleetT): void {
_o.id = this.id();
_o.name = this.name();
}
}
export class CommandShipGroupJoinFleetT implements flatbuffers.IGeneratedObject {
constructor(
public id: string|Uint8Array|null = null,
public name: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const id = (this.id !== null ? builder.createString(this.id!) : 0);
const name = (this.name !== null ? builder.createString(this.name!) : 0);
return CommandShipGroupJoinFleet.createCommandShipGroupJoinFleet(builder,
id,
name
);
}
}
@@ -0,0 +1,107 @@
// 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 { ShipGroupCargo } from './ship-group-cargo.js';
export class CommandShipGroupLoad implements flatbuffers.IUnpackableObject<CommandShipGroupLoadT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipGroupLoad {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipGroupLoad(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupLoad):CommandShipGroupLoad {
return (obj || new CommandShipGroupLoad()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipGroupLoad(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupLoad):CommandShipGroupLoad {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipGroupLoad()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(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;
}
cargo():ShipGroupCargo {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt8(this.bb_pos + offset) : ShipGroupCargo.UNKNOWN;
}
quantity():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
static startCommandShipGroupLoad(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static addCargo(builder:flatbuffers.Builder, cargo:ShipGroupCargo) {
builder.addFieldInt8(1, cargo, ShipGroupCargo.UNKNOWN);
}
static addQuantity(builder:flatbuffers.Builder, quantity:number) {
builder.addFieldFloat64(2, quantity, 0.0);
}
static endCommandShipGroupLoad(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipGroupLoad(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, cargo:ShipGroupCargo, quantity:number):flatbuffers.Offset {
CommandShipGroupLoad.startCommandShipGroupLoad(builder);
CommandShipGroupLoad.addId(builder, idOffset);
CommandShipGroupLoad.addCargo(builder, cargo);
CommandShipGroupLoad.addQuantity(builder, quantity);
return CommandShipGroupLoad.endCommandShipGroupLoad(builder);
}
unpack(): CommandShipGroupLoadT {
return new CommandShipGroupLoadT(
this.id(),
this.cargo(),
this.quantity()
);
}
unpackTo(_o: CommandShipGroupLoadT): void {
_o.id = this.id();
_o.cargo = this.cargo();
_o.quantity = this.quantity();
}
}
export class CommandShipGroupLoadT implements flatbuffers.IGeneratedObject {
constructor(
public id: string|Uint8Array|null = null,
public cargo: ShipGroupCargo = ShipGroupCargo.UNKNOWN,
public quantity: number = 0.0
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const id = (this.id !== null ? builder.createString(this.id!) : 0);
return CommandShipGroupLoad.createCommandShipGroupLoad(builder,
id,
this.cargo,
this.quantity
);
}
}
@@ -0,0 +1,56 @@
// 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 CommandShipGroupMerge implements flatbuffers.IUnpackableObject<CommandShipGroupMergeT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipGroupMerge {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipGroupMerge(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupMerge):CommandShipGroupMerge {
return (obj || new CommandShipGroupMerge()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipGroupMerge(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupMerge):CommandShipGroupMerge {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipGroupMerge()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static startCommandShipGroupMerge(builder:flatbuffers.Builder) {
builder.startObject(0);
}
static endCommandShipGroupMerge(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipGroupMerge(builder:flatbuffers.Builder):flatbuffers.Offset {
CommandShipGroupMerge.startCommandShipGroupMerge(builder);
return CommandShipGroupMerge.endCommandShipGroupMerge(builder);
}
unpack(): CommandShipGroupMergeT {
return new CommandShipGroupMergeT();
}
unpackTo(_o: CommandShipGroupMergeT): void {}
}
export class CommandShipGroupMergeT implements flatbuffers.IGeneratedObject {
constructor(){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return CommandShipGroupMerge.createCommandShipGroupMerge(builder);
}
}
@@ -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 CommandShipGroupSend implements flatbuffers.IUnpackableObject<CommandShipGroupSendT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipGroupSend {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipGroupSend(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupSend):CommandShipGroupSend {
return (obj || new CommandShipGroupSend()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipGroupSend(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupSend):CommandShipGroupSend {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipGroupSend()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(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;
}
destination():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
static startCommandShipGroupSend(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static addDestination(builder:flatbuffers.Builder, destination:bigint) {
builder.addFieldInt64(1, destination, BigInt('0'));
}
static endCommandShipGroupSend(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipGroupSend(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, destination:bigint):flatbuffers.Offset {
CommandShipGroupSend.startCommandShipGroupSend(builder);
CommandShipGroupSend.addId(builder, idOffset);
CommandShipGroupSend.addDestination(builder, destination);
return CommandShipGroupSend.endCommandShipGroupSend(builder);
}
unpack(): CommandShipGroupSendT {
return new CommandShipGroupSendT(
this.id(),
this.destination()
);
}
unpackTo(_o: CommandShipGroupSendT): void {
_o.id = this.id();
_o.destination = this.destination();
}
}
export class CommandShipGroupSendT implements flatbuffers.IGeneratedObject {
constructor(
public id: string|Uint8Array|null = null,
public destination: bigint = BigInt('0')
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const id = (this.id !== null ? builder.createString(this.id!) : 0);
return CommandShipGroupSend.createCommandShipGroupSend(builder,
id,
this.destination
);
}
}
@@ -0,0 +1,95 @@
// 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 CommandShipGroupTransfer implements flatbuffers.IUnpackableObject<CommandShipGroupTransferT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipGroupTransfer {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipGroupTransfer(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupTransfer):CommandShipGroupTransfer {
return (obj || new CommandShipGroupTransfer()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipGroupTransfer(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupTransfer):CommandShipGroupTransfer {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipGroupTransfer()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(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;
}
acceptor():string|null
acceptor(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
acceptor(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startCommandShipGroupTransfer(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static addAcceptor(builder:flatbuffers.Builder, acceptorOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, acceptorOffset, 0);
}
static endCommandShipGroupTransfer(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipGroupTransfer(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, acceptorOffset:flatbuffers.Offset):flatbuffers.Offset {
CommandShipGroupTransfer.startCommandShipGroupTransfer(builder);
CommandShipGroupTransfer.addId(builder, idOffset);
CommandShipGroupTransfer.addAcceptor(builder, acceptorOffset);
return CommandShipGroupTransfer.endCommandShipGroupTransfer(builder);
}
unpack(): CommandShipGroupTransferT {
return new CommandShipGroupTransferT(
this.id(),
this.acceptor()
);
}
unpackTo(_o: CommandShipGroupTransferT): void {
_o.id = this.id();
_o.acceptor = this.acceptor();
}
}
export class CommandShipGroupTransferT implements flatbuffers.IGeneratedObject {
constructor(
public id: string|Uint8Array|null = null,
public acceptor: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const id = (this.id !== null ? builder.createString(this.id!) : 0);
const acceptor = (this.acceptor !== null ? builder.createString(this.acceptor!) : 0);
return CommandShipGroupTransfer.createCommandShipGroupTransfer(builder,
id,
acceptor
);
}
}
@@ -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 CommandShipGroupUnload implements flatbuffers.IUnpackableObject<CommandShipGroupUnloadT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipGroupUnload {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipGroupUnload(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupUnload):CommandShipGroupUnload {
return (obj || new CommandShipGroupUnload()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipGroupUnload(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupUnload):CommandShipGroupUnload {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipGroupUnload()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(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;
}
quantity():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
static startCommandShipGroupUnload(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static addQuantity(builder:flatbuffers.Builder, quantity:number) {
builder.addFieldFloat64(1, quantity, 0.0);
}
static endCommandShipGroupUnload(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipGroupUnload(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, quantity:number):flatbuffers.Offset {
CommandShipGroupUnload.startCommandShipGroupUnload(builder);
CommandShipGroupUnload.addId(builder, idOffset);
CommandShipGroupUnload.addQuantity(builder, quantity);
return CommandShipGroupUnload.endCommandShipGroupUnload(builder);
}
unpack(): CommandShipGroupUnloadT {
return new CommandShipGroupUnloadT(
this.id(),
this.quantity()
);
}
unpackTo(_o: CommandShipGroupUnloadT): void {
_o.id = this.id();
_o.quantity = this.quantity();
}
}
export class CommandShipGroupUnloadT implements flatbuffers.IGeneratedObject {
constructor(
public id: string|Uint8Array|null = null,
public quantity: number = 0.0
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const id = (this.id !== null ? builder.createString(this.id!) : 0);
return CommandShipGroupUnload.createCommandShipGroupUnload(builder,
id,
this.quantity
);
}
}
@@ -0,0 +1,107 @@
// 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 { ShipGroupUpgradeTech } from './ship-group-upgrade-tech.js';
export class CommandShipGroupUpgrade implements flatbuffers.IUnpackableObject<CommandShipGroupUpgradeT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):CommandShipGroupUpgrade {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsCommandShipGroupUpgrade(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupUpgrade):CommandShipGroupUpgrade {
return (obj || new CommandShipGroupUpgrade()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsCommandShipGroupUpgrade(bb:flatbuffers.ByteBuffer, obj?:CommandShipGroupUpgrade):CommandShipGroupUpgrade {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new CommandShipGroupUpgrade()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id():string|null
id(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
id(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;
}
tech():ShipGroupUpgradeTech {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt8(this.bb_pos + offset) : ShipGroupUpgradeTech.UNKNOWN;
}
level():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readFloat64(this.bb_pos + offset) : 0.0;
}
static startCommandShipGroupUpgrade(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, idOffset, 0);
}
static addTech(builder:flatbuffers.Builder, tech:ShipGroupUpgradeTech) {
builder.addFieldInt8(1, tech, ShipGroupUpgradeTech.UNKNOWN);
}
static addLevel(builder:flatbuffers.Builder, level:number) {
builder.addFieldFloat64(2, level, 0.0);
}
static endCommandShipGroupUpgrade(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createCommandShipGroupUpgrade(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, tech:ShipGroupUpgradeTech, level:number):flatbuffers.Offset {
CommandShipGroupUpgrade.startCommandShipGroupUpgrade(builder);
CommandShipGroupUpgrade.addId(builder, idOffset);
CommandShipGroupUpgrade.addTech(builder, tech);
CommandShipGroupUpgrade.addLevel(builder, level);
return CommandShipGroupUpgrade.endCommandShipGroupUpgrade(builder);
}
unpack(): CommandShipGroupUpgradeT {
return new CommandShipGroupUpgradeT(
this.id(),
this.tech(),
this.level()
);
}
unpackTo(_o: CommandShipGroupUpgradeT): void {
_o.id = this.id();
_o.tech = this.tech();
_o.level = this.level();
}
}
export class CommandShipGroupUpgradeT implements flatbuffers.IGeneratedObject {
constructor(
public id: string|Uint8Array|null = null,
public tech: ShipGroupUpgradeTech = ShipGroupUpgradeTech.UNKNOWN,
public level: number = 0.0
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const id = (this.id !== null ? builder.createString(this.id!) : 0);
return CommandShipGroupUpgrade.createCommandShipGroupUpgrade(builder,
id,
this.tech,
this.level
);
}
}
@@ -0,0 +1,15 @@
// 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 enum PlanetProduction {
UNKNOWN = 0,
MAT = 1,
CAP = 2,
DRIVE = 3,
WEAPONS = 4,
SHIELDS = 5,
CARGO = 6,
SCIENCE = 7,
SHIP = 8
}
@@ -0,0 +1,11 @@
// 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 enum PlanetRouteLoadType {
UNKNOWN = 0,
MAT = 1,
CAP = 2,
COL = 3,
EMP = 4
}
@@ -0,0 +1,9 @@
// 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 enum Relation {
UNKNOWN = 0,
WAR = 1,
PEACE = 2
}
@@ -0,0 +1,10 @@
// 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 enum ShipGroupCargo {
UNKNOWN = 0,
COL = 1,
MAT = 2,
CAP = 3
}
@@ -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 enum ShipGroupUpgradeTech {
UNKNOWN = 0,
ALL = 1,
DRIVE = 2,
WEAPONS = 3,
SHIELDS = 4,
CARGO = 5
}
@@ -0,0 +1,56 @@
// 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 UserGamesCommandResponse implements flatbuffers.IUnpackableObject<UserGamesCommandResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):UserGamesCommandResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsUserGamesCommandResponse(bb:flatbuffers.ByteBuffer, obj?:UserGamesCommandResponse):UserGamesCommandResponse {
return (obj || new UserGamesCommandResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsUserGamesCommandResponse(bb:flatbuffers.ByteBuffer, obj?:UserGamesCommandResponse):UserGamesCommandResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new UserGamesCommandResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static startUserGamesCommandResponse(builder:flatbuffers.Builder) {
builder.startObject(0);
}
static endUserGamesCommandResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createUserGamesCommandResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
UserGamesCommandResponse.startUserGamesCommandResponse(builder);
return UserGamesCommandResponse.endUserGamesCommandResponse(builder);
}
unpack(): UserGamesCommandResponseT {
return new UserGamesCommandResponseT();
}
unpackTo(_o: UserGamesCommandResponseT): void {}
}
export class UserGamesCommandResponseT implements flatbuffers.IGeneratedObject {
constructor(){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return UserGamesCommandResponse.createUserGamesCommandResponse(builder);
}
}
@@ -0,0 +1,110 @@
// 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 '../common/uuid.js';
import { CommandItem, CommandItemT } from './command-item.js';
export class UserGamesCommand implements flatbuffers.IUnpackableObject<UserGamesCommandT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):UserGamesCommand {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsUserGamesCommand(bb:flatbuffers.ByteBuffer, obj?:UserGamesCommand):UserGamesCommand {
return (obj || new UserGamesCommand()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsUserGamesCommand(bb:flatbuffers.ByteBuffer, obj?:UserGamesCommand):UserGamesCommand {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new UserGamesCommand()).__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;
}
commands(index: number, obj?:CommandItem):CommandItem|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new CommandItem()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
commandsLength():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startUserGamesCommand(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addCommands(builder:flatbuffers.Builder, commandsOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, commandsOffset, 0);
}
static createCommandsVector(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 startCommandsVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endUserGamesCommand(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
return offset;
}
static createUserGamesCommand(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, commandsOffset:flatbuffers.Offset):flatbuffers.Offset {
UserGamesCommand.startUserGamesCommand(builder);
UserGamesCommand.addGameId(builder, gameIdOffset);
UserGamesCommand.addCommands(builder, commandsOffset);
return UserGamesCommand.endUserGamesCommand(builder);
}
unpack(): UserGamesCommandT {
return new UserGamesCommandT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
this.bb!.createObjList<CommandItem, CommandItemT>(this.commands.bind(this), this.commandsLength())
);
}
unpackTo(_o: UserGamesCommandT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.commands = this.bb!.createObjList<CommandItem, CommandItemT>(this.commands.bind(this), this.commandsLength());
}
}
export class UserGamesCommandT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public commands: (CommandItemT)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const commands = UserGamesCommand.createCommandsVector(builder, builder.createObjectOffsetList(this.commands));
return UserGamesCommand.createUserGamesCommand(builder,
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
commands
);
}
}
@@ -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 { UserGamesOrder, UserGamesOrderT } from './user-games-order.js';
export class UserGamesOrderGetResponse implements flatbuffers.IUnpackableObject<UserGamesOrderGetResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):UserGamesOrderGetResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsUserGamesOrderGetResponse(bb:flatbuffers.ByteBuffer, obj?:UserGamesOrderGetResponse):UserGamesOrderGetResponse {
return (obj || new UserGamesOrderGetResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsUserGamesOrderGetResponse(bb:flatbuffers.ByteBuffer, obj?:UserGamesOrderGetResponse):UserGamesOrderGetResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new UserGamesOrderGetResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
found():boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
order(obj?:UserGamesOrder):UserGamesOrder|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new UserGamesOrder()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startUserGamesOrderGetResponse(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addFound(builder:flatbuffers.Builder, found:boolean) {
builder.addFieldInt8(0, +found, +false);
}
static addOrder(builder:flatbuffers.Builder, orderOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, orderOffset, 0);
}
static endUserGamesOrderGetResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
unpack(): UserGamesOrderGetResponseT {
return new UserGamesOrderGetResponseT(
this.found(),
(this.order() !== null ? this.order()!.unpack() : null)
);
}
unpackTo(_o: UserGamesOrderGetResponseT): void {
_o.found = this.found();
_o.order = (this.order() !== null ? this.order()!.unpack() : null);
}
}
export class UserGamesOrderGetResponseT implements flatbuffers.IGeneratedObject {
constructor(
public found: boolean = false,
public order: UserGamesOrderT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const order = (this.order !== null ? this.order!.pack(builder) : 0);
UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
UserGamesOrderGetResponse.addFound(builder, this.found);
UserGamesOrderGetResponse.addOrder(builder, order);
return UserGamesOrderGetResponse.endUserGamesOrderGetResponse(builder);
}
}
@@ -0,0 +1,90 @@
// 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 '../common/uuid.js';
export class UserGamesOrderGet implements flatbuffers.IUnpackableObject<UserGamesOrderGetT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):UserGamesOrderGet {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsUserGamesOrderGet(bb:flatbuffers.ByteBuffer, obj?:UserGamesOrderGet):UserGamesOrderGet {
return (obj || new UserGamesOrderGet()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsUserGamesOrderGet(bb:flatbuffers.ByteBuffer, obj?:UserGamesOrderGet):UserGamesOrderGet {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new UserGamesOrderGet()).__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():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
static startUserGamesOrderGet(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addTurn(builder:flatbuffers.Builder, turn:bigint) {
builder.addFieldInt64(1, turn, BigInt('0'));
}
static endUserGamesOrderGet(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
return offset;
}
static createUserGamesOrderGet(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, turn:bigint):flatbuffers.Offset {
UserGamesOrderGet.startUserGamesOrderGet(builder);
UserGamesOrderGet.addGameId(builder, gameIdOffset);
UserGamesOrderGet.addTurn(builder, turn);
return UserGamesOrderGet.endUserGamesOrderGet(builder);
}
unpack(): UserGamesOrderGetT {
return new UserGamesOrderGetT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
this.turn()
);
}
unpackTo(_o: UserGamesOrderGetT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.turn = this.turn();
}
}
export class UserGamesOrderGetT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public turn: bigint = BigInt('0')
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return UserGamesOrderGet.createUserGamesOrderGet(builder,
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
this.turn
);
}
}
@@ -0,0 +1,123 @@
// 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 '../common/uuid.js';
import { CommandItem, CommandItemT } from './command-item.js';
export class UserGamesOrderResponse implements flatbuffers.IUnpackableObject<UserGamesOrderResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):UserGamesOrderResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsUserGamesOrderResponse(bb:flatbuffers.ByteBuffer, obj?:UserGamesOrderResponse):UserGamesOrderResponse {
return (obj || new UserGamesOrderResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsUserGamesOrderResponse(bb:flatbuffers.ByteBuffer, obj?:UserGamesOrderResponse):UserGamesOrderResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new UserGamesOrderResponse()).__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;
}
updatedAt():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
commands(index: number, obj?:CommandItem):CommandItem|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? (obj || new CommandItem()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
commandsLength():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startUserGamesOrderResponse(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addUpdatedAt(builder:flatbuffers.Builder, updatedAt:bigint) {
builder.addFieldInt64(1, updatedAt, BigInt('0'));
}
static addCommands(builder:flatbuffers.Builder, commandsOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, commandsOffset, 0);
}
static createCommandsVector(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 startCommandsVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endUserGamesOrderResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createUserGamesOrderResponse(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, updatedAt:bigint, commandsOffset:flatbuffers.Offset):flatbuffers.Offset {
UserGamesOrderResponse.startUserGamesOrderResponse(builder);
UserGamesOrderResponse.addGameId(builder, gameIdOffset);
UserGamesOrderResponse.addUpdatedAt(builder, updatedAt);
UserGamesOrderResponse.addCommands(builder, commandsOffset);
return UserGamesOrderResponse.endUserGamesOrderResponse(builder);
}
unpack(): UserGamesOrderResponseT {
return new UserGamesOrderResponseT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
this.updatedAt(),
this.bb!.createObjList<CommandItem, CommandItemT>(this.commands.bind(this), this.commandsLength())
);
}
unpackTo(_o: UserGamesOrderResponseT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.updatedAt = this.updatedAt();
_o.commands = this.bb!.createObjList<CommandItem, CommandItemT>(this.commands.bind(this), this.commandsLength());
}
}
export class UserGamesOrderResponseT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public updatedAt: bigint = BigInt('0'),
public commands: (CommandItemT)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const commands = UserGamesOrderResponse.createCommandsVector(builder, builder.createObjectOffsetList(this.commands));
return UserGamesOrderResponse.createUserGamesOrderResponse(builder,
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
this.updatedAt,
commands
);
}
}
@@ -0,0 +1,124 @@
// 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 '../common/uuid.js';
import { CommandItem, CommandItemT } from './command-item.js';
export class UserGamesOrder implements flatbuffers.IUnpackableObject<UserGamesOrderT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):UserGamesOrder {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsUserGamesOrder(bb:flatbuffers.ByteBuffer, obj?:UserGamesOrder):UserGamesOrder {
return (obj || new UserGamesOrder()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsUserGamesOrder(bb:flatbuffers.ByteBuffer, obj?:UserGamesOrder):UserGamesOrder {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new UserGamesOrder()).__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;
}
updatedAt():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
commands(index: number, obj?:CommandItem):CommandItem|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? (obj || new CommandItem()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
commandsLength():number {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startUserGamesOrder(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addUpdatedAt(builder:flatbuffers.Builder, updatedAt:bigint) {
builder.addFieldInt64(1, updatedAt, BigInt('0'));
}
static addCommands(builder:flatbuffers.Builder, commandsOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, commandsOffset, 0);
}
static createCommandsVector(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 startCommandsVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endUserGamesOrder(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
return offset;
}
static createUserGamesOrder(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, updatedAt:bigint, commandsOffset:flatbuffers.Offset):flatbuffers.Offset {
UserGamesOrder.startUserGamesOrder(builder);
UserGamesOrder.addGameId(builder, gameIdOffset);
UserGamesOrder.addUpdatedAt(builder, updatedAt);
UserGamesOrder.addCommands(builder, commandsOffset);
return UserGamesOrder.endUserGamesOrder(builder);
}
unpack(): UserGamesOrderT {
return new UserGamesOrderT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
this.updatedAt(),
this.bb!.createObjList<CommandItem, CommandItemT>(this.commands.bind(this), this.commandsLength())
);
}
unpackTo(_o: UserGamesOrderT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.updatedAt = this.updatedAt();
_o.commands = this.bb!.createObjList<CommandItem, CommandItemT>(this.commands.bind(this), this.commandsLength());
}
}
export class UserGamesOrderT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public updatedAt: bigint = BigInt('0'),
public commands: (CommandItemT)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const commands = UserGamesOrder.createCommandsVector(builder, builder.createObjectOffsetList(this.commands));
return UserGamesOrder.createUserGamesOrder(builder,
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
this.updatedAt,
commands
);
}
}
@@ -57,10 +57,18 @@ fresh.
SelectionStore,
SELECTION_CONTEXT_KEY,
} from "$lib/selection.svelte";
import {
createRenderedReportSource,
RENDERED_REPORT_CONTEXT_KEY,
} from "$lib/rendered-report.svelte";
import {
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../../../sync/order-draft.svelte";
import {
GALAXY_CLIENT_CONTEXT_KEY,
GalaxyClientHolder,
} from "$lib/galaxy-client-context.svelte";
import { session } from "$lib/session-store.svelte";
import { loadStore } from "../../../platform/store/index";
import { loadCore } from "../../../platform/core/index";
@@ -89,17 +97,23 @@ fresh.
setContext(ORDER_DRAFT_CONTEXT_KEY, orderDraft);
const selection = new SelectionStore();
setContext(SELECTION_CONTEXT_KEY, selection);
const renderedReport = createRenderedReportSource(gameState, orderDraft);
setContext(RENDERED_REPORT_CONTEXT_KEY, renderedReport);
const galaxyClient = new GalaxyClientHolder();
setContext(GALAXY_CLIENT_CONTEXT_KEY, galaxyClient);
// selectedPlanet resolves the current selection against the live
// report so both the desktop sidebar and the mobile sheet display
// the same snapshot. A selection that points at a planet missing
// from the current report (e.g. visibility lost between turns)
// reads as `null` here, which collapses the inspector and the
// sheet without surfacing a stale row.
// sheet without surfacing a stale row. The rendered report layers
// the local order draft on top so the player sees their pending
// renames immediately.
const selectedPlanet = $derived.by(() => {
const sel = selection.selected;
if (sel === null || sel.kind !== "planet") return null;
const report = gameState.report;
const report = renderedReport.report;
if (report === null) return null;
return report.planets.find((p) => p.number === sel.id) ?? null;
});
@@ -149,6 +163,13 @@ fresh.
gameState.init({ client, cache, gameId }),
orderDraft.init({ cache, gameId }),
]);
galaxyClient.set(client);
if (orderDraft.needsServerHydration) {
await orderDraft.hydrateFromServer({
client,
turn: gameState.currentTurn,
});
}
} catch (err) {
gameState.failBootstrap(describeBootstrapError(err));
}
+150 -5
View File
@@ -17,7 +17,10 @@
// any UI.
import type { Cache } from "../platform/store/index";
import type { OrderCommand } from "./order-types";
import type { GalaxyClient } from "../api/galaxy-client";
import { fetchOrder } from "./order-load";
import type { CommandStatus, OrderCommand } from "./order-types";
import { validateEntityName } from "$lib/util/entity-name";
const NAMESPACE = "order-drafts";
const draftKey = (gameId: string): string => `${gameId}/draft`;
@@ -34,9 +37,21 @@ type Status = "idle" | "ready" | "error";
export class OrderDraftStore {
commands: OrderCommand[] = $state([]);
statuses: Record<string, CommandStatus> = $state({});
updatedAt = $state(0);
status: Status = $state("idle");
error: string | null = $state(null);
/**
* needsServerHydration is `true` when the cache row for this game
* was absent at `init` time. The layout reads it after both
* `gameState.init` and `orderDraft.init` resolve and, if `true`,
* calls `hydrateFromServer` once the current turn is known.
* An explicitly empty cache row sets it to `false` (the user has
* an empty draft, not a missing one).
*/
needsServerHydration = $state(false);
private cache: Cache | null = null;
private gameId = "";
private destroyed = false;
@@ -47,6 +62,12 @@ export class OrderDraftStore {
* idempotent on the same store instance — the layout always
* constructs a fresh store per game, so there is no need to support
* mid-life game switching here.
*
* When the cache row is absent, `needsServerHydration` is set to
* `true`; the layout fans out a `hydrateFromServer` call once the
* current turn is known. An explicitly empty cache row is treated
* as "user has an empty draft" and skipped — local intent always
* wins over server snapshot.
*/
async init(opts: { cache: Cache; gameId: string }): Promise<void> {
this.cache = opts.cache;
@@ -57,7 +78,14 @@ export class OrderDraftStore {
draftKey(opts.gameId),
);
if (this.destroyed) return;
this.commands = Array.isArray(stored) ? [...stored] : [];
if (stored === undefined) {
this.commands = [];
this.needsServerHydration = true;
} else {
this.commands = Array.isArray(stored) ? [...stored] : [];
this.needsServerHydration = false;
}
this.recomputeStatuses();
this.status = "ready";
} catch (err) {
if (this.destroyed) return;
@@ -67,13 +95,44 @@ export class OrderDraftStore {
}
/**
* add appends a command to the end of the draft and persists the
* updated list. Mutations made before `init` resolves are ignored —
* the layout always awaits `init` before exposing the store.
* hydrateFromServer fetches the player's stored order from the
* gateway when the cache row was absent at boot. The result is
* merged into `commands` and persisted so subsequent reloads
* prefer the cached version. Failures are non-fatal — the draft
* stays empty and the user can keep composing.
*/
async hydrateFromServer(opts: {
client: GalaxyClient;
turn: number;
}): Promise<void> {
if (this.status !== "ready" || !this.needsServerHydration) return;
this.needsServerHydration = false;
try {
const fetched = await fetchOrder(opts.client, this.gameId, opts.turn);
if (this.destroyed) return;
this.commands = fetched.commands;
this.updatedAt = fetched.updatedAt;
this.recomputeStatuses();
await this.persist();
} catch (err) {
if (this.destroyed) return;
console.warn(
"order-draft: server hydration failed; staying on empty draft",
err,
);
}
}
/**
* add appends a command to the end of the draft, runs local
* validation for the new entry, and persists the updated list.
* Mutations made before `init` resolves are ignored — the layout
* always awaits `init` before exposing the store.
*/
async add(command: OrderCommand): Promise<void> {
if (this.status !== "ready") return;
this.commands = [...this.commands, command];
this.statuses = { ...this.statuses, [command.id]: validateCommand(command) };
await this.persist();
}
@@ -86,6 +145,9 @@ export class OrderDraftStore {
const next = this.commands.filter((cmd) => cmd.id !== id);
if (next.length === this.commands.length) return;
this.commands = next;
const nextStatuses = { ...this.statuses };
delete nextStatuses[id];
this.statuses = nextStatuses;
await this.persist();
}
@@ -109,11 +171,83 @@ export class OrderDraftStore {
await this.persist();
}
/**
* markSubmitting flips the status of every entry in `ids` to
* `submitting` so the order tab can disable per-row controls and
* show a spinner. The state machine runs `valid → submitting →
* applied | rejected` (see ui/docs/order-composer.md).
*/
markSubmitting(ids: string[]): void {
const next = { ...this.statuses };
for (const id of ids) {
next[id] = "submitting";
}
this.statuses = next;
}
/**
* applyResults merges the verdict map returned by `submitOrder`
* into the per-command status map. Entries not present in the
* map keep their current status — useful when only a subset of
* commands round-tripped to the server. The engine-assigned
* `updatedAt` is also stashed for the next submit's stale-order
* detection (kept as plumbing only in Phase 14).
*/
applyResults(opts: {
results: Map<string, CommandStatus>;
updatedAt: number;
}): void {
const next = { ...this.statuses };
for (const [id, status] of opts.results.entries()) {
next[id] = status;
}
this.statuses = next;
this.updatedAt = opts.updatedAt;
}
/**
* markRejected switches every supplied id to `rejected`. Used by
* the order tab when `submitOrder` returns `ok: false` — the
* gateway didn't process any command, so the entire batch is
* treated as rejected.
*/
markRejected(ids: string[]): void {
const next = { ...this.statuses };
for (const id of ids) {
next[id] = "rejected";
}
this.statuses = next;
}
/**
* revertSubmittingToValid resets every entry currently in
* `submitting` back to its pre-submit status (typically `valid`).
* Called when the network layer throws an exception so the
* operator can retry without the rows looking stuck mid-flight.
*/
revertSubmittingToValid(): void {
const next = { ...this.statuses };
for (const cmd of this.commands) {
if (next[cmd.id] === "submitting") {
next[cmd.id] = validateCommand(cmd);
}
}
this.statuses = next;
}
dispose(): void {
this.destroyed = true;
this.cache = null;
}
private recomputeStatuses(): void {
const next: Record<string, CommandStatus> = {};
for (const cmd of this.commands) {
next[cmd.id] = validateCommand(cmd);
}
this.statuses = next;
}
private async persist(): Promise<void> {
if (this.cache === null || this.destroyed) return;
// `commands` is `$state`, so individual entries are proxies.
@@ -123,3 +257,14 @@ export class OrderDraftStore {
await this.cache.put(NAMESPACE, draftKey(this.gameId), snapshot);
}
}
function validateCommand(cmd: OrderCommand): CommandStatus {
switch (cmd.kind) {
case "planetRename":
return validateEntityName(cmd.name).ok ? "valid" : "invalid";
case "placeholder":
// Phase 12 placeholder entries are content-free and never
// transition out of `draft` — they are not submittable.
return "draft";
}
}
+163
View File
@@ -0,0 +1,163 @@
// Reads back the player's stored order for the current turn through
// `user.games.order.get`. Used by `OrderDraftStore` only when the
// local cache row is absent (fresh install, cleared storage, or a
// brand-new device): the local draft is the source of truth, so a
// present-but-empty cache row means "no commands" and is honoured
// over the server snapshot.
import { Builder, ByteBuffer } from "flatbuffers";
import type { GalaxyClient } from "../api/galaxy-client";
import { uuidToHiLo } from "../api/game-state";
import { UUID } from "../proto/galaxy/fbs/common";
import {
CommandPayload,
CommandPlanetRename,
UserGamesOrderGet,
UserGamesOrderGetResponse,
} from "../proto/galaxy/fbs/order";
import type { OrderCommand } from "./order-types";
const MESSAGE_TYPE = "user.games.order.get";
export class OrderLoadError extends Error {
readonly resultCode: string;
readonly code: string;
constructor(resultCode: string, code: string, message: string) {
super(message);
this.name = "OrderLoadError";
this.resultCode = resultCode;
this.code = code;
}
}
export interface FetchedOrder {
commands: OrderCommand[];
updatedAt: number;
}
/**
* fetchOrder issues `user.games.order.get` for the given game and
* turn, decodes the response, and returns the typed draft. A
* `found = false` answer (no order stored on the server) surfaces as
* an empty `commands` array — the caller treats this as a clean
* draft. Unknown command kinds in the response are skipped with a
* console warning so a backend-side schema bump never silently
* corrupts the local draft.
*/
export async function fetchOrder(
client: GalaxyClient,
gameId: string,
turn: number,
): Promise<FetchedOrder> {
if (turn < 0) {
throw new OrderLoadError(
"invalid_request",
"invalid_request",
`turn must be non-negative, got ${turn}`,
);
}
const payload = buildRequest(gameId, turn);
const result = await client.executeCommand(MESSAGE_TYPE, payload);
if (result.resultCode !== "ok") {
const { code, message } = decodeError(result.payloadBytes, result.resultCode);
throw new OrderLoadError(result.resultCode, code, message);
}
return decodeResponse(result.payloadBytes);
}
function buildRequest(gameId: string, turn: number): Uint8Array {
const builder = new Builder(64);
const [hi, lo] = uuidToHiLo(gameId);
const gameIdOffset = UUID.createUUID(builder, hi, lo);
UserGamesOrderGet.startUserGamesOrderGet(builder);
UserGamesOrderGet.addGameId(builder, gameIdOffset);
UserGamesOrderGet.addTurn(builder, BigInt(turn));
const offset = UserGamesOrderGet.endUserGamesOrderGet(builder);
builder.finish(offset);
return builder.asUint8Array();
}
function decodeResponse(payload: Uint8Array): FetchedOrder {
if (payload.length === 0) {
throw new OrderLoadError(
"internal_error",
"internal_error",
"empty user.games.order.get payload",
);
}
const buffer = new ByteBuffer(payload);
const response = UserGamesOrderGetResponse.getRootAsUserGamesOrderGetResponse(buffer);
if (!response.found()) {
return { commands: [], updatedAt: 0 };
}
const order = response.order();
if (order === null) {
throw new OrderLoadError(
"internal_error",
"internal_error",
"order missing while found=true",
);
}
const commands: OrderCommand[] = [];
const length = order.commandsLength();
for (let i = 0; i < length; i++) {
const item = order.commands(i);
if (item === null) continue;
const cmd = decodeCommand(item);
if (cmd === null) continue;
commands.push(cmd);
}
return {
commands,
updatedAt: Number(order.updatedAt()),
};
}
type CommandItemView = NonNullable<
ReturnType<NonNullable<ReturnType<UserGamesOrderGetResponse["order"]>>["commands"]>
>;
function decodeCommand(item: CommandItemView): OrderCommand | null {
if (item === null) return null;
const id = item.cmdId();
if (id === null) return null;
const payloadType = item.payloadType();
switch (payloadType) {
case CommandPayload.CommandPlanetRename: {
const inner = new CommandPlanetRename();
item.payload(inner);
return {
kind: "planetRename",
id,
planetNumber: Number(inner.number()),
name: inner.name() ?? "",
};
}
default:
console.warn(
`fetchOrder: skipping unknown command kind (payloadType=${payloadType})`,
);
return null;
}
}
function decodeError(
payload: Uint8Array,
resultCode: string,
): { code: string; message: string } {
if (payload.length === 0) {
return { code: resultCode, message: resultCode };
}
try {
const text = new TextDecoder().decode(payload);
const parsed = JSON.parse(text) as { code?: string; message?: string };
return {
code: typeof parsed.code === "string" ? parsed.code : resultCode,
message: typeof parsed.message === "string" ? parsed.message : text,
};
} catch {
return { code: resultCode, message: resultCode };
}
}
+17 -2
View File
@@ -25,13 +25,28 @@ export interface PlaceholderCommand {
readonly label: string;
}
/**
* PlanetRenameCommand is the first real command variant — Phase 14
* lands the rename action together with the submit pipeline. The
* `name` is locally validated against `validateEntityName` (the TS
* port of `pkg/util/string.go.ValidateTypeName`) before the entry is
* accepted into the draft; the same rules run server-side, so a
* locally-valid command is always accepted at the wire level.
*/
export interface PlanetRenameCommand {
readonly kind: "planetRename";
readonly id: string;
readonly planetNumber: number;
readonly name: string;
}
/**
* OrderCommand is the discriminated union of every command shape the
* local order draft can hold. The `kind` field is the discriminator;
* narrowing on it enables exhaustive `switch` statements at every
* call site. Phase 14 will widen the union with `planetRename`.
* call site.
*/
export type OrderCommand = PlaceholderCommand;
export type OrderCommand = PlaceholderCommand | PlanetRenameCommand;
/**
* CommandStatus is the lifecycle of a single command from the moment
+230
View File
@@ -0,0 +1,230 @@
// Drives the order submit pipeline: builds a FlatBuffers
// `UserGamesOrder` payload from the local draft, calls
// `client.executeCommand("user.games.order", ...)`, and translates
// the engine response into per-command results the draft store can
// merge with `applyResults`.
//
// The engine populates `cmdApplied` and `cmdErrorCode` on every
// returned command (see `game/openapi.yaml`), so the happy path
// reads real per-command outcomes. An empty response `commands`
// array — the gateway's defensive fallback when no body comes back
// — collapses to a batch-level "all applied" verdict so the player
// is never left with submitted-without-result rows.
//
// Failures fall into two buckets:
// - the gateway answers with a non-`ok` `resultCode` (auth /
// transcoder / engine validation); the result is `ok: false`
// and every submitted entry should flip to `rejected`;
// - the request itself throws (network, signature mismatch, decoder
// panic); the exception bubbles up to the caller, which leaves
// the draft entries in `submitting` for the operator to retry.
import { Builder, ByteBuffer } from "flatbuffers";
import type { GalaxyClient } from "../api/galaxy-client";
import { uuidToHiLo } from "../api/game-state";
import { UUID } from "../proto/galaxy/fbs/common";
import {
CommandItem,
CommandPayload,
CommandPlanetRename,
UserGamesOrder,
UserGamesOrderResponse,
} from "../proto/galaxy/fbs/order";
import type { OrderCommand } from "./order-types";
const MESSAGE_TYPE = "user.games.order";
export class SubmitError extends Error {
readonly resultCode: string;
readonly code: string;
constructor(resultCode: string, code: string, message: string) {
super(message);
this.name = "SubmitError";
this.resultCode = resultCode;
this.code = code;
}
}
export type CommandOutcome = "applied" | "rejected";
export interface SubmitSuccess {
ok: true;
results: Map<string, CommandOutcome>;
errorCodes: Map<string, number | null>;
updatedAt: number;
}
export interface SubmitFailure {
ok: false;
resultCode: string;
code: string;
message: string;
}
export type SubmitResult = SubmitSuccess | SubmitFailure;
export interface SubmitOptions {
updatedAt?: number;
}
/**
* submitOrder posts the `commands` slice through `user.games.order`,
* decodes the FBS response, and returns per-command outcomes the
* caller (the order tab) feeds back into `OrderDraftStore.applyResults`.
*
* @param client GalaxyClient owning the signed-gRPC transport.
* @param gameId Stringified UUID of the game whose order is submitted.
* @param commands Subset of the local draft to send. The caller has
* already filtered out non-`valid` entries.
* @param options.updatedAt Optional engine-assigned timestamp from a
* prior submit — Phase 14 always sends `0` because stale-order
* detection is not yet wired client-side.
*/
export async function submitOrder(
client: GalaxyClient,
gameId: string,
commands: OrderCommand[],
options: SubmitOptions = {},
): Promise<SubmitResult> {
const payload = buildOrderPayload(gameId, commands, options.updatedAt ?? 0);
const result = await client.executeCommand(MESSAGE_TYPE, payload);
if (result.resultCode !== "ok") {
const { code, message } = decodeError(result.payloadBytes, result.resultCode);
return {
ok: false,
resultCode: result.resultCode,
code,
message,
};
}
return decodeOrderResponse(result.payloadBytes, commands);
}
function buildOrderPayload(
gameId: string,
commands: OrderCommand[],
updatedAt: number,
): Uint8Array {
const builder = new Builder(256);
const itemOffsets = commands.map((cmd) => encodeCommandItem(builder, cmd));
const commandsVec = UserGamesOrder.createCommandsVector(builder, itemOffsets);
const [hi, lo] = uuidToHiLo(gameId);
const gameIdOffset = UUID.createUUID(builder, hi, lo);
UserGamesOrder.startUserGamesOrder(builder);
UserGamesOrder.addGameId(builder, gameIdOffset);
UserGamesOrder.addUpdatedAt(builder, BigInt(updatedAt));
UserGamesOrder.addCommands(builder, commandsVec);
const offset = UserGamesOrder.endUserGamesOrder(builder);
builder.finish(offset);
return builder.asUint8Array();
}
function encodeCommandItem(builder: Builder, cmd: OrderCommand): number {
const cmdIdOffset = builder.createString(cmd.id);
const { payloadType, payloadOffset } = encodeCommandPayload(builder, cmd);
CommandItem.startCommandItem(builder);
CommandItem.addCmdId(builder, cmdIdOffset);
CommandItem.addPayloadType(builder, payloadType);
CommandItem.addPayload(builder, payloadOffset);
return CommandItem.endCommandItem(builder);
}
function encodeCommandPayload(
builder: Builder,
cmd: OrderCommand,
): { payloadType: CommandPayload; payloadOffset: number } {
switch (cmd.kind) {
case "planetRename": {
const nameOffset = builder.createString(cmd.name);
const offset = CommandPlanetRename.createCommandPlanetRename(
builder,
BigInt(cmd.planetNumber),
nameOffset,
);
return {
payloadType: CommandPayload.CommandPlanetRename,
payloadOffset: offset,
};
}
case "placeholder":
throw new SubmitError(
"invalid_request",
"invalid_request",
`placeholder commands cannot be submitted (cmd id ${cmd.id})`,
);
}
}
function decodeOrderResponse(
payload: Uint8Array,
commands: OrderCommand[],
): SubmitSuccess {
const results = new Map<string, CommandOutcome>();
const errorCodes = new Map<string, number | null>();
let updatedAt = 0;
if (payload.length === 0) {
// Empty envelope (gateway fallback). Apply batch-level verdict.
for (const cmd of commands) {
results.set(cmd.id, "applied");
errorCodes.set(cmd.id, null);
}
return { ok: true, results, errorCodes, updatedAt };
}
const buffer = new ByteBuffer(payload);
const response = UserGamesOrderResponse.getRootAsUserGamesOrderResponse(buffer);
updatedAt = Number(response.updatedAt());
const length = response.commandsLength();
if (length === 0) {
for (const cmd of commands) {
results.set(cmd.id, "applied");
errorCodes.set(cmd.id, null);
}
return { ok: true, results, errorCodes, updatedAt };
}
for (let i = 0; i < length; i++) {
const item = response.commands(i);
if (item === null) continue;
const cmdId = item.cmdId();
if (cmdId === null) continue;
const applied = item.cmdApplied();
const errorCode = item.cmdErrorCode();
results.set(cmdId, applied === false ? "rejected" : "applied");
errorCodes.set(cmdId, errorCode === null ? null : Number(errorCode));
}
// Defensive: any submitted command not echoed back falls back to
// applied so the draft entry leaves `submitting`.
for (const cmd of commands) {
if (!results.has(cmd.id)) {
results.set(cmd.id, "applied");
errorCodes.set(cmd.id, null);
}
}
return { ok: true, results, errorCodes, updatedAt };
}
function decodeError(
payload: Uint8Array,
resultCode: string,
): { code: string; message: string } {
if (payload.length === 0) {
return { code: resultCode, message: resultCode };
}
try {
const text = new TextDecoder().decode(payload);
const parsed = JSON.parse(text) as { code?: string; message?: string };
return {
code: typeof parsed.code === "string" ? parsed.code : resultCode,
message: typeof parsed.message === "string" ? parsed.message : text,
};
} catch {
return { code: resultCode, message: resultCode };
}
}
+101
View File
@@ -0,0 +1,101 @@
// FlatBuffers payload builders for the Phase 14 Playwright suite.
// Mirrors what `pkg/transcoder/order.go` produces in production for
// the `user.games.order` POST response and the
// `user.games.order.get` GET response.
import { Builder } from "flatbuffers";
import { uuidToHiLo } from "../../../src/api/game-state";
import { UUID } from "../../../src/proto/galaxy/fbs/common";
import {
CommandItem,
CommandPayload,
CommandPlanetRename,
UserGamesOrder,
UserGamesOrderGetResponse,
UserGamesOrderResponse,
} from "../../../src/proto/galaxy/fbs/order";
export interface CommandResultFixture {
cmdId: string;
planetNumber: number;
name: string;
applied: boolean | null;
errorCode: number | null;
}
export function buildOrderResponsePayload(
gameId: string,
commands: CommandResultFixture[],
updatedAt: number,
): Uint8Array {
const builder = new Builder(256);
const itemOffsets = commands.map((c) => encodeItem(builder, c));
const commandsVec = UserGamesOrderResponse.createCommandsVector(
builder,
itemOffsets,
);
const [hi, lo] = uuidToHiLo(gameId);
const gameIdOffset = UUID.createUUID(builder, hi, lo);
UserGamesOrderResponse.startUserGamesOrderResponse(builder);
UserGamesOrderResponse.addGameId(builder, gameIdOffset);
UserGamesOrderResponse.addUpdatedAt(builder, BigInt(updatedAt));
UserGamesOrderResponse.addCommands(builder, commandsVec);
const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder);
builder.finish(offset);
return builder.asUint8Array();
}
export function buildOrderGetResponsePayload(
gameId: string,
commands: CommandResultFixture[],
updatedAt: number,
found = true,
): Uint8Array {
const builder = new Builder(256);
let orderOffset = 0;
if (found) {
const itemOffsets = commands.map((c) => encodeItem(builder, c));
const commandsVec = UserGamesOrder.createCommandsVector(
builder,
itemOffsets,
);
const [hi, lo] = uuidToHiLo(gameId);
const gameIdOffset = UUID.createUUID(builder, hi, lo);
UserGamesOrder.startUserGamesOrder(builder);
UserGamesOrder.addGameId(builder, gameIdOffset);
UserGamesOrder.addUpdatedAt(builder, BigInt(updatedAt));
UserGamesOrder.addCommands(builder, commandsVec);
orderOffset = UserGamesOrder.endUserGamesOrder(builder);
}
UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
UserGamesOrderGetResponse.addFound(builder, found);
if (orderOffset !== 0) {
UserGamesOrderGetResponse.addOrder(builder, orderOffset);
}
const offset =
UserGamesOrderGetResponse.endUserGamesOrderGetResponse(builder);
builder.finish(offset);
return builder.asUint8Array();
}
function encodeItem(builder: Builder, c: CommandResultFixture): number {
const cmdIdOffset = builder.createString(c.cmdId);
const nameOffset = builder.createString(c.name);
const inner = CommandPlanetRename.createCommandPlanetRename(
builder,
BigInt(c.planetNumber),
nameOffset,
);
CommandItem.startCommandItem(builder);
CommandItem.addCmdId(builder, cmdIdOffset);
if (c.applied !== null) CommandItem.addCmdApplied(builder, c.applied);
if (c.errorCode !== null) {
CommandItem.addCmdErrorCode(builder, BigInt(c.errorCode));
}
CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename);
CommandItem.addPayload(builder, inner);
return CommandItem.endCommandItem(builder);
}
+315
View File
@@ -0,0 +1,315 @@
// Phase 14 end-to-end coverage for the rename-planet flow. Boots an
// authenticated session, mocks the lobby + report + order routes,
// drives a click into the renderer to select a planet, opens the
// Rename action, types a new name, submits, and verifies the
// optimistic overlay (inspector + map label). A second test covers
// the rejected path: the engine answers `cmdApplied: false` and the
// inspector keeps the original name while the order tab row reads
// `rejected`.
import { fromJson, type JsonValue } from "@bufbuild/protobuf";
import { expect, test, type Page } from "@playwright/test";
import { ByteBuffer } from "flatbuffers";
import { ExecuteCommandRequestSchema } from "../../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
UserGamesOrder,
UserGamesOrderGet,
} from "../../src/proto/galaxy/fbs/order";
import { GameReportRequest } from "../../src/proto/galaxy/fbs/report";
import { forgeExecuteCommandResponseJson } from "./fixtures/sign-response";
import {
buildMyGamesListPayload,
type GameFixture,
} from "./fixtures/lobby-fbs";
import { buildReportPayload } from "./fixtures/report-fbs";
import {
buildOrderGetResponsePayload,
buildOrderResponsePayload,
type CommandResultFixture,
} from "./fixtures/order-fbs";
const SESSION_ID = "phase-14-rename-session";
const GAME_ID = "14141414-1414-1414-1414-141414141414";
const WORLD = 4000;
const CENTRE = WORLD / 2;
const TURN = 4;
interface MockOpts {
storedOrder: CommandResultFixture[];
submitOutcome: "applied" | "rejected";
}
interface MockHandle {
get submittedRenameName(): string | null;
}
async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
const game: GameFixture = {
gameId: GAME_ID,
gameName: "Phase 14 Game",
gameType: "private",
status: "running",
ownerUserId: "user-1",
minPlayers: 2,
maxPlayers: 8,
enrollmentEndsAtMs: BigInt(Date.now() + 86_400_000),
createdAtMs: BigInt(Date.now() - 86_400_000),
updatedAtMs: BigInt(Date.now()),
currentTurn: TURN,
};
let storedOrder = opts.storedOrder.slice();
let lastSubmittedName: string | null = null;
let lastReportName = "Earth";
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/ExecuteCommand",
async (route) => {
const reqText = route.request().postData();
if (reqText === null) {
await route.fulfill({ status: 400 });
return;
}
const req = fromJson(
ExecuteCommandRequestSchema,
JSON.parse(reqText) as JsonValue,
);
let resultCode = "ok";
let payload: Uint8Array;
switch (req.messageType) {
case "lobby.my.games.list":
payload = buildMyGamesListPayload([game]);
break;
case "user.games.report": {
GameReportRequest.getRootAsGameReportRequest(
new ByteBuffer(req.payloadBytes),
).gameId(new UUID());
payload = buildReportPayload({
turn: TURN,
mapWidth: WORLD,
mapHeight: WORLD,
localPlanets: [
{
number: 17,
name: lastReportName,
x: CENTRE,
y: CENTRE,
size: 1000,
resources: 10,
capital: 0,
material: 0,
population: 850,
colonists: 25,
industry: 700,
production: "drive",
freeIndustry: 175,
},
],
});
break;
}
case "user.games.order": {
const decoded = UserGamesOrder.getRootAsUserGamesOrder(
new ByteBuffer(req.payloadBytes),
);
const length = decoded.commandsLength();
const fixtures: CommandResultFixture[] = [];
for (let i = 0; i < length; i++) {
const item = decoded.commands(i);
if (item === null) continue;
const cmdId = item.cmdId() ?? "";
// Decode the embedded planetRename payload to mirror it back
// in the response.
const inner = new (await import(
"../../src/proto/galaxy/fbs/order"
)).CommandPlanetRename();
item.payload(inner);
const submittedName = inner.name() ?? "";
lastSubmittedName = submittedName;
const applied = opts.submitOutcome === "applied";
fixtures.push({
cmdId,
planetNumber: Number(inner.number()),
name: submittedName,
applied,
errorCode: applied ? null : 1,
});
}
if (opts.submitOutcome === "applied") {
storedOrder = fixtures;
lastReportName = fixtures[0]?.name ?? lastReportName;
}
payload = buildOrderResponsePayload(GAME_ID, fixtures, Date.now());
break;
}
case "user.games.order.get": {
UserGamesOrderGet.getRootAsUserGamesOrderGet(
new ByteBuffer(req.payloadBytes),
);
payload = buildOrderGetResponsePayload(
GAME_ID,
storedOrder,
Date.now(),
storedOrder.length > 0,
);
break;
}
default:
resultCode = "internal_error";
payload = new Uint8Array();
}
const body = await forgeExecuteCommandResponseJson({
requestId: req.requestId,
timestampMs: BigInt(Date.now()),
resultCode,
payloadBytes: payload,
});
await route.fulfill({
status: 200,
contentType: "application/json",
body,
});
},
);
await page.route(
"**/galaxy.gateway.v1.EdgeGateway/SubscribeEvents",
async () => {
await new Promise<void>(() => {});
},
);
return {
get submittedRenameName(): string | null {
return lastSubmittedName;
},
};
}
async function bootSession(page: Page): Promise<void> {
await page.goto("/__debug/store");
await expect(page.getByTestId("debug-store-ready")).toBeVisible();
await page.waitForFunction(() => window.__galaxyDebug?.ready === true);
await page.evaluate(() => window.__galaxyDebug!.clearSession());
await page.evaluate(
(id) => window.__galaxyDebug!.setDeviceSessionId(id),
SESSION_ID,
);
await page.evaluate(
(gameId) => window.__galaxyDebug!.clearOrderDraft(gameId),
GAME_ID,
);
}
async function clickPlanetCentre(page: Page): Promise<void> {
const canvas = page.locator("canvas");
const box = await canvas.boundingBox();
expect(box).not.toBeNull();
if (box === null) throw new Error("canvas has no bounding box");
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
}
test("rename a seeded planet, submit, observe overlay + persist after reload", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name.startsWith("chromium-mobile"),
"phase 14 spec covers desktop layout; mobile inherits the same store",
);
const handle = await mockGateway(page, {
storedOrder: [],
submitOutcome: "applied",
});
await bootSession(page);
await page.goto(`/games/${GAME_ID}/map`);
await expect(page.getByTestId("active-view-map")).toHaveAttribute(
"data-status",
"ready",
);
await clickPlanetCentre(page);
const sidebar = page.getByTestId("sidebar-tool-inspector");
await expect(sidebar.getByTestId("inspector-planet-name")).toHaveText("Earth");
await sidebar.getByTestId("inspector-planet-rename-action").click();
const input = sidebar.getByTestId("inspector-planet-rename-input");
await input.fill("New-Earth");
await sidebar.getByTestId("inspector-planet-rename-confirm").click();
// Open the order tab and assert the row.
await page.getByTestId("sidebar-tab-order").click();
const orderTool = page.getByTestId("sidebar-tool-order");
await expect(orderTool.getByTestId("order-command-label-0")).toContainText(
"New-Earth",
);
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
"valid",
);
await orderTool.getByTestId("order-submit").click();
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
"applied",
);
expect(handle.submittedRenameName).toBe("New-Earth");
// Switch back to the inspector — overlay should reflect the new name.
await page.getByTestId("sidebar-tab-inspector").click();
await expect(sidebar.getByTestId("inspector-planet-name")).toHaveText(
"New-Earth",
);
// Reload: the order draft is persisted; on cache-miss boots the
// hydrate-from-server path takes over. Both round-trips re-apply
// the overlay so the player still sees the renamed planet.
await page.reload();
await expect(page.getByTestId("active-view-map")).toHaveAttribute(
"data-status",
"ready",
);
await page.getByTestId("sidebar-tab-order").click();
await expect(orderTool.getByTestId("order-command-label-0")).toContainText(
"New-Earth",
);
});
test("rejected submit keeps the old name and surfaces the failure", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name.startsWith("chromium-mobile"),
"phase 14 spec covers desktop layout; mobile inherits the same store",
);
await mockGateway(page, {
storedOrder: [],
submitOutcome: "rejected",
});
await bootSession(page);
await page.goto(`/games/${GAME_ID}/map`);
await expect(page.getByTestId("active-view-map")).toHaveAttribute(
"data-status",
"ready",
);
await clickPlanetCentre(page);
const sidebar = page.getByTestId("sidebar-tool-inspector");
await sidebar.getByTestId("inspector-planet-rename-action").click();
await sidebar.getByTestId("inspector-planet-rename-input").fill("Mars-2");
await sidebar.getByTestId("inspector-planet-rename-confirm").click();
await page.getByTestId("sidebar-tab-order").click();
const orderTool = page.getByTestId("sidebar-tool-order");
await orderTool.getByTestId("order-submit").click();
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
"rejected",
);
await page.getByTestId("sidebar-tab-inspector").click();
// Overlay does not apply rejected commands — old name persists.
await expect(sidebar.getByTestId("inspector-planet-name")).toHaveText("Earth");
});
+66
View File
@@ -0,0 +1,66 @@
// Parity tests for the TS port of `pkg/util/string.go.ValidateTypeName`.
// Cases are aligned with `pkg/util/string_test.go.TestValidateString`
// so the client-side and server-side validators reject the same set
// of inputs — a name that's locally valid is always accepted at the
// wire level.
import { describe, expect, test } from "vitest";
import {
validateEntityName,
type EntityNameInvalidReason,
} from "../src/lib/util/entity-name";
describe("validateEntityName", () => {
const valid: { name: string; input: string; expected: string }[] = [
{ name: "latin + digits", input: "Hello_World-123", expected: "Hello_World-123" },
{ name: "cyrillic", input: "Привет_мир-42", expected: "Привет_мир-42" },
{ name: "greek", input: "Αλφα_Βητα-2024", expected: "Αλφα_Βητα-2024" },
{ name: "arabic", input: "مرحبا_العالم-7", expected: "مرحبا_العالم-7" },
{ name: "japanese katakana", input: "テスト_ケース-1", expected: "テスト_ケース-1" },
{ name: "chinese", input: "你好_世界-123", expected: "你好_世界-123" },
{ name: "hindi (combining marks)", input: "नमस्ते_दुनिया-456", expected: "नमस्ते_दुनिया-456" },
{ name: "thai (combining marks)", input: "สวัสดี_โลก-789", expected: "สวัสดี_โลก-789" },
{ name: "korean", input: "안녕하세요_세계-101", expected: "안녕하세요_세계-101" },
{ name: "trim outer whitespace", input: " Earth ", expected: "Earth" },
{ name: "valid consecutive specials", input: "Valid_(special)_Chars", expected: "Valid_(special)_Chars" },
{ name: "all allowed specials", input: "A@#b$%c^*d-_e=+f~(g)[h]{i}j", expected: "A@#b$%c^*d-_e=+f~(g)[h]{i}j" },
];
for (const tc of valid) {
test(`accepts: ${tc.name}`, () => {
const result = validateEntityName(tc.input);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value).toBe(tc.expected);
}
});
}
const invalid: {
name: string;
input: string;
reason: EntityNameInvalidReason;
}[] = [
{ name: "empty after trim", input: " ", reason: "empty" },
{ name: "explicitly empty", input: "", reason: "empty" },
{ name: "too long", input: "ValidatedStringHasTooManyCharacters", reason: "too_long" },
{ name: "internal space", input: "Test 123", reason: "whitespace" },
{ name: "internal tab", input: "Test\tName", reason: "whitespace" },
{ name: "internal newline", input: "Test\nName", reason: "whitespace" },
{ name: "starts with special after trim", input: " -Test123", reason: "starts_with_special" },
{ name: "ends with special after trim", input: "Test123- ", reason: "ends_with_special" },
{ name: "emoji", input: "Test🙂Name", reason: "disallowed_character" },
{ name: "starts with special $", input: "$pecialString", reason: "starts_with_special" },
{ name: "ends with special _", input: "SpecialString_", reason: "ends_with_special" },
{ name: "too many consecutive specials", input: "Too_Many_(special[_]Chars", reason: "consecutive_specials" },
];
for (const tc of invalid) {
test(`rejects: ${tc.name}`, () => {
const result = validateEntityName(tc.input);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.reason).toBe(tc.reason);
}
});
}
});
+14 -1
View File
@@ -23,6 +23,14 @@ import {
SELECTION_CONTEXT_KEY,
SelectionStore,
} from "../src/lib/selection.svelte";
import {
RENDERED_REPORT_CONTEXT_KEY,
createRenderedReportSource,
} from "../src/lib/rendered-report.svelte";
import {
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../src/sync/order-draft.svelte";
import type { GameReport, ReportPlanet } from "../src/api/game-state";
const pageMock = vi.hoisted(() => ({
@@ -70,17 +78,22 @@ function makeReport(planets: ReportPlanet[]): GameReport {
function withStores(report: GameReport | null): {
gameState: GameStateStore;
selection: SelectionStore;
orderDraft: OrderDraftStore;
context: Map<unknown, unknown>;
} {
const gameState = new GameStateStore();
gameState.report = report;
gameState.status = report === null ? "idle" : "ready";
const selection = new SelectionStore();
const orderDraft = new OrderDraftStore();
const renderedReport = createRenderedReportSource(gameState, orderDraft);
const context = new Map<unknown, unknown>([
[GAME_STATE_CONTEXT_KEY, gameState],
[SELECTION_CONTEXT_KEY, selection],
[ORDER_DRAFT_CONTEXT_KEY, orderDraft],
[RENDERED_REPORT_CONTEXT_KEY, renderedReport],
]);
return { gameState, selection, context };
return { gameState, selection, orderDraft, context };
}
beforeEach(() => {
+123 -1
View File
@@ -5,12 +5,19 @@
// drive it with synthetic `ReportPlanet` literals — no store.
import "@testing-library/jest-dom/vitest";
import { render } from "@testing-library/svelte";
import "fake-indexeddb/auto";
import { fireEvent, render } from "@testing-library/svelte";
import { beforeEach, describe, expect, test } from "vitest";
import { i18n } from "../src/lib/i18n/index.svelte";
import type { ReportPlanet } from "../src/api/game-state";
import Planet from "../src/lib/inspectors/planet.svelte";
import {
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../src/sync/order-draft.svelte";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB } from "../src/platform/store/idb";
beforeEach(() => {
i18n.resetForTests("en");
@@ -192,6 +199,121 @@ describe("planet inspector", () => {
expect(ui.queryByTestId("inspector-planet-field-natural_resources")).toBeNull();
});
test("Rename action is hidden for non-local planets", () => {
const ui = render(Planet, {
props: {
planet: makePlanet({
number: 9,
name: "Far",
kind: "other",
owner: "Federation",
size: 100,
resources: 5,
}),
},
});
expect(ui.queryByTestId("inspector-planet-rename-action")).toBeNull();
});
test("Rename action opens an inline editor and validates locally", async () => {
const dbName = `galaxy-rename-${crypto.randomUUID()}`;
const db = await openGalaxyDB(dbName);
const cache = new IDBCache(db);
const draft = new OrderDraftStore();
await draft.init({ cache, gameId: "00000000-0000-0000-0000-000000000abc" });
const context = new Map<unknown, unknown>([
[ORDER_DRAFT_CONTEXT_KEY, draft],
]);
const ui = render(Planet, {
props: {
planet: makePlanet({
number: 7,
name: "Earth",
kind: "local",
size: 100,
resources: 5,
population: 100,
colonists: 0,
industry: 0,
industryStockpile: 0,
materialsStockpile: 0,
production: "drive",
freeIndustry: 0,
}),
},
context,
});
const action = ui.getByTestId("inspector-planet-rename-action");
await fireEvent.click(action);
const input = ui.getByTestId("inspector-planet-rename-input") as HTMLInputElement;
expect(input.value).toBe("Earth");
const confirm = ui.getByTestId("inspector-planet-rename-confirm");
expect(confirm).not.toBeDisabled();
await fireEvent.input(input, { target: { value: " " } });
expect(ui.getByTestId("inspector-planet-rename-error")).toBeVisible();
expect(confirm).toBeDisabled();
await fireEvent.input(input, { target: { value: "New Earth!" } });
// Whitespace inside disallowed
expect(ui.getByTestId("inspector-planet-rename-error")).toBeVisible();
expect(confirm).toBeDisabled();
await fireEvent.input(input, { target: { value: "Mars-2" } });
expect(ui.queryByTestId("inspector-planet-rename-error")).toBeNull();
expect(confirm).not.toBeDisabled();
await fireEvent.click(confirm);
expect(draft.commands).toHaveLength(1);
const cmd = draft.commands[0]!;
expect(cmd.kind).toBe("planetRename");
if (cmd.kind !== "planetRename") return;
expect(cmd.planetNumber).toBe(7);
expect(cmd.name).toBe("Mars-2");
draft.dispose();
db.close();
});
test("Cancel closes the editor without adding to the draft", async () => {
const dbName = `galaxy-rename-${crypto.randomUUID()}`;
const db = await openGalaxyDB(dbName);
const cache = new IDBCache(db);
const draft = new OrderDraftStore();
await draft.init({ cache, gameId: "00000000-0000-0000-0000-000000000abc" });
const context = new Map<unknown, unknown>([
[ORDER_DRAFT_CONTEXT_KEY, draft],
]);
const ui = render(Planet, {
props: {
planet: makePlanet({
number: 1,
name: "Earth",
kind: "local",
size: 100,
resources: 5,
population: 1,
colonists: 0,
industry: 0,
industryStockpile: 0,
materialsStockpile: 0,
production: "drive",
freeIndustry: 0,
}),
},
context,
});
await fireEvent.click(ui.getByTestId("inspector-planet-rename-action"));
await fireEvent.click(ui.getByTestId("inspector-planet-rename-cancel"));
expect(ui.queryByTestId("inspector-planet-rename")).toBeNull();
expect(draft.commands).toEqual([]);
draft.dispose();
db.close();
});
test("missing production string falls back to the localised placeholder", () => {
const ui = render(Planet, {
props: {
+154
View File
@@ -175,4 +175,158 @@ describe("OrderDraftStore", () => {
expect(reload.commands.map((c) => c.id)).toEqual(["c1"]);
reload.dispose();
});
test("absent cache row flips needsServerHydration flag", async () => {
const store = new OrderDraftStore();
await store.init({ cache, gameId: GAME_ID });
expect(store.needsServerHydration).toBe(true);
store.dispose();
});
test("explicitly empty cache row honours the user's empty draft", async () => {
const seeded = new OrderDraftStore();
await seeded.init({ cache, gameId: GAME_ID });
await seeded.add({
kind: "planetRename",
id: "00000000-0000-0000-0000-000000000001",
planetNumber: 7,
name: "Earth",
});
await seeded.remove("00000000-0000-0000-0000-000000000001");
seeded.dispose();
const reload = new OrderDraftStore();
await reload.init({ cache, gameId: GAME_ID });
expect(reload.needsServerHydration).toBe(false);
expect(reload.commands).toEqual([]);
reload.dispose();
});
test("planetRename validates locally and statuses reflect valid/invalid", async () => {
const store = new OrderDraftStore();
await store.init({ cache, gameId: GAME_ID });
await store.add({
kind: "planetRename",
id: "id-valid",
planetNumber: 1,
name: "Earth",
});
await store.add({
kind: "planetRename",
id: "id-invalid",
planetNumber: 2,
name: "$bad",
});
expect(store.statuses["id-valid"]).toBe("valid");
expect(store.statuses["id-invalid"]).toBe("invalid");
store.dispose();
});
test("markSubmitting / applyResults flip the status map", async () => {
const store = new OrderDraftStore();
await store.init({ cache, gameId: GAME_ID });
await store.add({
kind: "planetRename",
id: "id-1",
planetNumber: 1,
name: "Earth",
});
store.markSubmitting(["id-1"]);
expect(store.statuses["id-1"]).toBe("submitting");
store.applyResults({
results: new Map([["id-1", "applied"] as const]),
updatedAt: 99,
});
expect(store.statuses["id-1"]).toBe("applied");
expect(store.updatedAt).toBe(99);
store.dispose();
});
test("markRejected switches submitting entries to rejected", async () => {
const store = new OrderDraftStore();
await store.init({ cache, gameId: GAME_ID });
await store.add({
kind: "planetRename",
id: "id-1",
planetNumber: 1,
name: "Earth",
});
store.markSubmitting(["id-1"]);
store.markRejected(["id-1"]);
expect(store.statuses["id-1"]).toBe("rejected");
store.dispose();
});
test("revertSubmittingToValid restores status after a thrown submit", async () => {
const store = new OrderDraftStore();
await store.init({ cache, gameId: GAME_ID });
await store.add({
kind: "planetRename",
id: "id-1",
planetNumber: 1,
name: "Earth",
});
store.markSubmitting(["id-1"]);
store.revertSubmittingToValid();
expect(store.statuses["id-1"]).toBe("valid");
store.dispose();
});
test("hydrateFromServer seeds the draft on a fresh cache", async () => {
const fakeClient = {
executeCommand: async () => {
const { Builder } = await import("flatbuffers");
const { UUID } = await import("../src/proto/galaxy/fbs/common");
const order = await import("../src/proto/galaxy/fbs/order");
const builder = new Builder(128);
const cmdId = builder.createString("hydr-1");
const name = builder.createString("Hydrated");
const inner = order.CommandPlanetRename.createCommandPlanetRename(
builder,
BigInt(7),
name,
);
order.CommandItem.startCommandItem(builder);
order.CommandItem.addCmdId(builder, cmdId);
order.CommandItem.addPayloadType(
builder,
order.CommandPayload.CommandPlanetRename,
);
order.CommandItem.addPayload(builder, inner);
const item = order.CommandItem.endCommandItem(builder);
const cmds = order.UserGamesOrder.createCommandsVector(builder, [item]);
const [hi, lo] = (await import("../src/api/game-state")).uuidToHiLo(
GAME_ID,
);
const gameIdOffset = UUID.createUUID(builder, hi, lo);
order.UserGamesOrder.startUserGamesOrder(builder);
order.UserGamesOrder.addGameId(builder, gameIdOffset);
order.UserGamesOrder.addUpdatedAt(builder, BigInt(7));
order.UserGamesOrder.addCommands(builder, cmds);
const orderOffset = order.UserGamesOrder.endUserGamesOrder(builder);
order.UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
order.UserGamesOrderGetResponse.addFound(builder, true);
order.UserGamesOrderGetResponse.addOrder(builder, orderOffset);
const offset =
order.UserGamesOrderGetResponse.endUserGamesOrderGetResponse(builder);
builder.finish(offset);
return {
resultCode: "ok",
payloadBytes: builder.asUint8Array(),
};
},
};
const store = new OrderDraftStore();
await store.init({ cache, gameId: GAME_ID });
expect(store.needsServerHydration).toBe(true);
await store.hydrateFromServer({
client: fakeClient as never,
turn: 5,
});
expect(store.commands).toHaveLength(1);
expect(store.commands[0]!.id).toBe("hydr-1");
expect(store.updatedAt).toBe(7);
expect(store.needsServerHydration).toBe(false);
store.dispose();
});
});
+151
View File
@@ -0,0 +1,151 @@
// Vitest unit coverage for `sync/order-load.ts`. Builds FBS
// `UserGamesOrderGetResponse` payloads by hand and verifies the
// decoder produces the expected `OrderCommand[]`.
import { Builder } from "flatbuffers";
import { describe, expect, test, vi } from "vitest";
import type { GalaxyClient } from "../src/api/galaxy-client";
import { uuidToHiLo } from "../src/api/game-state";
import { UUID } from "../src/proto/galaxy/fbs/common";
import {
CommandItem,
CommandPayload,
CommandPlanetRename,
UserGamesOrder,
UserGamesOrderGet,
UserGamesOrderGetResponse,
} from "../src/proto/galaxy/fbs/order";
import { fetchOrder, OrderLoadError } from "../src/sync/order-load";
const GAME_ID = "11111111-2222-3333-4444-555555555555";
function mockClient(
executeCommand: (
messageType: string,
payload: Uint8Array,
) => Promise<{ resultCode: string; payloadBytes: Uint8Array }>,
): GalaxyClient {
return { executeCommand } as unknown as GalaxyClient;
}
function buildResponse(
commands: { id: string; planetNumber: number; name: string }[],
updatedAt: number,
found = true,
): Uint8Array {
const builder = new Builder(256);
let orderOffset = 0;
if (found) {
const itemOffsets = commands.map((c) => {
const cmdIdOffset = builder.createString(c.id);
const nameOffset = builder.createString(c.name);
const inner = CommandPlanetRename.createCommandPlanetRename(
builder,
BigInt(c.planetNumber),
nameOffset,
);
CommandItem.startCommandItem(builder);
CommandItem.addCmdId(builder, cmdIdOffset);
CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename);
CommandItem.addPayload(builder, inner);
return CommandItem.endCommandItem(builder);
});
const commandsVec = UserGamesOrder.createCommandsVector(builder, itemOffsets);
const [hi, lo] = uuidToHiLo(GAME_ID);
const gameIdOffset = UUID.createUUID(builder, hi, lo);
UserGamesOrder.startUserGamesOrder(builder);
UserGamesOrder.addGameId(builder, gameIdOffset);
UserGamesOrder.addUpdatedAt(builder, BigInt(updatedAt));
UserGamesOrder.addCommands(builder, commandsVec);
orderOffset = UserGamesOrder.endUserGamesOrder(builder);
}
UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
UserGamesOrderGetResponse.addFound(builder, found);
if (orderOffset !== 0) {
UserGamesOrderGetResponse.addOrder(builder, orderOffset);
}
const offset = UserGamesOrderGetResponse.endUserGamesOrderGetResponse(builder);
builder.finish(offset);
return builder.asUint8Array();
}
describe("fetchOrder", () => {
test("decodes a found response into typed commands", async () => {
const responsePayload = buildResponse(
[{ id: "cmd-1", planetNumber: 7, name: "Earth" }],
42,
);
const exec = vi.fn(async (messageType: string) => {
expect(messageType).toBe("user.games.order.get");
return { resultCode: "ok", payloadBytes: responsePayload };
});
const result = await fetchOrder(mockClient(exec), GAME_ID, 5);
expect(result.commands).toHaveLength(1);
const cmd = result.commands[0]!;
expect(cmd.kind).toBe("planetRename");
if (cmd.kind !== "planetRename") return;
expect(cmd.id).toBe("cmd-1");
expect(cmd.planetNumber).toBe(7);
expect(cmd.name).toBe("Earth");
expect(result.updatedAt).toBe(42);
});
test("found=false surfaces as an empty draft", async () => {
const responsePayload = buildResponse([], 0, false);
const exec = vi.fn(async () => ({
resultCode: "ok",
payloadBytes: responsePayload,
}));
const result = await fetchOrder(mockClient(exec), GAME_ID, 5);
expect(result.commands).toEqual([]);
expect(result.updatedAt).toBe(0);
});
test("rejects negative turn before issuing a request", async () => {
const exec = vi.fn(async () => ({
resultCode: "ok",
payloadBytes: new Uint8Array(),
}));
await expect(fetchOrder(mockClient(exec), GAME_ID, -1)).rejects.toBeInstanceOf(
OrderLoadError,
);
expect(exec).not.toHaveBeenCalled();
});
test("throws OrderLoadError on non-ok resultCode", async () => {
const exec = vi.fn(async () => ({
resultCode: "internal_error",
payloadBytes: new TextEncoder().encode(
JSON.stringify({ code: "boom", message: "down" }),
),
}));
await expect(fetchOrder(mockClient(exec), GAME_ID, 5)).rejects.toMatchObject({
name: "OrderLoadError",
resultCode: "internal_error",
code: "boom",
});
});
test("posts a well-formed UserGamesOrderGet payload", async () => {
let captured: Uint8Array | null = null;
const exec = vi.fn(async (_messageType, payload: Uint8Array) => {
captured = payload;
return {
resultCode: "ok",
payloadBytes: buildResponse([], 0, false),
};
});
await fetchOrder(mockClient(exec), GAME_ID, 9);
expect(captured).not.toBeNull();
const decoded = UserGamesOrderGet.getRootAsUserGamesOrderGet(
new (await import("flatbuffers")).ByteBuffer(captured!),
);
expect(Number(decoded.turn())).toBe(9);
const id = decoded.gameId();
expect(id).not.toBeNull();
});
});
+143
View File
@@ -0,0 +1,143 @@
// Vitest unit coverage for the pure `applyOrderOverlay` projection.
// Phase 14 understands `planetRename` only; future phases (set
// production, route updates) will extend the overlay and need
// equivalent cases here.
import { describe, expect, test } from "vitest";
import {
applyOrderOverlay,
type GameReport,
type ReportPlanet,
} from "../src/api/game-state";
import type { CommandStatus, OrderCommand } from "../src/sync/order-types";
function makePlanet(overrides: Partial<ReportPlanet>): ReportPlanet {
return {
number: 0,
name: "",
x: 0,
y: 0,
kind: "local",
owner: null,
size: null,
resources: null,
industryStockpile: null,
materialsStockpile: null,
industry: null,
population: null,
colonists: null,
production: null,
freeIndustry: null,
...overrides,
};
}
function makeReport(planets: ReportPlanet[]): GameReport {
return {
turn: 4,
mapWidth: 4000,
mapHeight: 4000,
planetCount: planets.length,
planets,
};
}
describe("applyOrderOverlay", () => {
test("returns the same report when no commands match", () => {
const report = makeReport([makePlanet({ number: 1, name: "Earth" })]);
const out = applyOrderOverlay(report, [], {});
expect(out).toBe(report);
});
test("renames a planet on applied commands", () => {
const report = makeReport([
makePlanet({ number: 1, name: "Earth" }),
makePlanet({ number: 2, name: "Mars" }),
]);
const cmd: OrderCommand = {
kind: "planetRename",
id: "cmd-1",
planetNumber: 1,
name: "New Earth",
};
const statuses: Record<string, CommandStatus> = { "cmd-1": "applied" };
const out = applyOrderOverlay(report, [cmd], statuses);
expect(out).not.toBe(report);
expect(out.planets[0]!.name).toBe("New Earth");
expect(out.planets[1]!.name).toBe("Mars");
// raw report stays untouched
expect(report.planets[0]!.name).toBe("Earth");
});
test("renames on submitting too (in-flight optimistic)", () => {
const report = makeReport([makePlanet({ number: 1, name: "Earth" })]);
const cmd: OrderCommand = {
kind: "planetRename",
id: "cmd-1",
planetNumber: 1,
name: "Pending",
};
const out = applyOrderOverlay(report, [cmd], { "cmd-1": "submitting" });
expect(out.planets[0]!.name).toBe("Pending");
});
test("skips unsubmitted statuses (draft/valid/invalid/rejected)", () => {
const report = makeReport([makePlanet({ number: 1, name: "Earth" })]);
const cmd: OrderCommand = {
kind: "planetRename",
id: "cmd-1",
planetNumber: 1,
name: "Tentative",
};
for (const status of ["draft", "valid", "invalid", "rejected"] as const) {
const out = applyOrderOverlay(report, [cmd], { "cmd-1": status });
expect(out.planets[0]!.name).toBe("Earth");
}
});
test("ignores rename for missing planet (visibility lost)", () => {
const report = makeReport([makePlanet({ number: 1, name: "Earth" })]);
const cmd: OrderCommand = {
kind: "planetRename",
id: "cmd-1",
planetNumber: 99,
name: "Phantom",
};
const out = applyOrderOverlay(report, [cmd], { "cmd-1": "applied" });
expect(out).toBe(report);
});
test("placeholder commands pass through", () => {
const report = makeReport([makePlanet({ number: 1, name: "Earth" })]);
const cmd: OrderCommand = {
kind: "placeholder",
id: "cmd-1",
label: "noop",
};
const out = applyOrderOverlay(report, [cmd], { "cmd-1": "applied" });
expect(out).toBe(report);
});
test("multiple renames apply in command order", () => {
const report = makeReport([makePlanet({ number: 1, name: "Old" })]);
const first: OrderCommand = {
kind: "planetRename",
id: "cmd-1",
planetNumber: 1,
name: "Mid",
};
const second: OrderCommand = {
kind: "planetRename",
id: "cmd-2",
planetNumber: 1,
name: "Final",
};
const out = applyOrderOverlay(report, [first, second], {
"cmd-1": "applied",
"cmd-2": "applied",
});
expect(out.planets[0]!.name).toBe("Final");
});
});
+222
View File
@@ -0,0 +1,222 @@
// Component coverage for the Phase 14 order-tab submit flow. Drives
// the tab against an in-memory `OrderDraftStore`, a synthetic
// `GalaxyClient`, and a stubbed `GameStateStore.refresh`. Every
// case asserts both the rendered DOM (status badges, button state)
// and the side effect on the draft store (per-command status flips).
import "@testing-library/jest-dom/vitest";
import "fake-indexeddb/auto";
import { fireEvent, render, waitFor } from "@testing-library/svelte";
import { Builder } from "flatbuffers";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import OrderTab from "../src/lib/sidebar/order-tab.svelte";
import {
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../src/sync/order-draft.svelte";
import {
GAME_STATE_CONTEXT_KEY,
GameStateStore,
} from "../src/lib/game-state.svelte";
import {
GALAXY_CLIENT_CONTEXT_KEY,
GalaxyClientHolder,
} from "../src/lib/galaxy-client-context.svelte";
import { i18n } from "../src/lib/i18n/index.svelte";
import { uuidToHiLo } from "../src/api/game-state";
import type { GalaxyClient } from "../src/api/galaxy-client";
import type { OrderCommand } from "../src/sync/order-types";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
import type { Cache } from "../src/platform/store/index";
import { UUID } from "../src/proto/galaxy/fbs/common";
import {
CommandItem,
CommandPayload,
CommandPlanetRename,
UserGamesOrderResponse,
} from "../src/proto/galaxy/fbs/order";
const GAME_ID = "11111111-2222-3333-4444-555555555555";
let db: Awaited<ReturnType<typeof openGalaxyDB>>;
let dbName: string;
let cache: Cache;
beforeEach(async () => {
dbName = `galaxy-order-tab-${crypto.randomUUID()}`;
db = await openGalaxyDB(dbName);
cache = new IDBCache(db);
i18n.resetForTests("en");
});
afterEach(async () => {
db.close();
await new Promise<void>((resolve) => {
const req = indexedDB.deleteDatabase(dbName);
req.onsuccess = () => resolve();
req.onerror = () => resolve();
req.onblocked = () => resolve();
});
});
interface Setup {
context: Map<unknown, unknown>;
draft: OrderDraftStore;
gameState: GameStateStore;
clientHolder: GalaxyClientHolder;
exec: ReturnType<typeof vi.fn>;
refresh: ReturnType<typeof vi.fn>;
}
function buildResponse(
commands: { id: string; applied: boolean | null; errorCode: number | null }[],
updatedAt: number,
): Uint8Array {
const builder = new Builder(256);
const itemOffsets = commands.map((c) => {
const cmdIdOffset = builder.createString(c.id);
const nameOffset = builder.createString("ignored");
const inner = CommandPlanetRename.createCommandPlanetRename(
builder,
BigInt(0),
nameOffset,
);
CommandItem.startCommandItem(builder);
CommandItem.addCmdId(builder, cmdIdOffset);
if (c.applied !== null) CommandItem.addCmdApplied(builder, c.applied);
if (c.errorCode !== null) {
CommandItem.addCmdErrorCode(builder, BigInt(c.errorCode));
}
CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename);
CommandItem.addPayload(builder, inner);
return CommandItem.endCommandItem(builder);
});
const commandsVec = UserGamesOrderResponse.createCommandsVector(
builder,
itemOffsets,
);
const [hi, lo] = uuidToHiLo(GAME_ID);
const gameIdOffset = UUID.createUUID(builder, hi, lo);
UserGamesOrderResponse.startUserGamesOrderResponse(builder);
UserGamesOrderResponse.addGameId(builder, gameIdOffset);
UserGamesOrderResponse.addUpdatedAt(builder, BigInt(updatedAt));
UserGamesOrderResponse.addCommands(builder, commandsVec);
const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder);
builder.finish(offset);
return builder.asUint8Array();
}
async function makeSetup(commands: OrderCommand[]): Promise<Setup> {
const draft = new OrderDraftStore();
await draft.init({ cache, gameId: GAME_ID });
for (const cmd of commands) {
await draft.add(cmd);
}
const gameState = new GameStateStore();
gameState.gameId = GAME_ID;
gameState.status = "ready";
const refresh = vi.fn(async () => {});
gameState.refresh = refresh as unknown as typeof gameState.refresh;
const clientHolder = new GalaxyClientHolder();
const exec = vi.fn(async (_messageType: string, _payload: Uint8Array) => ({
resultCode: "ok",
payloadBytes: buildResponse(
commands.map((cmd) => ({
id: cmd.id,
applied: true,
errorCode: null,
})),
17,
),
}));
clientHolder.set({ executeCommand: exec } as unknown as GalaxyClient);
const context = new Map<unknown, unknown>([
[ORDER_DRAFT_CONTEXT_KEY, draft],
[GAME_STATE_CONTEXT_KEY, gameState],
[GALAXY_CLIENT_CONTEXT_KEY, clientHolder],
]);
return { context, draft, gameState, clientHolder, exec, refresh };
}
describe("order-tab", () => {
test("renders the empty state when the draft has no commands", async () => {
const { context } = await makeSetup([]);
const ui = render(OrderTab, { context });
expect(ui.getByTestId("order-empty")).toBeVisible();
expect(ui.queryByTestId("order-submit")).toBeNull();
});
test("Submit is disabled when every entry is invalid", async () => {
const { context } = await makeSetup([
{ kind: "planetRename", id: "id-1", planetNumber: 1, name: "" },
]);
const ui = render(OrderTab, { context });
const submit = ui.getByTestId("order-submit");
expect(submit).toBeDisabled();
expect(ui.getByTestId("order-command-status-0")).toHaveTextContent(
"invalid",
);
});
test("Submit posts every valid command and applies returned statuses", async () => {
const { context, draft, exec, refresh } = await makeSetup([
{ kind: "planetRename", id: "id-1", planetNumber: 1, name: "Earth" },
]);
const ui = render(OrderTab, { context });
const submit = ui.getByTestId("order-submit");
expect(submit).not.toBeDisabled();
expect(ui.getByTestId("order-command-status-0")).toHaveTextContent("valid");
await fireEvent.click(submit);
await waitFor(() => {
expect(draft.statuses["id-1"]).toBe("applied");
});
expect(exec).toHaveBeenCalledTimes(1);
expect(refresh).toHaveBeenCalledTimes(1);
expect(ui.getByTestId("order-command-status-0")).toHaveTextContent(
"applied",
);
});
test("Non-ok response marks every submitting entry as rejected", async () => {
const { context, draft, refresh } = await makeSetup([
{ kind: "planetRename", id: "id-1", planetNumber: 1, name: "Earth" },
]);
const exec = vi.fn(async () => ({
resultCode: "invalid_request",
payloadBytes: new TextEncoder().encode(
JSON.stringify({ code: "boom", message: "down" }),
),
}));
const holder = context.get(GALAXY_CLIENT_CONTEXT_KEY) as GalaxyClientHolder;
holder.set({ executeCommand: exec } as unknown as GalaxyClient);
const ui = render(OrderTab, { context });
await fireEvent.click(ui.getByTestId("order-submit"));
await waitFor(() => {
expect(draft.statuses["id-1"]).toBe("rejected");
});
expect(refresh).not.toHaveBeenCalled();
expect(ui.getByTestId("order-submit-error")).toHaveTextContent("down");
});
test("Already-applied entries do not get re-submitted", async () => {
const { context, draft, exec } = await makeSetup([
{ kind: "planetRename", id: "id-1", planetNumber: 1, name: "Earth" },
]);
draft.markSubmitting(["id-1"]);
draft.applyResults({
results: new Map([["id-1", "applied"] as const]),
updatedAt: 1,
});
const ui = render(OrderTab, { context });
const submit = ui.getByTestId("order-submit");
expect(submit).toBeDisabled();
expect(exec).not.toHaveBeenCalled();
});
});
+181
View File
@@ -0,0 +1,181 @@
// Vitest unit coverage for `sync/submit.ts`. Drives the submit
// pipeline against a stub `GalaxyClient` whose `executeCommand`
// hand-builds FBS responses, so the parser is exercised against
// payloads identical to what the real gateway returns.
import { Builder } from "flatbuffers";
import { describe, expect, test, vi } from "vitest";
import type { GalaxyClient } from "../src/api/galaxy-client";
import { uuidToHiLo } from "../src/api/game-state";
import { UUID } from "../src/proto/galaxy/fbs/common";
import {
CommandItem,
CommandPlanetRename,
CommandPayload,
UserGamesOrder,
UserGamesOrderResponse,
} from "../src/proto/galaxy/fbs/order";
import { submitOrder } from "../src/sync/submit";
import type { OrderCommand } from "../src/sync/order-types";
const GAME_ID = "11111111-2222-3333-4444-555555555555";
function mockClient(
executeCommand: (
messageType: string,
payload: Uint8Array,
) => Promise<{ resultCode: string; payloadBytes: Uint8Array }>,
): GalaxyClient {
return { executeCommand } as unknown as GalaxyClient;
}
function buildResponse(
commands: { id: string; applied: boolean | null; errorCode: number | null }[],
updatedAt: number,
): Uint8Array {
const builder = new Builder(256);
const itemOffsets = commands.map((c) => {
const cmdIdOffset = builder.createString(c.id);
const nameOffset = builder.createString("ignored");
const payloadOffset = CommandPlanetRename.createCommandPlanetRename(
builder,
BigInt(0),
nameOffset,
);
CommandItem.startCommandItem(builder);
CommandItem.addCmdId(builder, cmdIdOffset);
if (c.applied !== null) CommandItem.addCmdApplied(builder, c.applied);
if (c.errorCode !== null) {
CommandItem.addCmdErrorCode(builder, BigInt(c.errorCode));
}
CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename);
CommandItem.addPayload(builder, payloadOffset);
return CommandItem.endCommandItem(builder);
});
const commandsVec = UserGamesOrderResponse.createCommandsVector(builder, itemOffsets);
const [hi, lo] = uuidToHiLo(GAME_ID);
const gameIdOffset = UUID.createUUID(builder, hi, lo);
UserGamesOrderResponse.startUserGamesOrderResponse(builder);
UserGamesOrderResponse.addGameId(builder, gameIdOffset);
UserGamesOrderResponse.addUpdatedAt(builder, BigInt(updatedAt));
UserGamesOrderResponse.addCommands(builder, commandsVec);
const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder);
builder.finish(offset);
return builder.asUint8Array();
}
const sampleRename: OrderCommand = {
kind: "planetRename",
id: "00000000-0000-0000-0000-00000000aaaa",
planetNumber: 7,
name: "Earth",
};
describe("submitOrder", () => {
test("decodes per-command results from a populated response", async () => {
const responsePayload = buildResponse(
[{ id: sampleRename.id, applied: true, errorCode: null }],
99,
);
const exec = vi.fn(async () => ({
resultCode: "ok",
payloadBytes: responsePayload,
}));
const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename]);
expect(exec).toHaveBeenCalledOnce();
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.results.get(sampleRename.id)).toBe("applied");
expect(result.errorCodes.get(sampleRename.id)).toBeNull();
expect(result.updatedAt).toBe(99);
});
test("falls back to batch-level applied when commands array is empty", async () => {
// Hand-craft an envelope without `commands` to mimic the legacy
// gateway behaviour (or a 204 wrapped via the fallback path).
const builder = new Builder(64);
UserGamesOrderResponse.startUserGamesOrderResponse(builder);
const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder);
builder.finish(offset);
const exec = vi.fn(async () => ({
resultCode: "ok",
payloadBytes: builder.asUint8Array(),
}));
const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename]);
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.results.get(sampleRename.id)).toBe("applied");
expect(result.errorCodes.get(sampleRename.id)).toBeNull();
});
test("surfaces mixed applied / rejected entries by cmd id", async () => {
const second: OrderCommand = {
kind: "planetRename",
id: "00000000-0000-0000-0000-00000000bbbb",
planetNumber: 8,
name: "Mars",
};
const responsePayload = buildResponse(
[
{ id: sampleRename.id, applied: true, errorCode: null },
{ id: second.id, applied: false, errorCode: 42 },
],
120,
);
const exec = vi.fn(async () => ({
resultCode: "ok",
payloadBytes: responsePayload,
}));
const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename, second]);
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.results.get(sampleRename.id)).toBe("applied");
expect(result.errorCodes.get(sampleRename.id)).toBeNull();
expect(result.results.get(second.id)).toBe("rejected");
expect(result.errorCodes.get(second.id)).toBe(42);
});
test("returns SubmitFailure on non-ok resultCode without throwing", async () => {
const exec = vi.fn(async () => ({
resultCode: "invalid_request",
payloadBytes: new TextEncoder().encode(
JSON.stringify({ code: "validation_failed", message: "bad name" }),
),
}));
const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename]);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.resultCode).toBe("invalid_request");
expect(result.code).toBe("validation_failed");
expect(result.message).toBe("bad name");
});
test("posts a well-formed UserGamesOrder payload", async () => {
let captured: Uint8Array | null = null;
const exec = vi.fn(async (_messageType, payload: Uint8Array) => {
captured = payload;
return { resultCode: "ok", payloadBytes: new Uint8Array() };
});
await submitOrder(mockClient(exec), GAME_ID, [sampleRename]);
expect(captured).not.toBeNull();
const decoded = UserGamesOrder.getRootAsUserGamesOrder(
new (await import("flatbuffers")).ByteBuffer(captured!),
);
expect(decoded.commandsLength()).toBe(1);
const item = decoded.commands(0);
expect(item).not.toBeNull();
expect(item!.cmdId()).toBe(sampleRename.id);
expect(item!.payloadType()).toBe(CommandPayload.CommandPlanetRename);
const inner = new CommandPlanetRename();
item!.payload(inner);
expect(Number(inner.number())).toBe(7);
expect(inner.name()).toBe("Earth");
});
});