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 }