ui: plan 01-27 done #1
@@ -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")
|
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>`
|
// GetReport calls `GET /api/v1/report?player=<raceName>&turn=<turn>`
|
||||||
// and returns the engine response body verbatim.
|
// and returns the engine response body verbatim.
|
||||||
func (c *Client) GetReport(ctx context.Context, baseURL, raceName string, turn int) (json.RawMessage, error) {
|
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) {
|
func TestClientHealthzSuccess(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != pathHealthz {
|
if r.URL.Path != pathHealthz {
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ var pathParamStubs = map[string]string{
|
|||||||
"turn": "42",
|
"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
|
// requestBodyStubs lists the JSON request bodies the contract test sends for
|
||||||
// each operationId. Operations missing from the map default to an empty
|
// each operationId. Operations missing from the map default to an empty
|
||||||
// object `{}`, which is a valid placeholder thanks to `additionalProperties:
|
// object `{}`, which is a valid placeholder thanks to `additionalProperties:
|
||||||
@@ -323,6 +331,9 @@ func buildRequest(t *testing.T, c contractOperation) *http.Request {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
target := substitutePathParams(t, c.path)
|
target := substitutePathParams(t, c.path)
|
||||||
|
if query := buildQuery(t, c); query != "" {
|
||||||
|
target += "?" + query
|
||||||
|
}
|
||||||
url := "http://backend.internal" + target
|
url := "http://backend.internal" + target
|
||||||
|
|
||||||
body := bodyFor(t, c)
|
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 {
|
func substitutePathParams(t *testing.T, templated string) string {
|
||||||
t.Helper()
|
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}.
|
// Report handles GET /api/v1/user/games/{game_id}/reports/{turn}.
|
||||||
func (h *UserGamesHandlers) Report() gin.HandlerFunc {
|
func (h *UserGamesHandlers) Report() gin.HandlerFunc {
|
||||||
if h == nil || h.runtime == nil || h.engine == nil {
|
if h == nil || h.runtime == nil || h.engine == nil {
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ func registerUserRoutes(router *gin.Engine, instruments *metrics.Instruments, de
|
|||||||
userGames := group.Group("/games")
|
userGames := group.Group("/games")
|
||||||
userGames.POST("/:game_id/commands", deps.UserGames.Commands())
|
userGames.POST("/:game_id/commands", deps.UserGames.Commands())
|
||||||
userGames.POST("/:game_id/orders", deps.UserGames.Orders())
|
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())
|
userGames.GET("/:game_id/reports/:turn", deps.UserGames.Report())
|
||||||
|
|
||||||
userSessions := group.Group("/sessions")
|
userSessions := group.Group("/sessions")
|
||||||
|
|||||||
+45
-1
@@ -1023,7 +1023,11 @@ paths:
|
|||||||
$ref: "#/components/schemas/EngineOrder"
|
$ref: "#/components/schemas/EngineOrder"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"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:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -1036,6 +1040,46 @@ paths:
|
|||||||
$ref: "#/components/responses/NotImplementedError"
|
$ref: "#/components/responses/NotImplementedError"
|
||||||
"500":
|
"500":
|
||||||
$ref: "#/components/responses/InternalError"
|
$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}:
|
/api/v1/user/games/{game_id}/reports/{turn}:
|
||||||
get:
|
get:
|
||||||
tags: [User]
|
tags: [User]
|
||||||
|
|||||||
@@ -371,11 +371,15 @@ Authenticated client traffic for in-game operations crosses three
|
|||||||
serialisation boundaries: signed-gRPC FlatBuffers (client ↔ gateway),
|
serialisation boundaries: signed-gRPC FlatBuffers (client ↔ gateway),
|
||||||
JSON over REST (gateway ↔ backend), and JSON over REST again
|
JSON over REST (gateway ↔ backend), and JSON over REST again
|
||||||
(backend ↔ engine). Gateway owns the FB ↔ JSON transcoding for the
|
(backend ↔ engine). Gateway owns the FB ↔ JSON transcoding for the
|
||||||
three message types `user.games.command`, `user.games.order`,
|
four message types `user.games.command`, `user.games.order`,
|
||||||
`user.games.report` (FB schemas in `pkg/schema/fbs/{order,report}`,
|
`user.games.order.get`, `user.games.report` (FB schemas in
|
||||||
encoders in `pkg/transcoder`). Backend never touches FlatBuffers and
|
`pkg/schema/fbs/{order,report}`, encoders in `pkg/transcoder`).
|
||||||
never re-interprets the JSON beyond rebinding the actor field from
|
`user.games.order.get` reads back the player's stored order for a
|
||||||
the runtime player mapping (clients never carry a trusted actor).
|
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`:
|
Container state is owned by `backend/internal/runtime`:
|
||||||
|
|
||||||
|
|||||||
+9
-6
@@ -606,13 +606,16 @@ not duplicated here.
|
|||||||
|
|
||||||
### 6.2 Backend's role: pass-through with authorisation
|
### 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`,
|
message types on the authenticated surface — `user.games.command`,
|
||||||
`user.games.order`, `user.games.report` — each with a typed
|
`user.games.order`, `user.games.order.get`, `user.games.report` —
|
||||||
FlatBuffers payload. Gateway transcodes the FB request into the JSON
|
each with a typed FlatBuffers payload. Gateway transcodes the FB
|
||||||
shape backend expects, forwards over plain REST to the corresponding
|
request into the JSON shape backend expects, forwards over plain
|
||||||
`/api/v1/user/games/{game_id}/*` endpoint, then transcodes the JSON
|
REST to the corresponding `/api/v1/user/games/{game_id}/*` endpoint,
|
||||||
response back into FB before signing the reply.
|
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
|
For every in-game endpoint the user surface acts as an authorised
|
||||||
pass-through to the engine container. Backend:
|
pass-through to the engine container. Backend:
|
||||||
|
|||||||
@@ -624,12 +624,17 @@ Wire-формат команд, приказов и отчётов — собс
|
|||||||
### 6.2 Роль backend: pass-through с авторизацией
|
### 6.2 Роль backend: pass-through с авторизацией
|
||||||
|
|
||||||
Подписанный конвейер аутентифицированного edge для in-game-трафика
|
Подписанный конвейер аутентифицированного edge для in-game-трафика
|
||||||
использует три message types на аутентифицированной поверхности —
|
использует четыре message types на аутентифицированной поверхности —
|
||||||
`user.games.command`, `user.games.order`, `user.games.report` —
|
`user.games.command`, `user.games.order`, `user.games.order.get`,
|
||||||
у каждого типизированный FlatBuffers-payload. Gateway транскодирует FB-запрос в JSON-форму,
|
`user.games.report` — у каждого типизированный FlatBuffers-payload.
|
||||||
которую ждёт backend, форвардит её REST'ом в соответствующий
|
Gateway транскодирует FB-запрос в JSON-форму, которую ждёт backend,
|
||||||
|
форвардит её REST'ом в соответствующий
|
||||||
`/api/v1/user/games/{game_id}/*` endpoint, после чего транскодирует
|
`/api/v1/user/games/{game_id}/*` endpoint, после чего транскодирует
|
||||||
JSON-ответ обратно в FB перед подписью.
|
JSON-ответ обратно в FB перед подписью.
|
||||||
|
`user.games.order.get` — read-back-компаньон для `user.games.order`:
|
||||||
|
клиент использует его, чтобы восстановить локальный черновик приказа
|
||||||
|
после потери кэша (свежая установка, очищенное хранилище, новое
|
||||||
|
устройство).
|
||||||
|
|
||||||
Для каждого in-game-endpoint user-surface работает как
|
Для каждого in-game-endpoint user-surface работает как
|
||||||
авторизующий pass-through к engine-контейнеру. Backend:
|
авторизующий 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 downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command %q: %w", command.MessageType, err)
|
||||||
}
|
}
|
||||||
return c.executeUserGamesOrder(ctx, command.UserID, req)
|
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:
|
case reportmodel.MessageTypeUserGamesReport:
|
||||||
req, err := transcoder.PayloadToGameReportRequest(command.PayloadBytes)
|
req, err := transcoder.PayloadToGameReportRequest(command.PayloadBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -91,7 +97,22 @@ func (c *RESTClient) executeUserGamesOrder(ctx context.Context, userID string, r
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.order: %w", err)
|
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) {
|
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
|
return gamerest.Command{Actor: "", Commands: raw}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// projectUserGamesAckResponse turns a backend response for command /
|
// projectUserGamesAckResponse turns a backend response for the
|
||||||
// order routes into a UnaryResult. Engine returns 204 on success, so
|
// `user.games.command` route into a UnaryResult. Engine returns 204
|
||||||
// any 2xx status is treated as ok and answered with the empty typed
|
// on success, so any 2xx status is treated as ok and answered with
|
||||||
// FB envelope produced by ackBuilder.
|
// the empty typed FB envelope produced by ackBuilder.
|
||||||
func projectUserGamesAckResponse(statusCode int, payload []byte, ackBuilder func() []byte) (downstream.UnaryResult, error) {
|
func projectUserGamesAckResponse(statusCode int, payload []byte, ackBuilder func() []byte) (downstream.UnaryResult, error) {
|
||||||
switch {
|
switch {
|
||||||
case statusCode >= 200 && statusCode < 300:
|
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
|
// projectUserGamesReportResponse decodes the engine's Report JSON
|
||||||
// payload (forwarded verbatim by backend) and re-encodes it as a
|
// payload (forwarded verbatim by backend) and re-encodes it as a
|
||||||
// FlatBuffers Report for the signed-gRPC client.
|
// 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
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ func GameRoutes(client *RESTClient) map[string]downstream.Client {
|
|||||||
return map[string]downstream.Client{
|
return map[string]downstream.Client{
|
||||||
ordermodel.MessageTypeUserGamesCommand: target,
|
ordermodel.MessageTypeUserGamesCommand: target,
|
||||||
ordermodel.MessageTypeUserGamesOrder: target,
|
ordermodel.MessageTypeUserGamesOrder: target,
|
||||||
|
ordermodel.MessageTypeUserGamesOrderGet: target,
|
||||||
reportmodel.MessageTypeUserGamesReport: target,
|
reportmodel.MessageTypeUserGamesReport: target,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ const MessageTypeUserGamesCommand = "user.games.command"
|
|||||||
// FlatBuffers `order.UserGamesOrder`.
|
// FlatBuffers `order.UserGamesOrder`.
|
||||||
const MessageTypeUserGamesOrder = "user.games.order"
|
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.
|
// UserGamesCommand is the typed payload of MessageTypeUserGamesCommand.
|
||||||
// `GameID` selects the running engine container; `Commands` is the
|
// `GameID` selects the running engine container; `Commands` is the
|
||||||
// player command batch executed atomically by the engine. The `Actor`
|
// 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)
|
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) {
|
func AsCommand[E DecodableCommand](c DecodableCommand) (result E, ok bool) {
|
||||||
if v, ok := c.(E); ok {
|
if v, ok := c.(E); ok {
|
||||||
return v, true
|
return v, true
|
||||||
|
|||||||
@@ -2,8 +2,15 @@
|
|||||||
|
|
||||||
## Generating sources
|
## 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
|
```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.
|
||||||
|
|||||||
@@ -220,6 +220,31 @@ table UserGamesOrder {
|
|||||||
// — kept as a typed envelope for future extension.
|
// — kept as a typed envelope for future extension.
|
||||||
table UserGamesCommandResponse {}
|
table UserGamesCommandResponse {}
|
||||||
|
|
||||||
// UserGamesOrderResponse is the success acknowledgement returned for
|
// UserGamesOrderResponse mirrors the engine's `PUT /api/v1/order`
|
||||||
// `MessageTypeUserGamesOrder`. Mirrors `UserGamesCommandResponse`.
|
// success body: it echoes the stored order back to the caller with
|
||||||
table UserGamesOrderResponse {}
|
// 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ package order
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
flatbuffers "github.com/google/flatbuffers/go"
|
flatbuffers "github.com/google/flatbuffers/go"
|
||||||
|
|
||||||
|
common "galaxy/schema/fbs/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserGamesOrderResponse struct {
|
type UserGamesOrderResponse struct {
|
||||||
@@ -41,8 +43,65 @@ func (rcv *UserGamesOrderResponse) Table() flatbuffers.Table {
|
|||||||
return rcv._tab
|
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) {
|
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 {
|
func UserGamesOrderResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||||
return builder.EndObject()
|
return builder.EndObject()
|
||||||
|
|||||||
+321
-5
@@ -1,6 +1,7 @@
|
|||||||
package transcoder
|
package transcoder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@@ -9,8 +10,117 @@ import (
|
|||||||
fbs "galaxy/schema/fbs/order"
|
fbs "galaxy/schema/fbs/order"
|
||||||
|
|
||||||
flatbuffers "github.com/google/flatbuffers/go"
|
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 {
|
type encodedCommand struct {
|
||||||
cmdID string
|
cmdID string
|
||||||
cmdApplied *bool
|
cmdApplied *bool
|
||||||
@@ -955,14 +1065,220 @@ func EmptyUserGamesCommandResponsePayload() []byte {
|
|||||||
return builder.FinishedBytes()
|
return builder.FinishedBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmptyUserGamesOrderResponsePayload mirrors
|
// UserGamesOrderResponseToPayload encodes the engine's response body
|
||||||
// EmptyUserGamesCommandResponsePayload for `MessageTypeUserGamesOrder`.
|
// for `PUT /api/v1/order` into the wire FlatBuffers envelope expected
|
||||||
func EmptyUserGamesOrderResponsePayload() []byte {
|
// for `MessageTypeUserGamesOrder`. The engine populates per-command
|
||||||
builder := flatbuffers.NewBuilder(16)
|
// `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)
|
fbs.UserGamesOrderResponseStart(builder)
|
||||||
offset := fbs.UserGamesOrderResponseEnd(builder)
|
offset := fbs.UserGamesOrderResponseEnd(builder)
|
||||||
fbs.FinishUserGamesOrderResponseBuffer(builder, offset)
|
fbs.FinishUserGamesOrderResponseBuffer(builder, offset)
|
||||||
return builder.FinishedBytes()
|
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(), 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
|
// encodeCommandItemVector serialises a slice of DecodableCommand into a
|
||||||
|
|||||||
@@ -77,6 +77,160 @@ func TestUserGamesCommandRejectsNilAndEmpty(t *testing.T) {
|
|||||||
if _, err := PayloadToUserGamesOrder(nil); err == nil {
|
if _, err := PayloadToUserGamesOrder(nil); err == nil {
|
||||||
t.Fatalf("expected error decoding empty user games order")
|
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) {
|
func TestInt64ToInt(t *testing.T) {
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ WASM_OUT := frontend/static/core.wasm
|
|||||||
WASM_EXEC := frontend/static/wasm_exec.js
|
WASM_EXEC := frontend/static/wasm_exec.js
|
||||||
TINYGO_ROOT := $(shell tinygo env TINYGOROOT 2>/dev/null)
|
TINYGO_ROOT := $(shell tinygo env TINYGOROOT 2>/dev/null)
|
||||||
FBS_OUT := frontend/src/proto/galaxy/fbs
|
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:
|
help:
|
||||||
@echo "ui targets:"
|
@echo "ui targets:"
|
||||||
|
|||||||
+139
-24
@@ -1522,27 +1522,138 @@ Targeted tests:
|
|||||||
inspector content, and on `chromium-mobile-iphone-13` asserts the
|
inspector content, and on `chromium-mobile-iphone-13` asserts the
|
||||||
bottom-sheet appears and the close button clears it.
|
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 →
|
Goal: prove the entire pipeline (inspector → composer → submit →
|
||||||
server → state refresh) by wiring up exactly one action: renaming a
|
server → state refresh) by wiring up exactly one action: renaming a
|
||||||
planet.
|
planet.
|
||||||
|
|
||||||
Artifacts:
|
Decisions taken with the project owner during implementation:
|
||||||
|
|
||||||
- `ui/frontend/src/lib/inspectors/planet.svelte` adds a `Rename` action
|
1. **Optimistic overlay over `user.games.order`.** The plan's
|
||||||
that opens a small inline editor and adds a `RenamePlanet` command
|
acceptance criterion ("name change within one second") is
|
||||||
to the order draft on confirm
|
inconsistent with the engine's order endpoint, which only
|
||||||
- `ui/frontend/src/sync/submit.ts` `submitOrder()` function that POSTs
|
validates and stores; rename takes effect at turn cutoff.
|
||||||
the entire draft via `GalaxyClient.execute('user.games.order', ...)`
|
Phase 14 keeps `user.games.order` for the wire path and adds a
|
||||||
and applies per-command results
|
pure projection `applyOrderOverlay(report, commands, statuses)`
|
||||||
- `ui/frontend/src/lib/sidebar/order-tab.svelte` adds a `Submit order`
|
in `api/game-state.ts`. Inspector, mobile sheet, and map
|
||||||
button calling `submitOrder()` and renders accepted / rejected
|
renderer read a derived `renderedReport` (context key
|
||||||
status per command after submit
|
`RENDERED_REPORT_CONTEXT_KEY`) that swaps planet names in for
|
||||||
- on successful submit, refresh game state so the rename appears on the
|
every applied or in-flight rename. Raw `gameState.report`
|
||||||
map and in the inspector
|
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.
|
Dependencies: Phases 12, 13.
|
||||||
|
|
||||||
@@ -1550,19 +1661,23 @@ Acceptance criteria:
|
|||||||
|
|
||||||
- the user can select a planet, click `Rename`, type a new name, see
|
- 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
|
the command appear in the order tab, click `Submit`, and observe the
|
||||||
planet's name change everywhere within one second;
|
planet's name change everywhere within one second (overlay applies
|
||||||
- attempting an empty or invalid name is blocked locally (button
|
immediately on the inspector / mobile sheet / map; server-side state
|
||||||
disabled with tooltip);
|
catches up at turn cutoff);
|
||||||
- a server-side rejection (race condition) is surfaced as `rejected`
|
- attempting an empty or invalid name is blocked locally (Submit
|
||||||
status in the order tab.
|
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:
|
Targeted tests:
|
||||||
|
|
||||||
- Vitest unit tests for `submitOrder` with mocked `GalaxyClient`;
|
- Vitest unit tests for `submitOrder`, `fetchOrder`,
|
||||||
- Vitest component test for the inline rename editor including
|
`applyOrderOverlay`, `validateEntityName`, and the extended
|
||||||
validation;
|
`OrderDraftStore`.
|
||||||
- Playwright e2e: rename a seeded planet, reload, confirm the new name
|
- Vitest component tests for the inline rename editor and the
|
||||||
persists.
|
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
|
## Phase 15. Inspector — Planet Production Controls
|
||||||
|
|
||||||
|
|||||||
+144
-25
@@ -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
|
remote-first composer that reflects the gateway's pending-orders
|
||||||
queue would force a sync on every keystroke.
|
queue would force a sync on every keystroke.
|
||||||
|
|
||||||
When the submit pipeline lands (Phase 25), it iterates the draft
|
Phase 14 lands the submit pipeline with batch semantics: every
|
||||||
in order, sending one `command` per line. The gateway's per-line
|
entry the user has marked `valid` is collected into one signed
|
||||||
result rejoins the draft entry through `cmdId`, and the entry's
|
`user.games.order` request. The engine validates and stores the
|
||||||
`CommandStatus` flips to `applied` or `rejected`. Successfully
|
order, returning per-command `cmdApplied` / `cmdErrorCode` in the
|
||||||
applied entries stay visible until the next turn cutoff so the
|
response body. The gateway re-encodes that JSON into the FBS
|
||||||
player can see what was committed; rejected entries stay until the
|
`UserGamesOrderResponse` envelope (with `commands: [CommandItem]`
|
||||||
player edits or removes them.
|
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
|
## 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
|
entries. The validation step is per-command and pure — it consults
|
||||||
the current `GameStateStore` snapshot only, never the network.
|
the current `GameStateStore` snapshot only, never the network.
|
||||||
|
|
||||||
Phase 12 ships the skeleton without any concrete validators: the
|
Phase 14's `planetRename` is the first variant that exercises the
|
||||||
single `placeholder` variant is content-free and stays at `draft`
|
`draft → valid | invalid` transition. The validator
|
||||||
forever. Phase 14's `planetRename` is the first variant that
|
(`lib/util/entity-name.ts`) is a TS port of
|
||||||
exercises the `draft → valid | invalid` transition.
|
`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
|
## Command status state machine
|
||||||
|
|
||||||
@@ -65,14 +77,25 @@ Transitions:
|
|||||||
- **`submitting → applied` / `submitting → rejected`**: the gateway
|
- **`submitting → applied` / `submitting → rejected`**: the gateway
|
||||||
responded; the entry is no longer in flight.
|
responded; the entry is no longer in flight.
|
||||||
|
|
||||||
Phase 12 stores the type but does not implement any transitions.
|
Phase 14 lands the local validators (`draft → valid | invalid`),
|
||||||
Every entry remains at `draft` until later phases land the
|
the submit pipeline (`valid → submitting → applied | rejected`),
|
||||||
validators (Phase 14) and the submit pipeline (Phase 25).
|
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
|
## Discriminated union shape
|
||||||
|
|
||||||
`OrderCommand` is a discriminated union on the `kind` field. Phase
|
`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
|
```ts
|
||||||
interface PlaceholderCommand {
|
interface PlaceholderCommand {
|
||||||
@@ -80,15 +103,25 @@ interface PlaceholderCommand {
|
|||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly label: 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
|
The `id` field is the canonical identifier the store uses for
|
||||||
remove and reorder; later variants must keep `id: string` so the
|
remove and reorder; later variants must keep `id: string` so the
|
||||||
store API stays uniform. The whole draft round-trips through
|
store API stays uniform. The whole draft round-trips through
|
||||||
IndexedDB structured clone, so every variant must use only
|
IndexedDB structured clone, so every variant must use only
|
||||||
JSON-friendly value types. Phase 14 adds the first real variant
|
JSON-friendly value types. Phase 14 lands `planetRename` together
|
||||||
(`planetRename`) and updates this list.
|
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
|
## Store
|
||||||
|
|
||||||
@@ -124,6 +157,70 @@ The order tab consumes the store via
|
|||||||
`getContext(ORDER_DRAFT_CONTEXT_KEY)`; Phase 14's planet inspector
|
`getContext(ORDER_DRAFT_CONTEXT_KEY)`; Phase 14's planet inspector
|
||||||
will use the same key to push a new command.
|
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
|
## Persistence
|
||||||
|
|
||||||
Cache row layout:
|
Cache row layout:
|
||||||
@@ -168,19 +265,41 @@ its own test suite.
|
|||||||
|
|
||||||
## Testing
|
## 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)
|
- [`../frontend/tests/order-draft.test.ts`](../frontend/tests/order-draft.test.ts)
|
||||||
— Vitest unit tests for the store. Drives `OrderDraftStore`
|
— Vitest unit tests for the store. Drives `OrderDraftStore`
|
||||||
directly with `IDBCache` over `fake-indexeddb`. Covers init,
|
directly with `IDBCache` over `fake-indexeddb`. Covers init,
|
||||||
add, remove, move, per-game isolation, mutations-before-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)
|
- [`../frontend/tests/e2e/order-composer.spec.ts`](../frontend/tests/e2e/order-composer.spec.ts)
|
||||||
— Playwright spec. Seeds three commands through
|
— Playwright spec for the Phase 12 skeleton (seed three
|
||||||
`__galaxyDebug.seedOrderDraft`, navigates into
|
commands, reload, persistence).
|
||||||
`/games/<id>/map`, opens the Order tool (sidebar tab on
|
- [`../frontend/tests/e2e/rename-planet.spec.ts`](../frontend/tests/e2e/rename-planet.spec.ts)
|
||||||
desktop, bottom tab on mobile), asserts the rows, reloads, and
|
— Phase 14 end-to-end: select a planet, rename, submit, observe
|
||||||
asserts the rows again.
|
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
|
The `__galaxyDebug.seedOrderDraft(gameId, commands)` and
|
||||||
`__galaxyDebug.clearOrderDraft(gameId)` helpers in
|
`__galaxyDebug.clearOrderDraft(gameId)` helpers in
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
// sets for `LocalPlanet`, `OtherPlanet`, `UninhabitedPlanet`, and
|
// sets for `LocalPlanet`, `OtherPlanet`, `UninhabitedPlanet`, and
|
||||||
// `UnidentifiedPlanet`, and the wrapper preserves that nullability
|
// `UnidentifiedPlanet`, and the wrapper preserves that nullability
|
||||||
// instead of inventing zero values.
|
// 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";
|
import { Builder, ByteBuffer } from "flatbuffers";
|
||||||
|
|
||||||
@@ -19,6 +24,7 @@ import {
|
|||||||
GameReportRequest,
|
GameReportRequest,
|
||||||
Report,
|
Report,
|
||||||
} from "../proto/galaxy/fbs/report";
|
} from "../proto/galaxy/fbs/report";
|
||||||
|
import type { CommandStatus, OrderCommand } from "../sync/order-types";
|
||||||
|
|
||||||
const MESSAGE_TYPE = "user.games.report";
|
const MESSAGE_TYPE = "user.games.report";
|
||||||
|
|
||||||
@@ -205,6 +211,42 @@ export function uuidToHiLo(value: string): [bigint, bigint] {
|
|||||||
return [hi, lo];
|
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 } {
|
function decodeErrorMessage(payload: Uint8Array): { code: string; message: string } {
|
||||||
if (payload.length === 0) {
|
if (payload.length === 0) {
|
||||||
return { code: "internal_error", message: "empty error payload" };
|
return { code: "internal_error", message: "empty error payload" };
|
||||||
|
|||||||
@@ -36,8 +36,15 @@ preference the store already manages.
|
|||||||
SELECTION_CONTEXT_KEY,
|
SELECTION_CONTEXT_KEY,
|
||||||
type SelectionStore,
|
type SelectionStore,
|
||||||
} from "$lib/selection.svelte";
|
} 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 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);
|
const selection = getContext<SelectionStore | undefined>(SELECTION_CONTEXT_KEY);
|
||||||
|
|
||||||
let canvasEl: HTMLCanvasElement | null = $state(null);
|
let canvasEl: HTMLCanvasElement | null = $state(null);
|
||||||
@@ -52,7 +59,11 @@ preference the store already manages.
|
|||||||
let mounted = false;
|
let mounted = false;
|
||||||
|
|
||||||
$effect(() => {
|
$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";
|
const status = store?.status ?? "idle";
|
||||||
// Track the wrap mode so the renderer remounts when Phase 29's
|
// Track the wrap mode so the renderer remounts when Phase 29's
|
||||||
// toggle UI flips it; the read here also subscribes the effect.
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,10 +41,17 @@ export class GameStateStore {
|
|||||||
report: GameReport | null = $state(null);
|
report: GameReport | null = $state(null);
|
||||||
wrapMode: WrapMode = $state("torus");
|
wrapMode: WrapMode = $state("torus");
|
||||||
error: string | null = $state(null);
|
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 client: GalaxyClient | null = null;
|
||||||
private cache: Cache | null = null;
|
private cache: Cache | null = null;
|
||||||
private currentTurn = 0;
|
|
||||||
private destroyed = false;
|
private destroyed = false;
|
||||||
private visibilityListener: (() => void) | null = null;
|
private visibilityListener: (() => void) | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,17 @@ const en = {
|
|||||||
"game.sidebar.empty.inspector": "select an object on the map",
|
"game.sidebar.empty.inspector": "select an object on the map",
|
||||||
"game.sidebar.empty.order": "order is empty",
|
"game.sidebar.empty.order": "order is empty",
|
||||||
"game.sidebar.order.command_delete": "delete",
|
"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.map": "map",
|
||||||
"game.bottom_tabs.calc": "calc",
|
"game.bottom_tabs.calc": "calc",
|
||||||
"game.bottom_tabs.order": "order",
|
"game.bottom_tabs.order": "order",
|
||||||
@@ -144,6 +155,17 @@ const en = {
|
|||||||
"game.inspector.planet.production_none": "none",
|
"game.inspector.planet.production_none": "none",
|
||||||
"game.inspector.planet.unidentified_no_data": "no data — only the location is known",
|
"game.inspector.planet.unidentified_no_data": "no data — only the location is known",
|
||||||
"game.inspector.sheet_close": "close",
|
"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;
|
} as const;
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
|||||||
@@ -121,6 +121,17 @@ const ru: Record<keyof typeof en, string> = {
|
|||||||
"game.sidebar.empty.inspector": "выберите объект на карте",
|
"game.sidebar.empty.inspector": "выберите объект на карте",
|
||||||
"game.sidebar.empty.order": "приказ пуст",
|
"game.sidebar.empty.order": "приказ пуст",
|
||||||
"game.sidebar.order.command_delete": "удалить",
|
"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.map": "карта",
|
||||||
"game.bottom_tabs.calc": "калк",
|
"game.bottom_tabs.calc": "калк",
|
||||||
"game.bottom_tabs.order": "приказ",
|
"game.bottom_tabs.order": "приказ",
|
||||||
@@ -145,6 +156,17 @@ const ru: Record<keyof typeof en, string> = {
|
|||||||
"game.inspector.planet.production_none": "не задано",
|
"game.inspector.planet.production_none": "не задано",
|
||||||
"game.inspector.planet.unidentified_no_data": "нет данных — известно только местоположение",
|
"game.inspector.planet.unidentified_no_data": "нет данных — известно только местоположение",
|
||||||
"game.inspector.sheet_close": "закрыть",
|
"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;
|
export default ru;
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
<!--
|
<!--
|
||||||
Phase 13 read-only planet inspector. Renders the documented field
|
Planet inspector. Renders the documented field set for each planet
|
||||||
set for the planet kind in question:
|
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),
|
The read-only path stays unchanged for non-`local` planets. The
|
||||||
coordinates, size, population, colonists, industry, both stockpiles,
|
inline editor lives directly inside this component per PLAN.md
|
||||||
natural resources, current production, free production potential.
|
Phase 14 — a separate file would be over-abstraction for one input
|
||||||
- `uninhabited` keeps name, coordinates, size, both stockpiles, and
|
field with five buttons.
|
||||||
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.
|
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getContext, tick } from "svelte";
|
||||||
import type { ReportPlanet } from "../../api/game-state";
|
import type { ReportPlanet } from "../../api/game-state";
|
||||||
import { i18n, type TranslationKey } 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 {
|
||||||
|
validateEntityName,
|
||||||
|
type EntityNameInvalidReason,
|
||||||
|
} from "$lib/util/entity-name";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
planet: ReportPlanet;
|
planet: ReportPlanet;
|
||||||
@@ -31,6 +37,34 @@ lookups happen here. Phase 14 will extend the same component with a
|
|||||||
unidentified: "game.inspector.planet.kind.unidentified",
|
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 kindLabel = $derived(i18n.t(kindKeyMap[planet.kind]));
|
||||||
const coordinates = $derived(
|
const coordinates = $derived(
|
||||||
`(${formatNumber(planet.x)}, ${formatNumber(planet.y)})`,
|
`(${formatNumber(planet.x)}, ${formatNumber(planet.y)})`,
|
||||||
@@ -47,6 +81,44 @@ lookups happen here. Phase 14 will extend the same component with a
|
|||||||
}
|
}
|
||||||
return value;
|
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>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -60,8 +132,65 @@ lookups happen here. Phase 14 will extend the same component with a
|
|||||||
{#if planet.kind !== "unidentified"}
|
{#if planet.kind !== "unidentified"}
|
||||||
<h3 class="name" data-testid="inspector-planet-name">{planet.name}</h3>
|
<h3 class="name" data-testid="inspector-planet-name">{planet.name}</h3>
|
||||||
{/if}
|
{/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>
|
</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">
|
<dl class="fields">
|
||||||
{#if planet.kind === "other" && planet.owner !== null}
|
{#if planet.kind === "other" && planet.owner !== null}
|
||||||
<div class="field" data-testid="inspector-planet-field-owner">
|
<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;
|
color: #888;
|
||||||
font-size: 0.85rem;
|
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>
|
</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">
|
<script lang="ts">
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import { i18n } from "$lib/i18n/index.svelte";
|
import { i18n } from "$lib/i18n/index.svelte";
|
||||||
import {
|
|
||||||
GAME_STATE_CONTEXT_KEY,
|
|
||||||
type GameStateStore,
|
|
||||||
} from "$lib/game-state.svelte";
|
|
||||||
import {
|
import {
|
||||||
SELECTION_CONTEXT_KEY,
|
SELECTION_CONTEXT_KEY,
|
||||||
type SelectionStore,
|
type SelectionStore,
|
||||||
} from "$lib/selection.svelte";
|
} from "$lib/selection.svelte";
|
||||||
|
import {
|
||||||
|
RENDERED_REPORT_CONTEXT_KEY,
|
||||||
|
type RenderedReportSource,
|
||||||
|
} from "$lib/rendered-report.svelte";
|
||||||
import Planet from "$lib/inspectors/planet.svelte";
|
import Planet from "$lib/inspectors/planet.svelte";
|
||||||
|
|
||||||
const gameState = getContext<GameStateStore | undefined>(
|
const renderedReport = getContext<RenderedReportSource | undefined>(
|
||||||
GAME_STATE_CONTEXT_KEY,
|
RENDERED_REPORT_CONTEXT_KEY,
|
||||||
);
|
);
|
||||||
const selection = getContext<SelectionStore | undefined>(
|
const selection = getContext<SelectionStore | undefined>(
|
||||||
SELECTION_CONTEXT_KEY,
|
SELECTION_CONTEXT_KEY,
|
||||||
@@ -34,7 +34,7 @@ from the Phase 10 stub.
|
|||||||
const selectedPlanet = $derived.by(() => {
|
const selectedPlanet = $derived.by(() => {
|
||||||
const sel = selection?.selected;
|
const sel = selection?.selected;
|
||||||
if (sel === undefined || sel === null || sel.kind !== "planet") return null;
|
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;
|
if (report === undefined || report === null) return null;
|
||||||
return report.planets.find((p) => p.number === sel.id) ?? null;
|
return report.planets.find((p) => p.number === sel.id) ?? null;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,31 +1,143 @@
|
|||||||
<!--
|
<!--
|
||||||
Order composer tool. Resolves the per-game `OrderDraftStore` from
|
Order composer tool. Resolves the per-game `OrderDraftStore`,
|
||||||
context (set by `routes/games/[id]/+layout.svelte`) and renders the
|
`GameStateStore`, and `GalaxyClient` from context (all set by
|
||||||
draft as a vertical, top-to-bottom command list. Empty state shows
|
`routes/games/[id]/+layout.svelte`) and renders the local draft as
|
||||||
the i18n empty-state copy; non-empty state shows an ordered list of
|
a vertical list with per-row status, a delete button, and a Submit
|
||||||
rows, each with a stable `data-testid` plus a per-row delete button.
|
button at the bottom.
|
||||||
|
|
||||||
Phase 12 has no UI for adding commands — Phase 14 lands the first
|
Phase 14 wires the first end-to-end command: clicking Submit calls
|
||||||
end-to-end command (`planetRename`) and the inspector affordance
|
`submitOrder` for every entry in `valid` status, flips the in-flight
|
||||||
that pushes it into the draft. Tests exercise the skeleton through
|
rows to `submitting`, then merges the per-command verdict back into
|
||||||
`__galaxyDebug.seedOrderDraft` (Playwright) and via direct store
|
the draft once the gateway responds. The optimistic overlay in
|
||||||
construction (Vitest).
|
`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">
|
<script lang="ts">
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import { i18n } from "$lib/i18n/index.svelte";
|
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||||
import {
|
import {
|
||||||
ORDER_DRAFT_CONTEXT_KEY,
|
ORDER_DRAFT_CONTEXT_KEY,
|
||||||
OrderDraftStore,
|
OrderDraftStore,
|
||||||
} from "../../sync/order-draft.svelte";
|
} 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>(
|
const draft = getContext<OrderDraftStore | undefined>(
|
||||||
ORDER_DRAFT_CONTEXT_KEY,
|
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 {
|
const statusKeyMap: Record<CommandStatus, TranslationKey> = {
|
||||||
if (cmd.kind === "placeholder") return cmd.label ?? cmd.kind;
|
draft: "game.sidebar.order.status.draft",
|
||||||
return cmd.kind;
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -38,11 +150,22 @@ construction (Vitest).
|
|||||||
{:else}
|
{:else}
|
||||||
<ol class="commands" data-testid="order-list">
|
<ol class="commands" data-testid="order-list">
|
||||||
{#each draft.commands as cmd, index (cmd.id)}
|
{#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="index" aria-hidden="true">{index + 1}.</span>
|
||||||
<span class="label" data-testid="order-command-label-{index}">
|
<span class="label" data-testid="order-command-label-{index}">
|
||||||
{describe(cmd)}
|
{describe(cmd)}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
class="status status-{status}"
|
||||||
|
data-testid="order-command-status-{index}"
|
||||||
|
>
|
||||||
|
{i18n.t(statusKeyMap[status])}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="delete"
|
class="delete"
|
||||||
@@ -54,6 +177,20 @@ construction (Vitest).
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ol>
|
</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}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -72,14 +209,15 @@ construction (Vitest).
|
|||||||
}
|
}
|
||||||
.commands {
|
.commands {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0 0 0.75rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
.command {
|
.command {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.4rem 0.5rem;
|
padding: 0.4rem 0.5rem;
|
||||||
@@ -88,17 +226,40 @@ construction (Vitest).
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.index {
|
.index {
|
||||||
min-width: 1.5rem;
|
|
||||||
color: #aab;
|
color: #aab;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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 {
|
.delete {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -113,4 +274,26 @@ construction (Vitest).
|
|||||||
color: #e8eaf6;
|
color: #e8eaf6;
|
||||||
border-color: #6d8cff;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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,
|
SelectionStore,
|
||||||
SELECTION_CONTEXT_KEY,
|
SELECTION_CONTEXT_KEY,
|
||||||
} from "$lib/selection.svelte";
|
} from "$lib/selection.svelte";
|
||||||
|
import {
|
||||||
|
createRenderedReportSource,
|
||||||
|
RENDERED_REPORT_CONTEXT_KEY,
|
||||||
|
} from "$lib/rendered-report.svelte";
|
||||||
import {
|
import {
|
||||||
ORDER_DRAFT_CONTEXT_KEY,
|
ORDER_DRAFT_CONTEXT_KEY,
|
||||||
OrderDraftStore,
|
OrderDraftStore,
|
||||||
} from "../../../sync/order-draft.svelte";
|
} 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 { session } from "$lib/session-store.svelte";
|
||||||
import { loadStore } from "../../../platform/store/index";
|
import { loadStore } from "../../../platform/store/index";
|
||||||
import { loadCore } from "../../../platform/core/index";
|
import { loadCore } from "../../../platform/core/index";
|
||||||
@@ -89,17 +97,23 @@ fresh.
|
|||||||
setContext(ORDER_DRAFT_CONTEXT_KEY, orderDraft);
|
setContext(ORDER_DRAFT_CONTEXT_KEY, orderDraft);
|
||||||
const selection = new SelectionStore();
|
const selection = new SelectionStore();
|
||||||
setContext(SELECTION_CONTEXT_KEY, selection);
|
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
|
// selectedPlanet resolves the current selection against the live
|
||||||
// report so both the desktop sidebar and the mobile sheet display
|
// report so both the desktop sidebar and the mobile sheet display
|
||||||
// the same snapshot. A selection that points at a planet missing
|
// the same snapshot. A selection that points at a planet missing
|
||||||
// from the current report (e.g. visibility lost between turns)
|
// from the current report (e.g. visibility lost between turns)
|
||||||
// reads as `null` here, which collapses the inspector and the
|
// 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 selectedPlanet = $derived.by(() => {
|
||||||
const sel = selection.selected;
|
const sel = selection.selected;
|
||||||
if (sel === null || sel.kind !== "planet") return null;
|
if (sel === null || sel.kind !== "planet") return null;
|
||||||
const report = gameState.report;
|
const report = renderedReport.report;
|
||||||
if (report === null) return null;
|
if (report === null) return null;
|
||||||
return report.planets.find((p) => p.number === sel.id) ?? null;
|
return report.planets.find((p) => p.number === sel.id) ?? null;
|
||||||
});
|
});
|
||||||
@@ -149,6 +163,13 @@ fresh.
|
|||||||
gameState.init({ client, cache, gameId }),
|
gameState.init({ client, cache, gameId }),
|
||||||
orderDraft.init({ cache, gameId }),
|
orderDraft.init({ cache, gameId }),
|
||||||
]);
|
]);
|
||||||
|
galaxyClient.set(client);
|
||||||
|
if (orderDraft.needsServerHydration) {
|
||||||
|
await orderDraft.hydrateFromServer({
|
||||||
|
client,
|
||||||
|
turn: gameState.currentTurn,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
gameState.failBootstrap(describeBootstrapError(err));
|
gameState.failBootstrap(describeBootstrapError(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
// any UI.
|
// any UI.
|
||||||
|
|
||||||
import type { Cache } from "../platform/store/index";
|
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 NAMESPACE = "order-drafts";
|
||||||
const draftKey = (gameId: string): string => `${gameId}/draft`;
|
const draftKey = (gameId: string): string => `${gameId}/draft`;
|
||||||
@@ -34,9 +37,21 @@ type Status = "idle" | "ready" | "error";
|
|||||||
|
|
||||||
export class OrderDraftStore {
|
export class OrderDraftStore {
|
||||||
commands: OrderCommand[] = $state([]);
|
commands: OrderCommand[] = $state([]);
|
||||||
|
statuses: Record<string, CommandStatus> = $state({});
|
||||||
|
updatedAt = $state(0);
|
||||||
status: Status = $state("idle");
|
status: Status = $state("idle");
|
||||||
error: string | null = $state(null);
|
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 cache: Cache | null = null;
|
||||||
private gameId = "";
|
private gameId = "";
|
||||||
private destroyed = false;
|
private destroyed = false;
|
||||||
@@ -47,6 +62,12 @@ export class OrderDraftStore {
|
|||||||
* idempotent on the same store instance — the layout always
|
* idempotent on the same store instance — the layout always
|
||||||
* constructs a fresh store per game, so there is no need to support
|
* constructs a fresh store per game, so there is no need to support
|
||||||
* mid-life game switching here.
|
* 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> {
|
async init(opts: { cache: Cache; gameId: string }): Promise<void> {
|
||||||
this.cache = opts.cache;
|
this.cache = opts.cache;
|
||||||
@@ -57,7 +78,14 @@ export class OrderDraftStore {
|
|||||||
draftKey(opts.gameId),
|
draftKey(opts.gameId),
|
||||||
);
|
);
|
||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
|
if (stored === undefined) {
|
||||||
|
this.commands = [];
|
||||||
|
this.needsServerHydration = true;
|
||||||
|
} else {
|
||||||
this.commands = Array.isArray(stored) ? [...stored] : [];
|
this.commands = Array.isArray(stored) ? [...stored] : [];
|
||||||
|
this.needsServerHydration = false;
|
||||||
|
}
|
||||||
|
this.recomputeStatuses();
|
||||||
this.status = "ready";
|
this.status = "ready";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
@@ -67,13 +95,44 @@ export class OrderDraftStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add appends a command to the end of the draft and persists the
|
* hydrateFromServer fetches the player's stored order from the
|
||||||
* updated list. Mutations made before `init` resolves are ignored —
|
* gateway when the cache row was absent at boot. The result is
|
||||||
* the layout always awaits `init` before exposing the store.
|
* 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> {
|
async add(command: OrderCommand): Promise<void> {
|
||||||
if (this.status !== "ready") return;
|
if (this.status !== "ready") return;
|
||||||
this.commands = [...this.commands, command];
|
this.commands = [...this.commands, command];
|
||||||
|
this.statuses = { ...this.statuses, [command.id]: validateCommand(command) };
|
||||||
await this.persist();
|
await this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +145,9 @@ export class OrderDraftStore {
|
|||||||
const next = this.commands.filter((cmd) => cmd.id !== id);
|
const next = this.commands.filter((cmd) => cmd.id !== id);
|
||||||
if (next.length === this.commands.length) return;
|
if (next.length === this.commands.length) return;
|
||||||
this.commands = next;
|
this.commands = next;
|
||||||
|
const nextStatuses = { ...this.statuses };
|
||||||
|
delete nextStatuses[id];
|
||||||
|
this.statuses = nextStatuses;
|
||||||
await this.persist();
|
await this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,11 +171,83 @@ export class OrderDraftStore {
|
|||||||
await this.persist();
|
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 {
|
dispose(): void {
|
||||||
this.destroyed = true;
|
this.destroyed = true;
|
||||||
this.cache = null;
|
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> {
|
private async persist(): Promise<void> {
|
||||||
if (this.cache === null || this.destroyed) return;
|
if (this.cache === null || this.destroyed) return;
|
||||||
// `commands` is `$state`, so individual entries are proxies.
|
// `commands` is `$state`, so individual entries are proxies.
|
||||||
@@ -123,3 +257,14 @@ export class OrderDraftStore {
|
|||||||
await this.cache.put(NAMESPACE, draftKey(this.gameId), snapshot);
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,13 +25,28 @@ export interface PlaceholderCommand {
|
|||||||
readonly label: string;
|
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
|
* OrderCommand is the discriminated union of every command shape the
|
||||||
* local order draft can hold. The `kind` field is the discriminator;
|
* local order draft can hold. The `kind` field is the discriminator;
|
||||||
* narrowing on it enables exhaustive `switch` statements at every
|
* 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
|
* CommandStatus is the lifecycle of a single command from the moment
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -23,6 +23,14 @@ import {
|
|||||||
SELECTION_CONTEXT_KEY,
|
SELECTION_CONTEXT_KEY,
|
||||||
SelectionStore,
|
SelectionStore,
|
||||||
} from "../src/lib/selection.svelte";
|
} 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";
|
import type { GameReport, ReportPlanet } from "../src/api/game-state";
|
||||||
|
|
||||||
const pageMock = vi.hoisted(() => ({
|
const pageMock = vi.hoisted(() => ({
|
||||||
@@ -70,17 +78,22 @@ function makeReport(planets: ReportPlanet[]): GameReport {
|
|||||||
function withStores(report: GameReport | null): {
|
function withStores(report: GameReport | null): {
|
||||||
gameState: GameStateStore;
|
gameState: GameStateStore;
|
||||||
selection: SelectionStore;
|
selection: SelectionStore;
|
||||||
|
orderDraft: OrderDraftStore;
|
||||||
context: Map<unknown, unknown>;
|
context: Map<unknown, unknown>;
|
||||||
} {
|
} {
|
||||||
const gameState = new GameStateStore();
|
const gameState = new GameStateStore();
|
||||||
gameState.report = report;
|
gameState.report = report;
|
||||||
gameState.status = report === null ? "idle" : "ready";
|
gameState.status = report === null ? "idle" : "ready";
|
||||||
const selection = new SelectionStore();
|
const selection = new SelectionStore();
|
||||||
|
const orderDraft = new OrderDraftStore();
|
||||||
|
const renderedReport = createRenderedReportSource(gameState, orderDraft);
|
||||||
const context = new Map<unknown, unknown>([
|
const context = new Map<unknown, unknown>([
|
||||||
[GAME_STATE_CONTEXT_KEY, gameState],
|
[GAME_STATE_CONTEXT_KEY, gameState],
|
||||||
[SELECTION_CONTEXT_KEY, selection],
|
[SELECTION_CONTEXT_KEY, selection],
|
||||||
|
[ORDER_DRAFT_CONTEXT_KEY, orderDraft],
|
||||||
|
[RENDERED_REPORT_CONTEXT_KEY, renderedReport],
|
||||||
]);
|
]);
|
||||||
return { gameState, selection, context };
|
return { gameState, selection, orderDraft, context };
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -5,12 +5,19 @@
|
|||||||
// drive it with synthetic `ReportPlanet` literals — no store.
|
// drive it with synthetic `ReportPlanet` literals — no store.
|
||||||
|
|
||||||
import "@testing-library/jest-dom/vitest";
|
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 { beforeEach, describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import { i18n } from "../src/lib/i18n/index.svelte";
|
import { i18n } from "../src/lib/i18n/index.svelte";
|
||||||
import type { ReportPlanet } from "../src/api/game-state";
|
import type { ReportPlanet } from "../src/api/game-state";
|
||||||
import Planet from "../src/lib/inspectors/planet.svelte";
|
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(() => {
|
beforeEach(() => {
|
||||||
i18n.resetForTests("en");
|
i18n.resetForTests("en");
|
||||||
@@ -192,6 +199,121 @@ describe("planet inspector", () => {
|
|||||||
expect(ui.queryByTestId("inspector-planet-field-natural_resources")).toBeNull();
|
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", () => {
|
test("missing production string falls back to the localised placeholder", () => {
|
||||||
const ui = render(Planet, {
|
const ui = render(Planet, {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -175,4 +175,158 @@ describe("OrderDraftStore", () => {
|
|||||||
expect(reload.commands.map((c) => c.id)).toEqual(["c1"]);
|
expect(reload.commands.map((c) => c.id)).toEqual(["c1"]);
|
||||||
reload.dispose();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user