f80c623a74
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>
1322 lines
49 KiB
Go
1322 lines
49 KiB
Go
package transcoder
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
model "galaxy/model/order"
|
|
commonfbs "galaxy/schema/fbs/common"
|
|
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
|
|
cmdErrCode *int
|
|
payloadType fbs.CommandPayload
|
|
payloadOffset flatbuffers.UOffsetT
|
|
}
|
|
|
|
func encodeOrderCommand(builder *flatbuffers.Builder, command model.DecodableCommand, index int) (encodedCommand, error) {
|
|
if command == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil", index)
|
|
}
|
|
|
|
switch cmd := command.(type) {
|
|
case *model.CommandRaceQuit:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
fbs.CommandRaceQuitStart(builder)
|
|
payload := fbs.CommandRaceQuitEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandRaceQuit, payload), nil
|
|
case *model.CommandRaceVote:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
acceptor := builder.CreateString(cmd.Acceptor)
|
|
fbs.CommandRaceVoteStart(builder)
|
|
fbs.CommandRaceVoteAddAcceptor(builder, acceptor)
|
|
payload := fbs.CommandRaceVoteEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandRaceVote, payload), nil
|
|
case *model.CommandRaceRelation:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
relation, err := relationToFBS(cmd.Relation)
|
|
if err != nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
|
|
}
|
|
acceptor := builder.CreateString(cmd.Acceptor)
|
|
fbs.CommandRaceRelationStart(builder)
|
|
fbs.CommandRaceRelationAddAcceptor(builder, acceptor)
|
|
fbs.CommandRaceRelationAddRelation(builder, relation)
|
|
payload := fbs.CommandRaceRelationEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandRaceRelation, payload), nil
|
|
case *model.CommandShipClassCreate:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
name := builder.CreateString(cmd.Name)
|
|
fbs.CommandShipClassCreateStart(builder)
|
|
fbs.CommandShipClassCreateAddName(builder, name)
|
|
fbs.CommandShipClassCreateAddDrive(builder, cmd.Drive)
|
|
fbs.CommandShipClassCreateAddArmament(builder, int64(cmd.Armament))
|
|
fbs.CommandShipClassCreateAddWeapons(builder, cmd.Weapons)
|
|
fbs.CommandShipClassCreateAddShields(builder, cmd.Shields)
|
|
fbs.CommandShipClassCreateAddCargo(builder, cmd.Cargo)
|
|
payload := fbs.CommandShipClassCreateEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipClassCreate, payload), nil
|
|
case *model.CommandShipClassMerge:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
name := builder.CreateString(cmd.Name)
|
|
target := builder.CreateString(cmd.Target)
|
|
fbs.CommandShipClassMergeStart(builder)
|
|
fbs.CommandShipClassMergeAddName(builder, name)
|
|
fbs.CommandShipClassMergeAddTarget(builder, target)
|
|
payload := fbs.CommandShipClassMergeEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipClassMerge, payload), nil
|
|
case *model.CommandShipClassRemove:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
name := builder.CreateString(cmd.Name)
|
|
fbs.CommandShipClassRemoveStart(builder)
|
|
fbs.CommandShipClassRemoveAddName(builder, name)
|
|
payload := fbs.CommandShipClassRemoveEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipClassRemove, payload), nil
|
|
case *model.CommandShipGroupBreak:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
id := builder.CreateString(cmd.ID)
|
|
newID := builder.CreateString(cmd.NewID)
|
|
fbs.CommandShipGroupBreakStart(builder)
|
|
fbs.CommandShipGroupBreakAddId(builder, id)
|
|
fbs.CommandShipGroupBreakAddNewId(builder, newID)
|
|
fbs.CommandShipGroupBreakAddQuantity(builder, int64(cmd.Quantity))
|
|
payload := fbs.CommandShipGroupBreakEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupBreak, payload), nil
|
|
case *model.CommandShipGroupLoad:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
cargo, err := shipGroupCargoToFBS(cmd.Cargo)
|
|
if err != nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
|
|
}
|
|
id := builder.CreateString(cmd.ID)
|
|
fbs.CommandShipGroupLoadStart(builder)
|
|
fbs.CommandShipGroupLoadAddId(builder, id)
|
|
fbs.CommandShipGroupLoadAddCargo(builder, cargo)
|
|
fbs.CommandShipGroupLoadAddQuantity(builder, cmd.Quantity)
|
|
payload := fbs.CommandShipGroupLoadEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupLoad, payload), nil
|
|
case *model.CommandShipGroupUnload:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
id := builder.CreateString(cmd.ID)
|
|
fbs.CommandShipGroupUnloadStart(builder)
|
|
fbs.CommandShipGroupUnloadAddId(builder, id)
|
|
fbs.CommandShipGroupUnloadAddQuantity(builder, cmd.Quantity)
|
|
payload := fbs.CommandShipGroupUnloadEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupUnload, payload), nil
|
|
case *model.CommandShipGroupSend:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
id := builder.CreateString(cmd.ID)
|
|
fbs.CommandShipGroupSendStart(builder)
|
|
fbs.CommandShipGroupSendAddId(builder, id)
|
|
fbs.CommandShipGroupSendAddDestination(builder, int64(cmd.Destination))
|
|
payload := fbs.CommandShipGroupSendEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupSend, payload), nil
|
|
case *model.CommandShipGroupUpgrade:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
tech, err := shipGroupUpgradeTechToFBS(cmd.Tech)
|
|
if err != nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
|
|
}
|
|
id := builder.CreateString(cmd.ID)
|
|
fbs.CommandShipGroupUpgradeStart(builder)
|
|
fbs.CommandShipGroupUpgradeAddId(builder, id)
|
|
fbs.CommandShipGroupUpgradeAddTech(builder, tech)
|
|
fbs.CommandShipGroupUpgradeAddLevel(builder, cmd.Level)
|
|
payload := fbs.CommandShipGroupUpgradeEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupUpgrade, payload), nil
|
|
case *model.CommandShipGroupMerge:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
fbs.CommandShipGroupMergeStart(builder)
|
|
payload := fbs.CommandShipGroupMergeEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupMerge, payload), nil
|
|
case *model.CommandShipGroupDismantle:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
id := builder.CreateString(cmd.ID)
|
|
fbs.CommandShipGroupDismantleStart(builder)
|
|
fbs.CommandShipGroupDismantleAddId(builder, id)
|
|
payload := fbs.CommandShipGroupDismantleEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupDismantle, payload), nil
|
|
case *model.CommandShipGroupTransfer:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
id := builder.CreateString(cmd.ID)
|
|
acceptor := builder.CreateString(cmd.Acceptor)
|
|
fbs.CommandShipGroupTransferStart(builder)
|
|
fbs.CommandShipGroupTransferAddId(builder, id)
|
|
fbs.CommandShipGroupTransferAddAcceptor(builder, acceptor)
|
|
payload := fbs.CommandShipGroupTransferEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupTransfer, payload), nil
|
|
case *model.CommandShipGroupJoinFleet:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
id := builder.CreateString(cmd.ID)
|
|
name := builder.CreateString(cmd.Name)
|
|
fbs.CommandShipGroupJoinFleetStart(builder)
|
|
fbs.CommandShipGroupJoinFleetAddId(builder, id)
|
|
fbs.CommandShipGroupJoinFleetAddName(builder, name)
|
|
payload := fbs.CommandShipGroupJoinFleetEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandShipGroupJoinFleet, payload), nil
|
|
case *model.CommandFleetMerge:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
name := builder.CreateString(cmd.Name)
|
|
target := builder.CreateString(cmd.Target)
|
|
fbs.CommandFleetMergeStart(builder)
|
|
fbs.CommandFleetMergeAddName(builder, name)
|
|
fbs.CommandFleetMergeAddTarget(builder, target)
|
|
payload := fbs.CommandFleetMergeEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandFleetMerge, payload), nil
|
|
case *model.CommandFleetSend:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
name := builder.CreateString(cmd.Name)
|
|
fbs.CommandFleetSendStart(builder)
|
|
fbs.CommandFleetSendAddName(builder, name)
|
|
fbs.CommandFleetSendAddDestination(builder, int64(cmd.Destination))
|
|
payload := fbs.CommandFleetSendEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandFleetSend, payload), nil
|
|
case *model.CommandScienceCreate:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
name := builder.CreateString(cmd.Name)
|
|
fbs.CommandScienceCreateStart(builder)
|
|
fbs.CommandScienceCreateAddName(builder, name)
|
|
fbs.CommandScienceCreateAddDrive(builder, cmd.Drive)
|
|
fbs.CommandScienceCreateAddWeapons(builder, cmd.Weapons)
|
|
fbs.CommandScienceCreateAddShields(builder, cmd.Shields)
|
|
fbs.CommandScienceCreateAddCargo(builder, cmd.Cargo)
|
|
payload := fbs.CommandScienceCreateEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandScienceCreate, payload), nil
|
|
case *model.CommandScienceRemove:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
name := builder.CreateString(cmd.Name)
|
|
fbs.CommandScienceRemoveStart(builder)
|
|
fbs.CommandScienceRemoveAddName(builder, name)
|
|
payload := fbs.CommandScienceRemoveEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandScienceRemove, payload), nil
|
|
case *model.CommandPlanetRename:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
name := builder.CreateString(cmd.Name)
|
|
fbs.CommandPlanetRenameStart(builder)
|
|
fbs.CommandPlanetRenameAddNumber(builder, int64(cmd.Number))
|
|
fbs.CommandPlanetRenameAddName(builder, name)
|
|
payload := fbs.CommandPlanetRenameEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandPlanetRename, payload), nil
|
|
case *model.CommandPlanetProduce:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
production, err := planetProductionToFBS(cmd.Production)
|
|
if err != nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
|
|
}
|
|
subject := builder.CreateString(cmd.Subject)
|
|
fbs.CommandPlanetProduceStart(builder)
|
|
fbs.CommandPlanetProduceAddNumber(builder, int64(cmd.Number))
|
|
fbs.CommandPlanetProduceAddProduction(builder, production)
|
|
fbs.CommandPlanetProduceAddSubject(builder, subject)
|
|
payload := fbs.CommandPlanetProduceEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandPlanetProduce, payload), nil
|
|
case *model.CommandPlanetRouteSet:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
loadType, err := planetRouteLoadTypeToFBS(cmd.LoadType)
|
|
if err != nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
|
|
}
|
|
fbs.CommandPlanetRouteSetStart(builder)
|
|
fbs.CommandPlanetRouteSetAddOrigin(builder, int64(cmd.Origin))
|
|
fbs.CommandPlanetRouteSetAddDestination(builder, int64(cmd.Destination))
|
|
fbs.CommandPlanetRouteSetAddLoadType(builder, loadType)
|
|
payload := fbs.CommandPlanetRouteSetEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandPlanetRouteSet, payload), nil
|
|
case *model.CommandPlanetRouteRemove:
|
|
if cmd == nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: command is nil %T", index, command)
|
|
}
|
|
loadType, err := planetRouteLoadTypeToFBS(cmd.LoadType)
|
|
if err != nil {
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: %w", index, err)
|
|
}
|
|
fbs.CommandPlanetRouteRemoveStart(builder)
|
|
fbs.CommandPlanetRouteRemoveAddOrigin(builder, int64(cmd.Origin))
|
|
fbs.CommandPlanetRouteRemoveAddLoadType(builder, loadType)
|
|
payload := fbs.CommandPlanetRouteRemoveEnd(builder)
|
|
return encodedCommandFromMeta(cmd.CommandMeta, fbs.CommandPayloadCommandPlanetRouteRemove, payload), nil
|
|
default:
|
|
return encodedCommand{}, fmt.Errorf("encode order command %d: unsupported command type %T", index, command)
|
|
}
|
|
}
|
|
|
|
func encodedCommandFromMeta(meta model.CommandMeta, payloadType fbs.CommandPayload, payloadOffset flatbuffers.UOffsetT) encodedCommand {
|
|
return encodedCommand{
|
|
cmdID: meta.CmdID,
|
|
cmdApplied: cloneBoolPointer(meta.CmdApplied),
|
|
cmdErrCode: cloneIntPointer(meta.CmdErrCode),
|
|
payloadType: payloadType,
|
|
payloadOffset: payloadOffset,
|
|
}
|
|
}
|
|
|
|
func decodeOrderCommand(flatCommand *fbs.CommandItem, index int) (model.DecodableCommand, error) {
|
|
commandMeta := model.CommandMeta{
|
|
CmdID: string(flatCommand.CmdId()),
|
|
CmdApplied: cloneBoolPointer(flatCommand.CmdApplied()),
|
|
}
|
|
|
|
if cmdErrCode := flatCommand.CmdErrorCode(); cmdErrCode != nil {
|
|
decodedCmdErrCode, err := int64ToInt(*cmdErrCode, "cmd_error_code")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
commandMeta.CmdErrCode = &decodedCmdErrCode
|
|
}
|
|
|
|
payloadType := flatCommand.PayloadType()
|
|
if payloadType == fbs.CommandPayloadNONE {
|
|
return nil, fmt.Errorf("decode order command %d: payload type is NONE", index)
|
|
}
|
|
|
|
payload := new(flatbuffers.Table)
|
|
if !flatCommand.Payload(payload) {
|
|
return nil, fmt.Errorf("decode order command %d: payload is missing", index)
|
|
}
|
|
|
|
switch payloadType {
|
|
case fbs.CommandPayloadCommandRaceQuit:
|
|
commandMeta.CmdType = model.CommandTypeRaceQuit
|
|
return &model.CommandRaceQuit{CommandMeta: commandMeta}, nil
|
|
case fbs.CommandPayloadCommandRaceVote:
|
|
commandMeta.CmdType = model.CommandTypeRaceVote
|
|
commandPayload := new(fbs.CommandRaceVote)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandRaceVote{
|
|
CommandMeta: commandMeta,
|
|
Acceptor: string(commandPayload.Acceptor()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandRaceRelation:
|
|
commandPayload := new(fbs.CommandRaceRelation)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
|
|
relation, err := relationFromFBS(commandPayload.Relation())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
|
|
commandMeta.CmdType = model.CommandTypeRaceRelation
|
|
return &model.CommandRaceRelation{
|
|
CommandMeta: commandMeta,
|
|
Acceptor: string(commandPayload.Acceptor()),
|
|
Relation: relation,
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipClassCreate:
|
|
commandMeta.CmdType = model.CommandTypeShipClassCreate
|
|
commandPayload := new(fbs.CommandShipClassCreate)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
armament, err := int64ToInt(commandPayload.Armament(), "armament")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
return &model.CommandShipClassCreate{
|
|
CommandMeta: commandMeta,
|
|
Name: string(commandPayload.Name()),
|
|
Drive: commandPayload.Drive(),
|
|
Armament: armament,
|
|
Weapons: commandPayload.Weapons(),
|
|
Shields: commandPayload.Shields(),
|
|
Cargo: commandPayload.Cargo(),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipClassMerge:
|
|
commandMeta.CmdType = model.CommandTypeShipClassMerge
|
|
commandPayload := new(fbs.CommandShipClassMerge)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandShipClassMerge{
|
|
CommandMeta: commandMeta,
|
|
Name: string(commandPayload.Name()),
|
|
Target: string(commandPayload.Target()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipClassRemove:
|
|
commandMeta.CmdType = model.CommandTypeShipClassRemove
|
|
commandPayload := new(fbs.CommandShipClassRemove)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandShipClassRemove{
|
|
CommandMeta: commandMeta,
|
|
Name: string(commandPayload.Name()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipGroupBreak:
|
|
commandMeta.CmdType = model.CommandTypeShipGroupBreak
|
|
commandPayload := new(fbs.CommandShipGroupBreak)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
quantity, err := int64ToInt(commandPayload.Quantity(), "quantity")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
return &model.CommandShipGroupBreak{
|
|
CommandMeta: commandMeta,
|
|
ID: string(commandPayload.Id()),
|
|
NewID: string(commandPayload.NewId()),
|
|
Quantity: quantity,
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipGroupLoad:
|
|
commandPayload := new(fbs.CommandShipGroupLoad)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
|
|
cargo, err := shipGroupCargoFromFBS(commandPayload.Cargo())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
|
|
commandMeta.CmdType = model.CommandTypeShipGroupLoad
|
|
return &model.CommandShipGroupLoad{
|
|
CommandMeta: commandMeta,
|
|
ID: string(commandPayload.Id()),
|
|
Cargo: cargo,
|
|
Quantity: commandPayload.Quantity(),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipGroupUnload:
|
|
commandMeta.CmdType = model.CommandTypeShipGroupUnload
|
|
commandPayload := new(fbs.CommandShipGroupUnload)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandShipGroupUnload{
|
|
CommandMeta: commandMeta,
|
|
ID: string(commandPayload.Id()),
|
|
Quantity: commandPayload.Quantity(),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipGroupSend:
|
|
commandMeta.CmdType = model.CommandTypeShipGroupSend
|
|
commandPayload := new(fbs.CommandShipGroupSend)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
destination, err := int64ToInt(commandPayload.Destination(), "destination")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
return &model.CommandShipGroupSend{
|
|
CommandMeta: commandMeta,
|
|
ID: string(commandPayload.Id()),
|
|
Destination: destination,
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipGroupUpgrade:
|
|
commandPayload := new(fbs.CommandShipGroupUpgrade)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
|
|
tech, err := shipGroupUpgradeTechFromFBS(commandPayload.Tech())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
|
|
commandMeta.CmdType = model.CommandTypeShipGroupUpgrade
|
|
return &model.CommandShipGroupUpgrade{
|
|
CommandMeta: commandMeta,
|
|
ID: string(commandPayload.Id()),
|
|
Tech: tech,
|
|
Level: commandPayload.Level(),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipGroupMerge:
|
|
commandMeta.CmdType = model.CommandTypeShipGroupMerge
|
|
return &model.CommandShipGroupMerge{CommandMeta: commandMeta}, nil
|
|
case fbs.CommandPayloadCommandShipGroupDismantle:
|
|
commandMeta.CmdType = model.CommandTypeShipGroupDismantle
|
|
commandPayload := new(fbs.CommandShipGroupDismantle)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandShipGroupDismantle{
|
|
CommandMeta: commandMeta,
|
|
ID: string(commandPayload.Id()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipGroupTransfer:
|
|
commandMeta.CmdType = model.CommandTypeShipGroupTransfer
|
|
commandPayload := new(fbs.CommandShipGroupTransfer)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandShipGroupTransfer{
|
|
CommandMeta: commandMeta,
|
|
ID: string(commandPayload.Id()),
|
|
Acceptor: string(commandPayload.Acceptor()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandShipGroupJoinFleet:
|
|
commandMeta.CmdType = model.CommandTypeShipGroupJoinFleet
|
|
commandPayload := new(fbs.CommandShipGroupJoinFleet)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandShipGroupJoinFleet{
|
|
CommandMeta: commandMeta,
|
|
ID: string(commandPayload.Id()),
|
|
Name: string(commandPayload.Name()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandFleetMerge:
|
|
commandMeta.CmdType = model.CommandTypeFleetMerge
|
|
commandPayload := new(fbs.CommandFleetMerge)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandFleetMerge{
|
|
CommandMeta: commandMeta,
|
|
Name: string(commandPayload.Name()),
|
|
Target: string(commandPayload.Target()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandFleetSend:
|
|
commandMeta.CmdType = model.CommandTypeFleetSend
|
|
commandPayload := new(fbs.CommandFleetSend)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
destination, err := int64ToInt(commandPayload.Destination(), "destination")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
return &model.CommandFleetSend{
|
|
CommandMeta: commandMeta,
|
|
Name: string(commandPayload.Name()),
|
|
Destination: destination,
|
|
}, nil
|
|
case fbs.CommandPayloadCommandScienceCreate:
|
|
commandMeta.CmdType = model.CommandTypeScienceCreate
|
|
commandPayload := new(fbs.CommandScienceCreate)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandScienceCreate{
|
|
CommandMeta: commandMeta,
|
|
Name: string(commandPayload.Name()),
|
|
Drive: commandPayload.Drive(),
|
|
Weapons: commandPayload.Weapons(),
|
|
Shields: commandPayload.Shields(),
|
|
Cargo: commandPayload.Cargo(),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandScienceRemove:
|
|
commandMeta.CmdType = model.CommandTypeScienceRemove
|
|
commandPayload := new(fbs.CommandScienceRemove)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
return &model.CommandScienceRemove{
|
|
CommandMeta: commandMeta,
|
|
Name: string(commandPayload.Name()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandPlanetRename:
|
|
commandMeta.CmdType = model.CommandTypePlanetRename
|
|
commandPayload := new(fbs.CommandPlanetRename)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
number, err := int64ToInt(commandPayload.Number(), "number")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
return &model.CommandPlanetRename{
|
|
CommandMeta: commandMeta,
|
|
Number: number,
|
|
Name: string(commandPayload.Name()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandPlanetProduce:
|
|
commandPayload := new(fbs.CommandPlanetProduce)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
|
|
production, err := planetProductionFromFBS(commandPayload.Production())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
number, err := int64ToInt(commandPayload.Number(), "number")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
|
|
commandMeta.CmdType = model.CommandTypePlanetProduce
|
|
return &model.CommandPlanetProduce{
|
|
CommandMeta: commandMeta,
|
|
Number: number,
|
|
Production: production,
|
|
Subject: string(commandPayload.Subject()),
|
|
}, nil
|
|
case fbs.CommandPayloadCommandPlanetRouteSet:
|
|
commandPayload := new(fbs.CommandPlanetRouteSet)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
|
|
loadType, err := planetRouteLoadTypeFromFBS(commandPayload.LoadType())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
origin, err := int64ToInt(commandPayload.Origin(), "origin")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
destination, err := int64ToInt(commandPayload.Destination(), "destination")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
|
|
commandMeta.CmdType = model.CommandTypePlanetRouteSet
|
|
return &model.CommandPlanetRouteSet{
|
|
CommandMeta: commandMeta,
|
|
Origin: origin,
|
|
Destination: destination,
|
|
LoadType: loadType,
|
|
}, nil
|
|
case fbs.CommandPayloadCommandPlanetRouteRemove:
|
|
commandPayload := new(fbs.CommandPlanetRouteRemove)
|
|
commandPayload.Init(payload.Bytes, payload.Pos)
|
|
|
|
loadType, err := planetRouteLoadTypeFromFBS(commandPayload.LoadType())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
origin, err := int64ToInt(commandPayload.Origin(), "origin")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode order command %d: %w", index, err)
|
|
}
|
|
|
|
commandMeta.CmdType = model.CommandTypePlanetRouteRemove
|
|
return &model.CommandPlanetRouteRemove{
|
|
CommandMeta: commandMeta,
|
|
Origin: origin,
|
|
LoadType: loadType,
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("decode order command %d: unknown command payload type %d", index, payloadType)
|
|
}
|
|
}
|
|
|
|
// int64ToInt narrows v to a Go int. Returns an error when v overflows
|
|
// the platform `int` range (only possible on 32-bit builds; on 64-bit
|
|
// the check is a no-op). fieldName is used in the error for caller
|
|
// context.
|
|
func int64ToInt(value int64, field string) (int, error) {
|
|
maxInt := int64(int(^uint(0) >> 1))
|
|
minInt := -maxInt - 1
|
|
|
|
if value < minInt || value > maxInt {
|
|
return 0, fmt.Errorf("%s value %d overflows int", field, value)
|
|
}
|
|
|
|
return int(value), nil
|
|
}
|
|
|
|
func relationToFBS(value string) (fbs.Relation, error) {
|
|
switch value {
|
|
case "WAR":
|
|
return fbs.RelationWAR, nil
|
|
case "PEACE":
|
|
return fbs.RelationPEACE, nil
|
|
default:
|
|
return fbs.RelationUNKNOWN, fmt.Errorf("unsupported relation value %q", value)
|
|
}
|
|
}
|
|
|
|
func relationFromFBS(value fbs.Relation) (string, error) {
|
|
switch value {
|
|
case fbs.RelationWAR:
|
|
return "WAR", nil
|
|
case fbs.RelationPEACE:
|
|
return "PEACE", nil
|
|
case fbs.RelationUNKNOWN:
|
|
return "", errors.New("relation value UNKNOWN is not allowed")
|
|
default:
|
|
return "", fmt.Errorf("unsupported relation enum value %d", value)
|
|
}
|
|
}
|
|
|
|
func shipGroupCargoToFBS(value string) (fbs.ShipGroupCargo, error) {
|
|
switch value {
|
|
case "COL":
|
|
return fbs.ShipGroupCargoCOL, nil
|
|
case "MAT":
|
|
return fbs.ShipGroupCargoMAT, nil
|
|
case "CAP":
|
|
return fbs.ShipGroupCargoCAP, nil
|
|
default:
|
|
return fbs.ShipGroupCargoUNKNOWN, fmt.Errorf("unsupported ship group cargo value %q", value)
|
|
}
|
|
}
|
|
|
|
func shipGroupCargoFromFBS(value fbs.ShipGroupCargo) (string, error) {
|
|
switch value {
|
|
case fbs.ShipGroupCargoCOL:
|
|
return "COL", nil
|
|
case fbs.ShipGroupCargoMAT:
|
|
return "MAT", nil
|
|
case fbs.ShipGroupCargoCAP:
|
|
return "CAP", nil
|
|
case fbs.ShipGroupCargoUNKNOWN:
|
|
return "", errors.New("ship group cargo value UNKNOWN is not allowed")
|
|
default:
|
|
return "", fmt.Errorf("unsupported ship group cargo enum value %d", value)
|
|
}
|
|
}
|
|
|
|
func shipGroupUpgradeTechToFBS(value string) (fbs.ShipGroupUpgradeTech, error) {
|
|
switch value {
|
|
case "ALL":
|
|
return fbs.ShipGroupUpgradeTechALL, nil
|
|
case "DRIVE":
|
|
return fbs.ShipGroupUpgradeTechDRIVE, nil
|
|
case "WEAPONS":
|
|
return fbs.ShipGroupUpgradeTechWEAPONS, nil
|
|
case "SHIELDS":
|
|
return fbs.ShipGroupUpgradeTechSHIELDS, nil
|
|
case "CARGO":
|
|
return fbs.ShipGroupUpgradeTechCARGO, nil
|
|
default:
|
|
return fbs.ShipGroupUpgradeTechUNKNOWN, fmt.Errorf("unsupported ship group upgrade tech value %q", value)
|
|
}
|
|
}
|
|
|
|
func shipGroupUpgradeTechFromFBS(value fbs.ShipGroupUpgradeTech) (string, error) {
|
|
switch value {
|
|
case fbs.ShipGroupUpgradeTechALL:
|
|
return "ALL", nil
|
|
case fbs.ShipGroupUpgradeTechDRIVE:
|
|
return "DRIVE", nil
|
|
case fbs.ShipGroupUpgradeTechWEAPONS:
|
|
return "WEAPONS", nil
|
|
case fbs.ShipGroupUpgradeTechSHIELDS:
|
|
return "SHIELDS", nil
|
|
case fbs.ShipGroupUpgradeTechCARGO:
|
|
return "CARGO", nil
|
|
case fbs.ShipGroupUpgradeTechUNKNOWN:
|
|
return "", errors.New("ship group upgrade tech value UNKNOWN is not allowed")
|
|
default:
|
|
return "", fmt.Errorf("unsupported ship group upgrade tech enum value %d", value)
|
|
}
|
|
}
|
|
|
|
func planetProductionToFBS(value string) (fbs.PlanetProduction, error) {
|
|
switch value {
|
|
case "MAT":
|
|
return fbs.PlanetProductionMAT, nil
|
|
case "CAP":
|
|
return fbs.PlanetProductionCAP, nil
|
|
case "DRIVE":
|
|
return fbs.PlanetProductionDRIVE, nil
|
|
case "WEAPONS":
|
|
return fbs.PlanetProductionWEAPONS, nil
|
|
case "SHIELDS":
|
|
return fbs.PlanetProductionSHIELDS, nil
|
|
case "CARGO":
|
|
return fbs.PlanetProductionCARGO, nil
|
|
case "SCIENCE":
|
|
return fbs.PlanetProductionSCIENCE, nil
|
|
case "SHIP":
|
|
return fbs.PlanetProductionSHIP, nil
|
|
default:
|
|
return fbs.PlanetProductionUNKNOWN, fmt.Errorf("unsupported planet production value %q", value)
|
|
}
|
|
}
|
|
|
|
func planetProductionFromFBS(value fbs.PlanetProduction) (string, error) {
|
|
switch value {
|
|
case fbs.PlanetProductionMAT:
|
|
return "MAT", nil
|
|
case fbs.PlanetProductionCAP:
|
|
return "CAP", nil
|
|
case fbs.PlanetProductionDRIVE:
|
|
return "DRIVE", nil
|
|
case fbs.PlanetProductionWEAPONS:
|
|
return "WEAPONS", nil
|
|
case fbs.PlanetProductionSHIELDS:
|
|
return "SHIELDS", nil
|
|
case fbs.PlanetProductionCARGO:
|
|
return "CARGO", nil
|
|
case fbs.PlanetProductionSCIENCE:
|
|
return "SCIENCE", nil
|
|
case fbs.PlanetProductionSHIP:
|
|
return "SHIP", nil
|
|
case fbs.PlanetProductionUNKNOWN:
|
|
return "", errors.New("planet production value UNKNOWN is not allowed")
|
|
default:
|
|
return "", fmt.Errorf("unsupported planet production enum value %d", value)
|
|
}
|
|
}
|
|
|
|
func planetRouteLoadTypeToFBS(value string) (fbs.PlanetRouteLoadType, error) {
|
|
switch value {
|
|
case "MAT":
|
|
return fbs.PlanetRouteLoadTypeMAT, nil
|
|
case "CAP":
|
|
return fbs.PlanetRouteLoadTypeCAP, nil
|
|
case "COL":
|
|
return fbs.PlanetRouteLoadTypeCOL, nil
|
|
case "EMP":
|
|
return fbs.PlanetRouteLoadTypeEMP, nil
|
|
default:
|
|
return fbs.PlanetRouteLoadTypeUNKNOWN, fmt.Errorf("unsupported planet route load type value %q", value)
|
|
}
|
|
}
|
|
|
|
func planetRouteLoadTypeFromFBS(value fbs.PlanetRouteLoadType) (string, error) {
|
|
switch value {
|
|
case fbs.PlanetRouteLoadTypeMAT:
|
|
return "MAT", nil
|
|
case fbs.PlanetRouteLoadTypeCAP:
|
|
return "CAP", nil
|
|
case fbs.PlanetRouteLoadTypeCOL:
|
|
return "COL", nil
|
|
case fbs.PlanetRouteLoadTypeEMP:
|
|
return "EMP", nil
|
|
case fbs.PlanetRouteLoadTypeUNKNOWN:
|
|
return "", errors.New("planet route load type value UNKNOWN is not allowed")
|
|
default:
|
|
return "", fmt.Errorf("unsupported planet route load type enum value %d", value)
|
|
}
|
|
}
|
|
|
|
func cloneBoolPointer(value *bool) *bool {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := *value
|
|
return &cloned
|
|
}
|
|
|
|
func cloneIntPointer(value *int) *int {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
cloned := *value
|
|
return &cloned
|
|
}
|
|
|
|
// UserGamesCommandToPayload converts model.UserGamesCommand to
|
|
// FlatBuffers bytes suitable for the authenticated gateway transport.
|
|
// `GameID` is required.
|
|
func UserGamesCommandToPayload(req *model.UserGamesCommand) ([]byte, error) {
|
|
if req == nil {
|
|
return nil, errors.New("encode user games command payload: request is nil")
|
|
}
|
|
|
|
builder := flatbuffers.NewBuilder(1024)
|
|
commandsVec, err := encodeCommandItemVector(builder, req.Commands, "user games command")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fbs.UserGamesCommandStart(builder)
|
|
hi, lo := uuidToHiLo(req.GameID)
|
|
fbs.UserGamesCommandAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
|
|
if commandsVec != 0 {
|
|
fbs.UserGamesCommandAddCommands(builder, commandsVec)
|
|
}
|
|
offset := fbs.UserGamesCommandEnd(builder)
|
|
fbs.FinishUserGamesCommandBuffer(builder, offset)
|
|
|
|
return builder.FinishedBytes(), nil
|
|
}
|
|
|
|
// PayloadToUserGamesCommand converts FlatBuffers payload bytes into
|
|
// model.UserGamesCommand.
|
|
func PayloadToUserGamesCommand(data []byte) (result *model.UserGamesCommand, err error) {
|
|
if len(data) == 0 {
|
|
return nil, errors.New("decode user games command payload: data is empty")
|
|
}
|
|
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
result = nil
|
|
err = fmt.Errorf("decode user games command payload: panic recovered: %v", recovered)
|
|
}
|
|
}()
|
|
|
|
flat := fbs.GetRootAsUserGamesCommand(data, 0)
|
|
gameID := flat.GameId(nil)
|
|
if gameID == nil {
|
|
return nil, errors.New("decode user games command payload: game_id is missing")
|
|
}
|
|
out := &model.UserGamesCommand{
|
|
GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
|
|
}
|
|
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 command %d: command item is missing", i)
|
|
}
|
|
cmd, decodeErr := decodeOrderCommand(flatCommand, i)
|
|
if decodeErr != nil {
|
|
return nil, decodeErr
|
|
}
|
|
out.Commands[i] = cmd
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// UserGamesOrderToPayload converts model.UserGamesOrder to FlatBuffers
|
|
// bytes suitable for the authenticated gateway transport.
|
|
func UserGamesOrderToPayload(req *model.UserGamesOrder) ([]byte, error) {
|
|
if req == nil {
|
|
return nil, errors.New("encode user games order payload: request is nil")
|
|
}
|
|
|
|
builder := flatbuffers.NewBuilder(1024)
|
|
commandsVec, err := encodeCommandItemVector(builder, req.Commands, "user games order")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fbs.UserGamesOrderStart(builder)
|
|
hi, lo := uuidToHiLo(req.GameID)
|
|
fbs.UserGamesOrderAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
|
|
fbs.UserGamesOrderAddUpdatedAt(builder, int64(req.UpdatedAt))
|
|
if commandsVec != 0 {
|
|
fbs.UserGamesOrderAddCommands(builder, commandsVec)
|
|
}
|
|
offset := fbs.UserGamesOrderEnd(builder)
|
|
fbs.FinishUserGamesOrderBuffer(builder, offset)
|
|
|
|
return builder.FinishedBytes(), nil
|
|
}
|
|
|
|
// PayloadToUserGamesOrder converts FlatBuffers payload bytes into
|
|
// model.UserGamesOrder.
|
|
func PayloadToUserGamesOrder(data []byte) (result *model.UserGamesOrder, err error) {
|
|
if len(data) == 0 {
|
|
return nil, errors.New("decode user games order payload: data is empty")
|
|
}
|
|
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
result = nil
|
|
err = fmt.Errorf("decode user games order payload: panic recovered: %v", recovered)
|
|
}
|
|
}()
|
|
|
|
flat := fbs.GetRootAsUserGamesOrder(data, 0)
|
|
gameID := flat.GameId(nil)
|
|
if gameID == nil {
|
|
return nil, errors.New("decode user games order payload: game_id is missing")
|
|
}
|
|
// updatedAt, convErr := int64ToInt(flat.UpdatedAt(), "updated_at")
|
|
// if convErr != nil {
|
|
// return nil, fmt.Errorf("decode user games order payload: %w", convErr)
|
|
// }
|
|
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 %d: command item is missing", i)
|
|
}
|
|
cmd, decodeErr := decodeOrderCommand(flatCommand, i)
|
|
if decodeErr != nil {
|
|
return nil, decodeErr
|
|
}
|
|
out.Commands[i] = cmd
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// EmptyUserGamesCommandResponsePayload returns a FlatBuffers-encoded
|
|
// empty `UserGamesCommandResponse` buffer. Used by gateway to ack a
|
|
// successful `MessageTypeUserGamesCommand` even though the engine
|
|
// returns 204 No Content — the typed envelope keeps the message-type
|
|
// contract symmetric with other authenticated routes.
|
|
func EmptyUserGamesCommandResponsePayload() []byte {
|
|
builder := flatbuffers.NewBuilder(16)
|
|
fbs.UserGamesCommandResponseStart(builder)
|
|
offset := fbs.UserGamesCommandResponseEnd(builder)
|
|
fbs.FinishUserGamesCommandResponseBuffer(builder, offset)
|
|
return builder.FinishedBytes()
|
|
}
|
|
|
|
// 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(), 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
|
|
// FlatBuffers vector of CommandItem. Used by UserGamesCommandToPayload
|
|
// and UserGamesOrderToPayload to keep the per-command encoding logic in
|
|
// one place.
|
|
func encodeCommandItemVector(builder *flatbuffers.Builder, commands []model.DecodableCommand, opLabel string) (flatbuffers.UOffsetT, error) {
|
|
offsets := make([]flatbuffers.UOffsetT, len(commands))
|
|
for i := range commands {
|
|
encoded, err := encodeOrderCommand(builder, commands[i], i)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("encode %s: %w", opLabel, err)
|
|
}
|
|
cmdID := builder.CreateString(encoded.cmdID)
|
|
fbs.CommandItemStart(builder)
|
|
fbs.CommandItemAddCmdId(builder, cmdID)
|
|
if encoded.cmdApplied != nil {
|
|
fbs.CommandItemAddCmdApplied(builder, *encoded.cmdApplied)
|
|
}
|
|
if encoded.cmdErrCode != nil {
|
|
fbs.CommandItemAddCmdErrorCode(builder, int64(*encoded.cmdErrCode))
|
|
}
|
|
fbs.CommandItemAddPayloadType(builder, encoded.payloadType)
|
|
fbs.CommandItemAddPayload(builder, encoded.payloadOffset)
|
|
offsets[i] = fbs.CommandItemEnd(builder)
|
|
}
|
|
if len(offsets) == 0 {
|
|
return 0, nil
|
|
}
|
|
// `UserGamesCommandStartCommandsVector` and the corresponding
|
|
// `UserGamesOrderStartCommandsVector` are identical helpers (both
|
|
// expand to `builder.StartVector(4, numElems, 4)`); we use the
|
|
// command flavour for both message types so the helper has a
|
|
// single dependency point.
|
|
fbs.UserGamesCommandStartCommandsVector(builder, len(offsets))
|
|
for i := len(offsets) - 1; i >= 0; i-- {
|
|
builder.PrependUOffsetT(offsets[i])
|
|
}
|
|
return builder.EndVector(len(offsets)), nil
|
|
}
|