ui/phase-14: rename planet end-to-end + order read-back
Wires the first end-to-end command through the full pipeline:
inspector rename action → local order draft → user.games.order
submit → optimistic overlay on map / inspector → server hydration
on cache miss via the new user.games.order.get message type.
Backend: GET /api/v1/user/games/{id}/orders forwards to engine
GET /api/v1/order. Gateway parses the engine PUT response into the
extended UserGamesOrderResponse FBS envelope and adds
executeUserGamesOrderGet for the read-back path. Frontend ports
ValidateTypeName to TS, lands the inline rename editor + Submit
button, and exposes a renderedReport context so consumers see the
overlay-applied snapshot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,12 @@ const MessageTypeUserGamesCommand = "user.games.command"
|
||||
// FlatBuffers `order.UserGamesOrder`.
|
||||
const MessageTypeUserGamesOrder = "user.games.order"
|
||||
|
||||
// MessageTypeUserGamesOrderGet is the authenticated gateway message
|
||||
// type used to read back the player's stored order for a given turn
|
||||
// through `GET /api/v1/user/games/{game_id}/orders?turn=N`. The
|
||||
// signed payload is a FlatBuffers `order.UserGamesOrderGet`.
|
||||
const MessageTypeUserGamesOrderGet = "user.games.order.get"
|
||||
|
||||
// UserGamesCommand is the typed payload of MessageTypeUserGamesCommand.
|
||||
// `GameID` selects the running engine container; `Commands` is the
|
||||
// player command batch executed atomically by the engine. The `Actor`
|
||||
@@ -54,6 +60,21 @@ func (o *UserGamesOrder) UnmarshalBinary(data []byte) error {
|
||||
return json.Unmarshal(data, o)
|
||||
}
|
||||
|
||||
// UserGamesOrderGet is the typed payload of
|
||||
// MessageTypeUserGamesOrderGet. `Turn` is mandatory and must be
|
||||
// non-negative; the caller pulls it from the lobby record at game
|
||||
// boot. Backend rebinds the player from the runtime player mapping
|
||||
// before forwarding to the engine.
|
||||
type UserGamesOrderGet struct {
|
||||
// GameID identifies the running game whose order is being
|
||||
// read back.
|
||||
GameID uuid.UUID `json:"game_id"`
|
||||
|
||||
// Turn selects the turn the stored order belongs to. Negative
|
||||
// values are invalid.
|
||||
Turn int `json:"turn"`
|
||||
}
|
||||
|
||||
func AsCommand[E DecodableCommand](c DecodableCommand) (result E, ok bool) {
|
||||
if v, ok := c.(E); ok {
|
||||
return v, true
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
## Generating sources
|
||||
|
||||
Given a `.fbs` file, source code can be generated using `flatc` command:
|
||||
Given a `.fbs` file, source code can be generated using `flatc` from
|
||||
this directory:
|
||||
|
||||
```shell
|
||||
flatc --go {file}.fbs
|
||||
flatc --go --go-module-name galaxy/schema/fbs {file}.fbs
|
||||
```
|
||||
|
||||
The `--go-module-name` flag rewrites cross-namespace imports to the
|
||||
fully-qualified module path (e.g. `common "galaxy/schema/fbs/common"`)
|
||||
so the generated code links inside this Go module without local
|
||||
replace directives. Omitting the flag yields imports such as
|
||||
`common "common"` which fail to resolve.
|
||||
|
||||
@@ -220,6 +220,31 @@ table UserGamesOrder {
|
||||
// — kept as a typed envelope for future extension.
|
||||
table UserGamesCommandResponse {}
|
||||
|
||||
// UserGamesOrderResponse is the success acknowledgement returned for
|
||||
// `MessageTypeUserGamesOrder`. Mirrors `UserGamesCommandResponse`.
|
||||
table UserGamesOrderResponse {}
|
||||
// UserGamesOrderResponse mirrors the engine's `PUT /api/v1/order`
|
||||
// success body: it echoes the stored order back to the caller with
|
||||
// the engine-assigned `updated_at` timestamp and per-command
|
||||
// `cmd_applied` / `cmd_error_code` populated on every entry.
|
||||
table UserGamesOrderResponse {
|
||||
game_id: common.UUID;
|
||||
updated_at: int64;
|
||||
commands: [CommandItem];
|
||||
}
|
||||
|
||||
// UserGamesOrderGet is the signed-gRPC request payload for
|
||||
// `MessageTypeUserGamesOrderGet`. Fetches the player's stored order
|
||||
// for the given turn — the caller always knows the current turn from
|
||||
// the lobby record so `turn` is required and must be non-negative.
|
||||
table UserGamesOrderGet {
|
||||
game_id: common.UUID (required);
|
||||
turn: int64;
|
||||
}
|
||||
|
||||
// UserGamesOrderGetResponse carries the result of
|
||||
// `MessageTypeUserGamesOrderGet`. `found = false` is how the FBS
|
||||
// envelope conveys the engine's `204 No Content` (no order stored
|
||||
// for this player on this turn). When `found = true`, `order` is
|
||||
// the engine's stored order for the turn.
|
||||
table UserGamesOrderGetResponse {
|
||||
found: bool;
|
||||
order: UserGamesOrder;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
|
||||
common "galaxy/schema/fbs/common"
|
||||
)
|
||||
|
||||
type UserGamesOrderResponse struct {
|
||||
@@ -41,8 +43,65 @@ func (rcv *UserGamesOrderResponse) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *UserGamesOrderResponse) GameId(obj *common.UUID) *common.UUID {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := o + rcv._tab.Pos
|
||||
if obj == nil {
|
||||
obj = new(common.UUID)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *UserGamesOrderResponse) UpdatedAt() int64 {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetInt64(o + rcv._tab.Pos)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rcv *UserGamesOrderResponse) MutateUpdatedAt(n int64) bool {
|
||||
return rcv._tab.MutateInt64Slot(6, n)
|
||||
}
|
||||
|
||||
func (rcv *UserGamesOrderResponse) Commands(obj *CommandItem, j int) bool {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
|
||||
if o != 0 {
|
||||
x := rcv._tab.Vector(o)
|
||||
x += flatbuffers.UOffsetT(j) * 4
|
||||
x = rcv._tab.Indirect(x)
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rcv *UserGamesOrderResponse) CommandsLength() int {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
|
||||
if o != 0 {
|
||||
return rcv._tab.VectorLen(o)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func UserGamesOrderResponseStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(0)
|
||||
builder.StartObject(3)
|
||||
}
|
||||
func UserGamesOrderResponseAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
|
||||
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
|
||||
}
|
||||
func UserGamesOrderResponseAddUpdatedAt(builder *flatbuffers.Builder, updatedAt int64) {
|
||||
builder.PrependInt64Slot(1, updatedAt, 0)
|
||||
}
|
||||
func UserGamesOrderResponseAddCommands(builder *flatbuffers.Builder, commands flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(commands), 0)
|
||||
}
|
||||
func UserGamesOrderResponseStartCommandsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
|
||||
return builder.StartVector(4, numElems, 4)
|
||||
}
|
||||
func UserGamesOrderResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
|
||||
+321
-5
@@ -1,6 +1,7 @@
|
||||
package transcoder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -9,8 +10,117 @@ import (
|
||||
fbs "galaxy/schema/fbs/order"
|
||||
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// JSONToUserGamesOrder decodes the engine's JSON response body for
|
||||
// `PUT /api/v1/order` and `GET /api/v1/order` into the typed
|
||||
// `*model.UserGamesOrder`. The model's `Commands` field is an
|
||||
// interface (`order.DecodableCommand`), so plain `json.Unmarshal`
|
||||
// can't reach it — this helper performs the same per-`@type`
|
||||
// dispatch as `game/internal/repo.ParseOrder`, but stays inside the
|
||||
// shared transcoder so non-engine callers (the gateway, tests) can
|
||||
// reuse it without crossing module boundaries.
|
||||
func JSONToUserGamesOrder(payload []byte) (*model.UserGamesOrder, error) {
|
||||
if len(payload) == 0 {
|
||||
return nil, errors.New("decode user games order json: payload is empty")
|
||||
}
|
||||
var raw struct {
|
||||
GameID string `json:"game_id"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
Commands []json.RawMessage `json:"cmd"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &raw); err != nil {
|
||||
return nil, fmt.Errorf("decode user games order json: %w", err)
|
||||
}
|
||||
out := &model.UserGamesOrder{
|
||||
UpdatedAt: raw.UpdatedAt,
|
||||
}
|
||||
if raw.GameID != "" {
|
||||
gameID, err := uuid.Parse(raw.GameID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode user games order json: invalid game_id %q: %w", raw.GameID, err)
|
||||
}
|
||||
out.GameID = gameID
|
||||
}
|
||||
if len(raw.Commands) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
out.Commands = make([]model.DecodableCommand, len(raw.Commands))
|
||||
for i, rawCmd := range raw.Commands {
|
||||
cmd, err := decodeJSONCommand(rawCmd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode user games order json command %d: %w", i, err)
|
||||
}
|
||||
out.Commands[i] = cmd
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func decodeJSONCommand(raw json.RawMessage) (model.DecodableCommand, error) {
|
||||
meta := new(model.CommandMeta)
|
||||
if err := json.Unmarshal(raw, meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch meta.CmdType {
|
||||
case model.CommandTypeRaceQuit:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandRaceQuit))
|
||||
case model.CommandTypeRaceVote:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandRaceVote))
|
||||
case model.CommandTypeRaceRelation:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandRaceRelation))
|
||||
case model.CommandTypeShipClassCreate:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipClassCreate))
|
||||
case model.CommandTypeShipClassMerge:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipClassMerge))
|
||||
case model.CommandTypeShipClassRemove:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipClassRemove))
|
||||
case model.CommandTypeShipGroupBreak:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipGroupBreak))
|
||||
case model.CommandTypeShipGroupLoad:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipGroupLoad))
|
||||
case model.CommandTypeShipGroupUnload:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipGroupUnload))
|
||||
case model.CommandTypeShipGroupSend:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipGroupSend))
|
||||
case model.CommandTypeShipGroupUpgrade:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipGroupUpgrade))
|
||||
case model.CommandTypeShipGroupMerge:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipGroupMerge))
|
||||
case model.CommandTypeShipGroupDismantle:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipGroupDismantle))
|
||||
case model.CommandTypeShipGroupTransfer:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipGroupTransfer))
|
||||
case model.CommandTypeShipGroupJoinFleet:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandShipGroupJoinFleet))
|
||||
case model.CommandTypeFleetMerge:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandFleetMerge))
|
||||
case model.CommandTypeFleetSend:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandFleetSend))
|
||||
case model.CommandTypeScienceCreate:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandScienceCreate))
|
||||
case model.CommandTypeScienceRemove:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandScienceRemove))
|
||||
case model.CommandTypePlanetRename:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandPlanetRename))
|
||||
case model.CommandTypePlanetProduce:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandPlanetProduce))
|
||||
case model.CommandTypePlanetRouteSet:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandPlanetRouteSet))
|
||||
case model.CommandTypePlanetRouteRemove:
|
||||
return unmarshalJSONCommand(raw, new(model.CommandPlanetRouteRemove))
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown command type: %s", meta.CmdType)
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalJSONCommand[T model.DecodableCommand](raw json.RawMessage, v T) (model.DecodableCommand, error) {
|
||||
if err := json.Unmarshal(raw, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type encodedCommand struct {
|
||||
cmdID string
|
||||
cmdApplied *bool
|
||||
@@ -955,14 +1065,220 @@ func EmptyUserGamesCommandResponsePayload() []byte {
|
||||
return builder.FinishedBytes()
|
||||
}
|
||||
|
||||
// EmptyUserGamesOrderResponsePayload mirrors
|
||||
// EmptyUserGamesCommandResponsePayload for `MessageTypeUserGamesOrder`.
|
||||
func EmptyUserGamesOrderResponsePayload() []byte {
|
||||
builder := flatbuffers.NewBuilder(16)
|
||||
// UserGamesOrderResponseToPayload encodes the engine's response body
|
||||
// for `PUT /api/v1/order` into the wire FlatBuffers envelope expected
|
||||
// for `MessageTypeUserGamesOrder`. The engine populates per-command
|
||||
// `cmdApplied` / `cmdErrorCode` fields, and they round-trip into the
|
||||
// FBS `CommandItem` entries unchanged. A nil response is encoded as
|
||||
// an empty envelope so the gateway can fall back to a batch-level
|
||||
// "ok" answer when the engine body is unavailable.
|
||||
func UserGamesOrderResponseToPayload(req *model.UserGamesOrder) ([]byte, error) {
|
||||
builder := flatbuffers.NewBuilder(1024)
|
||||
if req == nil {
|
||||
fbs.UserGamesOrderResponseStart(builder)
|
||||
offset := fbs.UserGamesOrderResponseEnd(builder)
|
||||
fbs.FinishUserGamesOrderResponseBuffer(builder, offset)
|
||||
return builder.FinishedBytes(), nil
|
||||
}
|
||||
|
||||
commandsVec, err := encodeCommandItemVector(builder, req.Commands, "user games order response")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fbs.UserGamesOrderResponseStart(builder)
|
||||
hi, lo := uuidToHiLo(req.GameID)
|
||||
fbs.UserGamesOrderResponseAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
|
||||
fbs.UserGamesOrderResponseAddUpdatedAt(builder, req.UpdatedAt)
|
||||
if commandsVec != 0 {
|
||||
fbs.UserGamesOrderResponseAddCommands(builder, commandsVec)
|
||||
}
|
||||
offset := fbs.UserGamesOrderResponseEnd(builder)
|
||||
fbs.FinishUserGamesOrderResponseBuffer(builder, offset)
|
||||
return builder.FinishedBytes()
|
||||
return builder.FinishedBytes(), nil
|
||||
}
|
||||
|
||||
// PayloadToUserGamesOrderGet decodes the FlatBuffers payload of
|
||||
// `MessageTypeUserGamesOrderGet` into the typed model. `Turn` is
|
||||
// validated to be non-negative; the gateway and backend reject
|
||||
// negative values before forwarding to the engine.
|
||||
func PayloadToUserGamesOrderGet(data []byte) (result *model.UserGamesOrderGet, err error) {
|
||||
if len(data) == 0 {
|
||||
return nil, errors.New("decode user games order get payload: data is empty")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
result = nil
|
||||
err = fmt.Errorf("decode user games order get payload: panic recovered: %v", recovered)
|
||||
}
|
||||
}()
|
||||
|
||||
flat := fbs.GetRootAsUserGamesOrderGet(data, 0)
|
||||
gameID := flat.GameId(nil)
|
||||
if gameID == nil {
|
||||
return nil, errors.New("decode user games order get payload: game_id is missing")
|
||||
}
|
||||
turn := flat.Turn()
|
||||
if turn < 0 {
|
||||
return nil, fmt.Errorf("decode user games order get payload: turn must be non-negative, got %d", turn)
|
||||
}
|
||||
return &model.UserGamesOrderGet{
|
||||
GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
|
||||
Turn: int(turn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UserGamesOrderGetToPayload encodes a `model.UserGamesOrderGet`
|
||||
// request into FlatBuffers bytes suitable for the authenticated
|
||||
// gateway transport.
|
||||
func UserGamesOrderGetToPayload(req *model.UserGamesOrderGet) ([]byte, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("encode user games order get payload: request is nil")
|
||||
}
|
||||
if req.Turn < 0 {
|
||||
return nil, fmt.Errorf("encode user games order get payload: turn must be non-negative, got %d", req.Turn)
|
||||
}
|
||||
builder := flatbuffers.NewBuilder(64)
|
||||
fbs.UserGamesOrderGetStart(builder)
|
||||
hi, lo := uuidToHiLo(req.GameID)
|
||||
fbs.UserGamesOrderGetAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
|
||||
fbs.UserGamesOrderGetAddTurn(builder, int64(req.Turn))
|
||||
offset := fbs.UserGamesOrderGetEnd(builder)
|
||||
fbs.FinishUserGamesOrderGetBuffer(builder, offset)
|
||||
return builder.FinishedBytes(), nil
|
||||
}
|
||||
|
||||
// UserGamesOrderGetResponseToPayload encodes the typed response of
|
||||
// `MessageTypeUserGamesOrderGet`. `found = false` corresponds to the
|
||||
// engine's `204 No Content` answer; `order` is omitted in that case.
|
||||
func UserGamesOrderGetResponseToPayload(order *model.UserGamesOrder, found bool) ([]byte, error) {
|
||||
builder := flatbuffers.NewBuilder(1024)
|
||||
|
||||
var orderOffset flatbuffers.UOffsetT
|
||||
if found && order != nil {
|
||||
commandsVec, err := encodeCommandItemVector(builder, order.Commands, "user games order get response")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fbs.UserGamesOrderStart(builder)
|
||||
hi, lo := uuidToHiLo(order.GameID)
|
||||
fbs.UserGamesOrderAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
|
||||
fbs.UserGamesOrderAddUpdatedAt(builder, order.UpdatedAt)
|
||||
if commandsVec != 0 {
|
||||
fbs.UserGamesOrderAddCommands(builder, commandsVec)
|
||||
}
|
||||
orderOffset = fbs.UserGamesOrderEnd(builder)
|
||||
}
|
||||
|
||||
fbs.UserGamesOrderGetResponseStart(builder)
|
||||
fbs.UserGamesOrderGetResponseAddFound(builder, found)
|
||||
if orderOffset != 0 {
|
||||
fbs.UserGamesOrderGetResponseAddOrder(builder, orderOffset)
|
||||
}
|
||||
offset := fbs.UserGamesOrderGetResponseEnd(builder)
|
||||
fbs.FinishUserGamesOrderGetResponseBuffer(builder, offset)
|
||||
return builder.FinishedBytes(), nil
|
||||
}
|
||||
|
||||
// PayloadToUserGamesOrderResponse decodes the engine's PUT response
|
||||
// envelope into a typed `*UserGamesOrder`. Empty payloads decode to
|
||||
// nil so callers can fall back to batch-level handling without a
|
||||
// dedicated marker.
|
||||
func PayloadToUserGamesOrderResponse(data []byte) (result *model.UserGamesOrder, err error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
result = nil
|
||||
err = fmt.Errorf("decode user games order response payload: panic recovered: %v", recovered)
|
||||
}
|
||||
}()
|
||||
|
||||
flat := fbs.GetRootAsUserGamesOrderResponse(data, 0)
|
||||
gameID := flat.GameId(nil)
|
||||
if gameID == nil {
|
||||
// Empty envelope (gateway fallback). The caller treats this
|
||||
// as "no per-command detail" and synthesises a batch-level
|
||||
// answer.
|
||||
if flat.CommandsLength() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("decode user games order response payload: game_id is missing")
|
||||
}
|
||||
out := &model.UserGamesOrder{
|
||||
GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
|
||||
UpdatedAt: flat.UpdatedAt(),
|
||||
}
|
||||
count := flat.CommandsLength()
|
||||
if count > 0 {
|
||||
out.Commands = make([]model.DecodableCommand, count)
|
||||
flatCommand := new(fbs.CommandItem)
|
||||
for i := 0; i < count; i++ {
|
||||
if !flat.Commands(flatCommand, i) {
|
||||
return nil, fmt.Errorf("decode user games order response %d: command item is missing", i)
|
||||
}
|
||||
cmd, decodeErr := decodeOrderCommand(flatCommand, i)
|
||||
if decodeErr != nil {
|
||||
return nil, decodeErr
|
||||
}
|
||||
out.Commands[i] = cmd
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// PayloadToUserGamesOrderGetResponse decodes the FlatBuffers response
|
||||
// of `MessageTypeUserGamesOrderGet`. When `found = false`, returns
|
||||
// `(nil, false, nil)` matching the engine's `204 No Content`
|
||||
// semantics.
|
||||
func PayloadToUserGamesOrderGetResponse(data []byte) (order *model.UserGamesOrder, found bool, err error) {
|
||||
if len(data) == 0 {
|
||||
return nil, false, errors.New("decode user games order get response payload: data is empty")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
order = nil
|
||||
found = false
|
||||
err = fmt.Errorf("decode user games order get response payload: panic recovered: %v", recovered)
|
||||
}
|
||||
}()
|
||||
|
||||
flat := fbs.GetRootAsUserGamesOrderGetResponse(data, 0)
|
||||
if !flat.Found() {
|
||||
return nil, false, nil
|
||||
}
|
||||
inner := flat.Order(nil)
|
||||
if inner == nil {
|
||||
return nil, true, errors.New("decode user games order get response payload: order is missing while found=true")
|
||||
}
|
||||
gameID := inner.GameId(nil)
|
||||
if gameID == nil {
|
||||
return nil, true, errors.New("decode user games order get response payload: order.game_id is missing")
|
||||
}
|
||||
out := &model.UserGamesOrder{
|
||||
GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
|
||||
UpdatedAt: inner.UpdatedAt(),
|
||||
}
|
||||
count := inner.CommandsLength()
|
||||
if count > 0 {
|
||||
out.Commands = make([]model.DecodableCommand, count)
|
||||
flatCommand := new(fbs.CommandItem)
|
||||
for i := 0; i < count; i++ {
|
||||
if !inner.Commands(flatCommand, i) {
|
||||
return nil, true, fmt.Errorf("decode user games order get response %d: command item is missing", i)
|
||||
}
|
||||
cmd, decodeErr := decodeOrderCommand(flatCommand, i)
|
||||
if decodeErr != nil {
|
||||
return nil, true, decodeErr
|
||||
}
|
||||
out.Commands[i] = cmd
|
||||
}
|
||||
}
|
||||
return out, true, nil
|
||||
}
|
||||
|
||||
// encodeCommandItemVector serialises a slice of DecodableCommand into a
|
||||
|
||||
@@ -77,6 +77,160 @@ func TestUserGamesCommandRejectsNilAndEmpty(t *testing.T) {
|
||||
if _, err := PayloadToUserGamesOrder(nil); err == nil {
|
||||
t.Fatalf("expected error decoding empty user games order")
|
||||
}
|
||||
if _, err := UserGamesOrderGetToPayload(nil); err == nil {
|
||||
t.Fatalf("expected error encoding nil user games order get")
|
||||
}
|
||||
if _, err := PayloadToUserGamesOrderGet(nil); err == nil {
|
||||
t.Fatalf("expected error decoding empty user games order get")
|
||||
}
|
||||
if _, _, err := PayloadToUserGamesOrderGetResponse(nil); err == nil {
|
||||
t.Fatalf("expected error decoding empty user games order get response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGamesOrderResponsePayloadRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
applied := true
|
||||
rejected := false
|
||||
errCode := 7
|
||||
source := &model.UserGamesOrder{
|
||||
GameID: uuid.MustParse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"),
|
||||
UpdatedAt: 99,
|
||||
Commands: []model.DecodableCommand{
|
||||
&model.CommandPlanetRename{
|
||||
CommandMeta: commandMeta("cmd-1", model.CommandTypePlanetRename, &applied, nil),
|
||||
Number: 5,
|
||||
Name: "alpha",
|
||||
},
|
||||
&model.CommandPlanetRename{
|
||||
CommandMeta: commandMeta("cmd-2", model.CommandTypePlanetRename, &rejected, &errCode),
|
||||
Number: 6,
|
||||
Name: "beta",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
payload, err := UserGamesOrderResponseToPayload(source)
|
||||
if err != nil {
|
||||
t.Fatalf("encode user games order response: %v", err)
|
||||
}
|
||||
|
||||
decoded, err := PayloadToUserGamesOrderResponse(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("decode user games order response: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(source, decoded) {
|
||||
t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGamesOrderResponseEmptyPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
payload, err := UserGamesOrderResponseToPayload(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("encode empty user games order response: %v", err)
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
t.Fatal("empty envelope payload must be non-zero length")
|
||||
}
|
||||
|
||||
decoded, err := PayloadToUserGamesOrderResponse(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("decode empty user games order response: %v", err)
|
||||
}
|
||||
if decoded != nil {
|
||||
t.Fatalf("empty envelope must decode to nil, got %#v", decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGamesOrderGetPayloadRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
source := &model.UserGamesOrderGet{
|
||||
GameID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
|
||||
Turn: 7,
|
||||
}
|
||||
|
||||
payload, err := UserGamesOrderGetToPayload(source)
|
||||
if err != nil {
|
||||
t.Fatalf("encode user games order get: %v", err)
|
||||
}
|
||||
|
||||
decoded, err := PayloadToUserGamesOrderGet(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("decode user games order get: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(source, decoded) {
|
||||
t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGamesOrderGetRejectsNegativeTurn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := UserGamesOrderGetToPayload(&model.UserGamesOrderGet{
|
||||
GameID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
|
||||
Turn: -1,
|
||||
}); err == nil {
|
||||
t.Fatalf("expected error encoding negative turn")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGamesOrderGetResponseRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
applied := true
|
||||
stored := &model.UserGamesOrder{
|
||||
GameID: uuid.MustParse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"),
|
||||
UpdatedAt: 1234,
|
||||
Commands: []model.DecodableCommand{
|
||||
&model.CommandPlanetRename{
|
||||
CommandMeta: commandMeta("cmd-1", model.CommandTypePlanetRename, &applied, nil),
|
||||
Number: 5,
|
||||
Name: "stored",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
payload, err := UserGamesOrderGetResponseToPayload(stored, true)
|
||||
if err != nil {
|
||||
t.Fatalf("encode user games order get response: %v", err)
|
||||
}
|
||||
|
||||
decoded, found, err := PayloadToUserGamesOrderGetResponse(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("decode user games order get response: %v", err)
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("expected found=true round-trip")
|
||||
}
|
||||
if !reflect.DeepEqual(stored, decoded) {
|
||||
t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", stored, decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserGamesOrderGetResponseNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
payload, err := UserGamesOrderGetResponseToPayload(nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("encode not-found response: %v", err)
|
||||
}
|
||||
|
||||
decoded, found, err := PayloadToUserGamesOrderGetResponse(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("decode not-found response: %v", err)
|
||||
}
|
||||
if found {
|
||||
t.Fatal("expected found=false")
|
||||
}
|
||||
if decoded != nil {
|
||||
t.Fatalf("expected nil order, got %#v", decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInt64ToInt(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user