flatbuffers & transcoders

This commit is contained in:
Ilia Denisov
2026-03-31 19:16:34 +02:00
committed by GitHub
parent 6e01d73a5e
commit f616e3f5ca
75 changed files with 10540 additions and 0 deletions
+384
View File
@@ -0,0 +1,384 @@
package transcoder
import (
"errors"
"fmt"
"sort"
model "galaxy/model/report"
fbs "galaxy/schema/fbs/battle"
flatbuffers "github.com/google/flatbuffers/go"
"github.com/google/uuid"
)
// BattleReportToPayload converts model.BattleReport from the internal
// representation to FlatBuffers bytes that can be sent over network
// transports.
//
// The function returns an error when the input is nil.
func BattleReportToPayload(report *model.BattleReport) ([]byte, error) {
if report == nil {
return nil, errors.New("encode battle report payload: report is nil")
}
builder := flatbuffers.NewBuilder(2048)
planetName := builder.CreateString(report.PlanetName)
raceOffsets := encodeBattleRaceEntryOffsets(builder, report.Races)
shipOffsets := encodeBattleShipEntryOffsets(builder, report.Ships)
protocolOffsets := encodeBattleActionOffsets(builder, report.Protocol)
raceVector := encodeBattleOffsetVector(builder, len(raceOffsets), fbs.BattleReportStartRacesVector, raceOffsets)
shipVector := encodeBattleOffsetVector(builder, len(shipOffsets), fbs.BattleReportStartShipsVector, shipOffsets)
protocolVector := encodeBattleOffsetVector(builder, len(protocolOffsets), fbs.BattleReportStartProtocolVector, protocolOffsets)
idHi, idLo := reportUUIDToHiLo(report.ID)
fbs.BattleReportStart(builder)
fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, idHi, idLo))
fbs.BattleReportAddPlanet(builder, uint64(report.Planet))
fbs.BattleReportAddPlanetName(builder, planetName)
if len(raceOffsets) > 0 {
fbs.BattleReportAddRaces(builder, raceVector)
}
if len(shipOffsets) > 0 {
fbs.BattleReportAddShips(builder, shipVector)
}
if len(protocolOffsets) > 0 {
fbs.BattleReportAddProtocol(builder, protocolVector)
}
payload := fbs.BattleReportEnd(builder)
fbs.FinishBattleReportBuffer(builder, payload)
return builder.FinishedBytes(), nil
}
// PayloadToBattleReport converts FlatBuffers payload bytes into
// model.BattleReport.
//
// The function validates payload structure and integer conversions.
// Malformed payloads are returned as errors.
func PayloadToBattleReport(data []byte) (result *model.BattleReport, err error) {
if len(data) == 0 {
return nil, errors.New("decode battle report payload: data is empty")
}
defer func() {
if recovered := recover(); recovered != nil {
result = nil
err = fmt.Errorf("decode battle report payload: panic recovered: %v", recovered)
}
}()
flatReport := fbs.GetRootAsBattleReport(data, 0)
id := flatReport.Id(nil)
if id == nil {
return nil, errors.New("decode battle report payload: id is missing")
}
planet, err := uint64ToUint(flatReport.Planet(), "planet")
if err != nil {
return nil, fmt.Errorf("decode battle report payload: %w", err)
}
result = &model.BattleReport{
ID: reportUUIDFromHiLo(id.Hi(), id.Lo()),
Planet: planet,
PlanetName: string(flatReport.PlanetName()),
}
if err := decodeBattleRaceMap(flatReport, result); err != nil {
return nil, err
}
if err := decodeBattleShipMap(flatReport, result); err != nil {
return nil, err
}
if err := decodeBattleProtocol(flatReport, result); err != nil {
return nil, err
}
return result, nil
}
func encodeBattleRaceEntryOffsets(builder *flatbuffers.Builder, races map[int]uuid.UUID) []flatbuffers.UOffsetT {
if len(races) == 0 {
return nil
}
keys := make([]int, 0, len(races))
for key := range races {
keys = append(keys, key)
}
sort.Ints(keys)
offsets := make([]flatbuffers.UOffsetT, len(keys))
for i, key := range keys {
hi, lo := reportUUIDToHiLo(races[key])
fbs.RaceEntryStart(builder)
fbs.RaceEntryAddKey(builder, int64(key))
fbs.RaceEntryAddValue(builder, fbs.CreateUUID(builder, hi, lo))
offsets[i] = fbs.RaceEntryEnd(builder)
}
return offsets
}
func encodeBattleShipEntryOffsets(builder *flatbuffers.Builder, ships map[int]model.BattleReportGroup) []flatbuffers.UOffsetT {
if len(ships) == 0 {
return nil
}
keys := make([]int, 0, len(ships))
for key := range ships {
keys = append(keys, key)
}
sort.Ints(keys)
offsets := make([]flatbuffers.UOffsetT, len(keys))
for i, key := range keys {
group := ships[key]
groupOffset := encodeBattleReportGroup(builder, &group)
fbs.ShipEntryStart(builder)
fbs.ShipEntryAddKey(builder, int64(key))
fbs.ShipEntryAddValue(builder, groupOffset)
offsets[i] = fbs.ShipEntryEnd(builder)
}
return offsets
}
func encodeBattleActionOffsets(builder *flatbuffers.Builder, protocol []model.BattleActionReport) []flatbuffers.UOffsetT {
if len(protocol) == 0 {
return nil
}
offsets := make([]flatbuffers.UOffsetT, len(protocol))
for i := range protocol {
item := &protocol[i]
fbs.BattleActionReportStart(builder)
fbs.BattleActionReportAddAttacker(builder, int64(item.Attacker))
fbs.BattleActionReportAddAttackerShipClass(builder, int64(item.AttackerShipClass))
fbs.BattleActionReportAddDefender(builder, int64(item.Defender))
fbs.BattleActionReportAddDefenderShipClass(builder, int64(item.DefenderShipClass))
fbs.BattleActionReportAddDestroyed(builder, item.Destroyed)
offsets[i] = fbs.BattleActionReportEnd(builder)
}
return offsets
}
func encodeBattleReportGroup(builder *flatbuffers.Builder, group *model.BattleReportGroup) flatbuffers.UOffsetT {
race := builder.CreateString(group.Race)
className := builder.CreateString(group.ClassName)
loadType := builder.CreateString(group.LoadType)
tech := encodeBattleTechEntryVector(builder, group.Tech)
fbs.BattleReportGroupStart(builder)
fbs.BattleReportGroupAddInBattle(builder, group.InBattle)
fbs.BattleReportGroupAddNumber(builder, uint64(group.Number))
fbs.BattleReportGroupAddNumberLeft(builder, uint64(group.NumberLeft))
fbs.BattleReportGroupAddLoadQuantity(builder, reportFloatToFBS(group.LoadQuantity))
if tech != 0 {
fbs.BattleReportGroupAddTech(builder, tech)
}
fbs.BattleReportGroupAddRace(builder, race)
fbs.BattleReportGroupAddClassName(builder, className)
fbs.BattleReportGroupAddLoadType(builder, loadType)
return fbs.BattleReportGroupEnd(builder)
}
func encodeBattleTechEntryVector(builder *flatbuffers.Builder, tech map[string]model.Float) flatbuffers.UOffsetT {
if len(tech) == 0 {
return 0
}
keys := make([]string, 0, len(tech))
for key := range tech {
keys = append(keys, key)
}
sort.Strings(keys)
offsets := make([]flatbuffers.UOffsetT, len(keys))
for i, key := range keys {
encodedKey := builder.CreateString(key)
fbs.TechEntryStart(builder)
fbs.TechEntryAddKey(builder, encodedKey)
fbs.TechEntryAddValue(builder, reportFloatToFBS(tech[key]))
offsets[i] = fbs.TechEntryEnd(builder)
}
fbs.BattleReportGroupStartTechVector(builder, len(offsets))
for i := len(offsets) - 1; i >= 0; i-- {
builder.PrependUOffsetT(offsets[i])
}
return builder.EndVector(len(offsets))
}
func decodeBattleRaceMap(flatReport *fbs.BattleReport, result *model.BattleReport) error {
length := flatReport.RacesLength()
if length == 0 {
return nil
}
result.Races = make(map[int]uuid.UUID, length)
item := new(fbs.RaceEntry)
for i := 0; i < length; i++ {
if !flatReport.Races(item, i) {
return fmt.Errorf("decode battle report race %d: race entry is missing", i)
}
key, err := int64ToInt(item.Key(), "race key")
if err != nil {
return fmt.Errorf("decode battle report race %d: %w", i, err)
}
value := item.Value(nil)
if value == nil {
return fmt.Errorf("decode battle report race %d: race value is missing", i)
}
result.Races[key] = reportUUIDFromHiLo(value.Hi(), value.Lo())
}
return nil
}
func decodeBattleShipMap(flatReport *fbs.BattleReport, result *model.BattleReport) error {
length := flatReport.ShipsLength()
if length == 0 {
return nil
}
result.Ships = make(map[int]model.BattleReportGroup, length)
item := new(fbs.ShipEntry)
for i := 0; i < length; i++ {
if !flatReport.Ships(item, i) {
return fmt.Errorf("decode battle report ship %d: ship entry is missing", i)
}
key, err := int64ToInt(item.Key(), "ship key")
if err != nil {
return fmt.Errorf("decode battle report ship %d: %w", i, err)
}
value := item.Value(nil)
if value == nil {
return fmt.Errorf("decode battle report ship %d: ship value is missing", i)
}
group, err := decodeBattleReportGroup(value, i)
if err != nil {
return err
}
result.Ships[key] = group
}
return nil
}
func decodeBattleProtocol(flatReport *fbs.BattleReport, result *model.BattleReport) error {
length := flatReport.ProtocolLength()
if length == 0 {
return nil
}
result.Protocol = make([]model.BattleActionReport, length)
item := new(fbs.BattleActionReport)
for i := 0; i < length; i++ {
if !flatReport.Protocol(item, i) {
return fmt.Errorf("decode battle report protocol %d: protocol entry is missing", i)
}
attacker, err := int64ToInt(item.Attacker(), "attacker")
if err != nil {
return fmt.Errorf("decode battle report protocol %d: %w", i, err)
}
attackerShipClass, err := int64ToInt(item.AttackerShipClass(), "attacker_ship_class")
if err != nil {
return fmt.Errorf("decode battle report protocol %d: %w", i, err)
}
defender, err := int64ToInt(item.Defender(), "defender")
if err != nil {
return fmt.Errorf("decode battle report protocol %d: %w", i, err)
}
defenderShipClass, err := int64ToInt(item.DefenderShipClass(), "defender_ship_class")
if err != nil {
return fmt.Errorf("decode battle report protocol %d: %w", i, err)
}
result.Protocol[i] = model.BattleActionReport{
Attacker: attacker,
AttackerShipClass: attackerShipClass,
Defender: defender,
DefenderShipClass: defenderShipClass,
Destroyed: item.Destroyed(),
}
}
return nil
}
func decodeBattleReportGroup(group *fbs.BattleReportGroup, shipIndex int) (model.BattleReportGroup, error) {
number, err := uint64ToUint(group.Number(), "number")
if err != nil {
return model.BattleReportGroup{}, fmt.Errorf("decode battle report ship %d: %w", shipIndex, err)
}
numberLeft, err := uint64ToUint(group.NumberLeft(), "number_left")
if err != nil {
return model.BattleReportGroup{}, fmt.Errorf("decode battle report ship %d: %w", shipIndex, err)
}
tech, err := decodeBattleTechMap(group, shipIndex)
if err != nil {
return model.BattleReportGroup{}, err
}
return model.BattleReportGroup{
InBattle: group.InBattle(),
Number: number,
NumberLeft: numberLeft,
LoadQuantity: reportFloatFromFBS(group.LoadQuantity()),
Tech: tech,
Race: string(group.Race()),
ClassName: string(group.ClassName()),
LoadType: string(group.LoadType()),
}, nil
}
func decodeBattleTechMap(group *fbs.BattleReportGroup, shipIndex int) (map[string]model.Float, error) {
length := group.TechLength()
if length == 0 {
return nil, nil
}
result := make(map[string]model.Float, length)
item := new(fbs.TechEntry)
for i := 0; i < length; i++ {
if !group.Tech(item, i) {
return nil, fmt.Errorf("decode battle report ship %d tech entry %d: tech entry is missing", shipIndex, i)
}
result[string(item.Key())] = reportFloatFromFBS(item.Value())
}
return result, nil
}
func encodeBattleOffsetVector(
builder *flatbuffers.Builder,
length int,
startVector func(*flatbuffers.Builder, int) flatbuffers.UOffsetT,
offsets []flatbuffers.UOffsetT,
) flatbuffers.UOffsetT {
if length == 0 {
return 0
}
startVector(builder, length)
for i := length - 1; i >= 0; i-- {
builder.PrependUOffsetT(offsets[i])
}
return builder.EndVector(length)
}
+275
View File
@@ -0,0 +1,275 @@
package transcoder
import (
"bytes"
"reflect"
"strconv"
"strings"
"testing"
model "galaxy/model/report"
fbs "galaxy/schema/fbs/battle"
flatbuffers "github.com/google/flatbuffers/go"
"github.com/google/uuid"
)
func TestBattleReportToPayloadAndPayloadToBattleReportRoundTrip(t *testing.T) {
t.Parallel()
source := sampleBattleReport()
payload, err := BattleReportToPayload(source)
if err != nil {
t.Fatalf("encode battle report payload: %v", err)
}
decoded, err := PayloadToBattleReport(payload)
if err != nil {
t.Fatalf("decode battle report payload: %v", err)
}
expected := battleReportWireClone(t, source)
if !reflect.DeepEqual(expected, decoded) {
t.Fatalf("round-trip mismatch\nexpected: %#v\ndecoded: %#v", expected, decoded)
}
}
func TestBattleReportToPayloadNilReport(t *testing.T) {
t.Parallel()
_, err := BattleReportToPayload(nil)
if err == nil {
t.Fatal("expected error for nil battle report")
}
}
func TestPayloadToBattleReportEmptyData(t *testing.T) {
t.Parallel()
_, err := PayloadToBattleReport(nil)
if err == nil {
t.Fatal("expected error for empty payload")
}
}
func TestPayloadToBattleReportGarbageDataDoesNotPanic(t *testing.T) {
t.Parallel()
_, err := PayloadToBattleReport([]byte{0x01, 0x02, 0x03})
if err == nil {
t.Fatal("expected error for malformed payload")
}
}
func TestPayloadToBattleReportMissingID(t *testing.T) {
t.Parallel()
payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
fbs.BattleReportStart(builder)
fbs.BattleReportAddPlanet(builder, 7)
return fbs.BattleReportEnd(builder)
})
_, err := PayloadToBattleReport(payload)
if err == nil {
t.Fatal("expected error for missing battle report id")
}
if !strings.Contains(err.Error(), "id is missing") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToBattleReportMissingRaceValue(t *testing.T) {
t.Parallel()
payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
fbs.RaceEntryStart(builder)
fbs.RaceEntryAddKey(builder, 1)
race := fbs.RaceEntryEnd(builder)
fbs.BattleReportStartRacesVector(builder, 1)
builder.PrependUOffsetT(race)
races := builder.EndVector(1)
fbs.BattleReportStart(builder)
fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2))
fbs.BattleReportAddRaces(builder, races)
return fbs.BattleReportEnd(builder)
})
_, err := PayloadToBattleReport(payload)
if err == nil {
t.Fatal("expected error for missing race value")
}
if !strings.Contains(err.Error(), "race value is missing") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToBattleReportMissingShipValue(t *testing.T) {
t.Parallel()
payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
fbs.ShipEntryStart(builder)
fbs.ShipEntryAddKey(builder, 1)
ship := fbs.ShipEntryEnd(builder)
fbs.BattleReportStartShipsVector(builder, 1)
builder.PrependUOffsetT(ship)
ships := builder.EndVector(1)
fbs.BattleReportStart(builder)
fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2))
fbs.BattleReportAddShips(builder, ships)
return fbs.BattleReportEnd(builder)
})
_, err := PayloadToBattleReport(payload)
if err == nil {
t.Fatal("expected error for missing ship value")
}
if !strings.Contains(err.Error(), "ship value is missing") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToBattleReportOverflowInt(t *testing.T) {
t.Parallel()
if strconv.IntSize == 64 {
t.Skip("int overflow from int64 is not possible on 64-bit runtime")
}
maxInt := int(^uint(0) >> 1)
overflowValue := int64(maxInt) + 1
payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
fbs.BattleActionReportStart(builder)
fbs.BattleActionReportAddAttacker(builder, overflowValue)
protocol := fbs.BattleActionReportEnd(builder)
fbs.BattleReportStartProtocolVector(builder, 1)
builder.PrependUOffsetT(protocol)
protocolVector := builder.EndVector(1)
fbs.BattleReportStart(builder)
fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2))
fbs.BattleReportAddProtocol(builder, protocolVector)
return fbs.BattleReportEnd(builder)
})
_, err := PayloadToBattleReport(payload)
if err == nil {
t.Fatal("expected overflow error")
}
if !strings.Contains(err.Error(), "overflows int") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToBattleReportOverflowUint(t *testing.T) {
t.Parallel()
if strconv.IntSize == 64 {
t.Skip("uint overflow from uint64 is not possible on 64-bit runtime")
}
maxUint := uint64(^uint(0))
overflowValue := maxUint + 1
payload := buildBattleReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
fbs.BattleReportStart(builder)
fbs.BattleReportAddId(builder, fbs.CreateUUID(builder, 1, 2))
fbs.BattleReportAddPlanet(builder, overflowValue)
return fbs.BattleReportEnd(builder)
})
_, err := PayloadToBattleReport(payload)
if err == nil {
t.Fatal("expected overflow error")
}
if !strings.Contains(err.Error(), "overflows uint") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestBattleReportToPayloadDeterministicMapEncoding(t *testing.T) {
t.Parallel()
report := sampleBattleReport()
firstPayload, err := BattleReportToPayload(report)
if err != nil {
t.Fatalf("encode battle report payload: %v", err)
}
for i := 0; i < 20; i++ {
nextPayload, nextErr := BattleReportToPayload(report)
if nextErr != nil {
t.Fatalf("encode battle report payload #%d: %v", i+2, nextErr)
}
if !bytes.Equal(firstPayload, nextPayload) {
t.Fatalf("payload differs between runs at iteration %d", i+2)
}
}
}
func sampleBattleReport() *model.BattleReport {
return &model.BattleReport{
ID: uuid.MustParse("44444444-4444-4444-4444-444444444444"),
Planet: 12,
PlanetName: "Nexus",
Races: map[int]uuid.UUID{
2: uuid.MustParse("55555555-5555-5555-5555-555555555555"),
1: uuid.MustParse("66666666-6666-6666-6666-666666666666"),
},
Ships: map[int]model.BattleReportGroup{
3: {
InBattle: true,
Number: 20,
NumberLeft: 7,
LoadQuantity: model.Float(3.5),
Tech: map[string]model.Float{"WEAPONS": model.Float(2.0), "DRIVE": model.Float(1.5)},
Race: "Terrans",
ClassName: "Frigate",
LoadType: "MAT",
},
1: {
InBattle: false,
Number: 15,
NumberLeft: 15,
LoadQuantity: model.Float(0.0),
Tech: map[string]model.Float{"CARGO": model.Float(1.25), "SHIELDS": model.Float(1.75)},
Race: "Martians",
ClassName: "Destroyer",
LoadType: "CAP",
},
},
Protocol: []model.BattleActionReport{
{Attacker: 1, AttackerShipClass: 3, Defender: 2, DefenderShipClass: 1, Destroyed: true},
{Attacker: 2, AttackerShipClass: 1, Defender: 1, DefenderShipClass: 3, Destroyed: false},
},
}
}
func battleReportWireClone(t *testing.T, source *model.BattleReport) *model.BattleReport {
t.Helper()
data, err := source.MarshalBinary()
if err != nil {
t.Fatalf("marshal source battle report: %v", err)
}
result := new(model.BattleReport)
if err := result.UnmarshalBinary(data); err != nil {
t.Fatalf("unmarshal source battle report clone: %v", err)
}
return result
}
func buildBattleReportPayload(build func(*flatbuffers.Builder) flatbuffers.UOffsetT) []byte {
builder := flatbuffers.NewBuilder(256)
offset := build(builder)
fbs.FinishBattleReportBuffer(builder, offset)
return builder.FinishedBytes()
}
+8
View File
@@ -0,0 +1,8 @@
module galaxy/transcoder
go 1.26.0
require (
github.com/google/flatbuffers v25.12.19+incompatible
github.com/google/uuid v1.6.0
)
+2
View File
@@ -0,0 +1,2 @@
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+899
View File
@@ -0,0 +1,899 @@
package transcoder
import (
"errors"
"fmt"
model "galaxy/model/order"
fbs "galaxy/schema/fbs/order"
flatbuffers "github.com/google/flatbuffers/go"
)
// OrderToPayload converts model.Order from the internal representation to
// FlatBuffers bytes that can be sent over network transports.
//
// The function returns an error when the input contains unsupported command
// types or values that cannot be represented by the current FlatBuffers schema.
func OrderToPayload(o *model.Order) ([]byte, error) {
if o == nil {
return nil, errors.New("encode order payload: order is nil")
}
builder := flatbuffers.NewBuilder(1024)
commandOffsets := make([]flatbuffers.UOffsetT, len(o.Commands))
for i := range o.Commands {
encoded, err := encodeOrderCommand(builder, o.Commands[i], i)
if err != nil {
return nil, 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)
commandOffsets[i] = fbs.CommandItemEnd(builder)
}
var commandsVector flatbuffers.UOffsetT
if len(commandOffsets) > 0 {
fbs.OrderStartCommandsVector(builder, len(commandOffsets))
for i := len(commandOffsets) - 1; i >= 0; i-- {
builder.PrependUOffsetT(commandOffsets[i])
}
commandsVector = builder.EndVector(len(commandOffsets))
}
fbs.OrderStart(builder)
fbs.OrderAddUpdatedAt(builder, int64(o.UpdatedAt))
if len(commandOffsets) > 0 {
fbs.OrderAddCommands(builder, commandsVector)
}
orderOffset := fbs.OrderEnd(builder)
fbs.FinishOrderBuffer(builder, orderOffset)
return builder.FinishedBytes(), nil
}
// PayloadToOrder converts FlatBuffers payload bytes into model.Order.
//
// The function validates payload structure, command payload type, enum values,
// and integer conversions. Malformed payloads are returned as errors.
func PayloadToOrder(data []byte) (result *model.Order, err error) {
if len(data) == 0 {
return nil, errors.New("decode order payload: data is empty")
}
defer func() {
if recovered := recover(); recovered != nil {
result = nil
err = fmt.Errorf("decode order payload: panic recovered: %v", recovered)
}
}()
flatOrder := fbs.GetRootAsOrder(data, 0)
updatedAt, err := int64ToInt(flatOrder.UpdatedAt(), "updated_at")
if err != nil {
return nil, fmt.Errorf("decode order payload: %w", err)
}
result = &model.Order{UpdatedAt: updatedAt}
commandsLen := flatOrder.CommandsLength()
if commandsLen > 0 {
result.Commands = make([]model.DecodableCommand, commandsLen)
}
flatCommand := new(fbs.CommandItem)
for i := 0; i < commandsLen; i++ {
if !flatOrder.Commands(flatCommand, i) {
return nil, fmt.Errorf("decode order command %d: command item is missing", i)
}
command, err := decodeOrderCommand(flatCommand, i)
if err != nil {
return nil, err
}
result.Commands[i] = command
}
return result, 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)
}
}
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
}
+333
View File
@@ -0,0 +1,333 @@
package transcoder
import (
"reflect"
"strconv"
"strings"
"testing"
model "galaxy/model/order"
fbs "galaxy/schema/fbs/order"
flatbuffers "github.com/google/flatbuffers/go"
)
func TestOrderToPayloadAndPayloadToOrderRoundTrip(t *testing.T) {
t.Parallel()
appliedTrue := true
appliedFalse := false
errZero := 0
errThree := 3
errSeven := 7
source := &model.Order{
UpdatedAt: 42,
Commands: []model.DecodableCommand{
&model.CommandRaceQuit{CommandMeta: commandMeta("cmd-01", model.CommandTypeRaceQuit, &appliedTrue, &errZero)},
&model.CommandRaceVote{CommandMeta: commandMeta("cmd-02", model.CommandTypeRaceVote, nil, nil), Acceptor: "race-a"},
&model.CommandRaceRelation{CommandMeta: commandMeta("cmd-03", model.CommandTypeRaceRelation, &appliedFalse, nil), Acceptor: "race-b", Relation: "WAR"},
&model.CommandShipClassCreate{CommandMeta: commandMeta("cmd-04", model.CommandTypeShipClassCreate, nil, &errThree), Name: "frigate", Drive: 1.5, Armament: 5, Weapons: 2.5, Shields: 3.5, Cargo: 4.5},
&model.CommandShipClassMerge{CommandMeta: commandMeta("cmd-05", model.CommandTypeShipClassMerge, nil, nil), Name: "alpha", Target: "beta"},
&model.CommandShipClassRemove{CommandMeta: commandMeta("cmd-06", model.CommandTypeShipClassRemove, nil, nil), Name: "obsolete"},
&model.CommandShipGroupBreak{CommandMeta: commandMeta("cmd-07", model.CommandTypeShipGroupBreak, nil, nil), ID: "group-1", NewID: "group-2", Quantity: 12},
&model.CommandShipGroupLoad{CommandMeta: commandMeta("cmd-08", model.CommandTypeShipGroupLoad, nil, nil), ID: "group-3", Cargo: "MAT", Quantity: 7.25},
&model.CommandShipGroupUnload{CommandMeta: commandMeta("cmd-09", model.CommandTypeShipGroupUnload, nil, nil), ID: "group-4", Quantity: 1.75},
&model.CommandShipGroupSend{CommandMeta: commandMeta("cmd-10", model.CommandTypeShipGroupSend, nil, nil), ID: "group-5", Destination: 19},
&model.CommandShipGroupUpgrade{CommandMeta: commandMeta("cmd-11", model.CommandTypeShipGroupUpgrade, nil, nil), ID: "group-6", Tech: "SHIELDS", Level: 2.0},
&model.CommandShipGroupMerge{CommandMeta: commandMeta("cmd-12", model.CommandTypeShipGroupMerge, nil, nil)},
&model.CommandShipGroupDismantle{CommandMeta: commandMeta("cmd-13", model.CommandTypeShipGroupDismantle, nil, nil), ID: "group-7"},
&model.CommandShipGroupTransfer{CommandMeta: commandMeta("cmd-14", model.CommandTypeShipGroupTransfer, nil, &errSeven), ID: "group-8", Acceptor: "race-c"},
&model.CommandShipGroupJoinFleet{CommandMeta: commandMeta("cmd-15", model.CommandTypeShipGroupJoinFleet, nil, nil), ID: "group-9", Name: "fleet-a"},
&model.CommandFleetMerge{CommandMeta: commandMeta("cmd-16", model.CommandTypeFleetMerge, nil, nil), Name: "fleet-b", Target: "fleet-c"},
&model.CommandFleetSend{CommandMeta: commandMeta("cmd-17", model.CommandTypeFleetSend, nil, nil), Name: "fleet-d", Destination: 31},
&model.CommandScienceCreate{CommandMeta: commandMeta("cmd-18", model.CommandTypeScienceCreate, nil, nil), Name: "science-a", Drive: 0.1, Weapons: 0.2, Shields: 0.3, Cargo: 0.4},
&model.CommandScienceRemove{CommandMeta: commandMeta("cmd-19", model.CommandTypeScienceRemove, nil, nil), Name: "science-b"},
&model.CommandPlanetRename{CommandMeta: commandMeta("cmd-20", model.CommandTypePlanetRename, nil, nil), Number: 7, Name: "new-name"},
&model.CommandPlanetProduce{CommandMeta: commandMeta("cmd-21", model.CommandTypePlanetProduce, nil, nil), Number: 8, Production: "SHIP", Subject: "frigate"},
&model.CommandPlanetRouteSet{CommandMeta: commandMeta("cmd-22", model.CommandTypePlanetRouteSet, nil, nil), Origin: 9, Destination: 10, LoadType: "EMP"},
&model.CommandPlanetRouteRemove{CommandMeta: commandMeta("cmd-23", model.CommandTypePlanetRouteRemove, nil, nil), Origin: 11, LoadType: "COL"},
},
}
payload, err := OrderToPayload(source)
if err != nil {
t.Fatalf("encode order payload: %v", err)
}
decoded, err := PayloadToOrder(payload)
if err != nil {
t.Fatalf("decode order payload: %v", err)
}
if !reflect.DeepEqual(source, decoded) {
t.Fatalf("round-trip mismatch\nsource: %#v\ndecoded:%#v", source, decoded)
}
}
func TestOrderToPayloadNilOrder(t *testing.T) {
t.Parallel()
_, err := OrderToPayload(nil)
if err == nil {
t.Fatal("expected error for nil order")
}
}
func TestOrderToPayloadUnsupportedCommandType(t *testing.T) {
t.Parallel()
source := &model.Order{
Commands: []model.DecodableCommand{unsupportedCommand{}},
}
_, err := OrderToPayload(source)
if err == nil {
t.Fatal("expected error for unsupported command type")
}
if !strings.Contains(err.Error(), "unsupported command type") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestOrderToPayloadTypedNilCommand(t *testing.T) {
t.Parallel()
var typedNil *model.CommandRaceQuit
source := &model.Order{
Commands: []model.DecodableCommand{typedNil},
}
_, err := OrderToPayload(source)
if err == nil {
t.Fatal("expected error for typed nil command")
}
if !strings.Contains(err.Error(), "command is nil") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestOrderToPayloadInvalidEnum(t *testing.T) {
t.Parallel()
source := &model.Order{
Commands: []model.DecodableCommand{
&model.CommandRaceRelation{
CommandMeta: commandMeta("cmd-1", model.CommandTypeRaceRelation, nil, nil),
Acceptor: "race-a",
Relation: "ALLY",
},
},
}
_, err := OrderToPayload(source)
if err == nil {
t.Fatal("expected error for invalid enum value")
}
if !strings.Contains(err.Error(), "unsupported relation value") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToOrderEmptyData(t *testing.T) {
t.Parallel()
_, err := PayloadToOrder(nil)
if err == nil {
t.Fatal("expected error for empty payload")
}
}
func TestPayloadToOrderGarbageDataDoesNotPanic(t *testing.T) {
t.Parallel()
_, err := PayloadToOrder([]byte{0x01, 0x02, 0x03})
if err == nil {
t.Fatal("expected error for malformed payload")
}
}
func TestPayloadToOrderUnknownPayloadType(t *testing.T) {
t.Parallel()
payload := buildSingleCommandOrderPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
fbs.CommandRaceQuitStart(builder)
commandPayload := fbs.CommandRaceQuitEnd(builder)
cmdID := builder.CreateString("cmd-1")
fbs.CommandItemStart(builder)
fbs.CommandItemAddCmdId(builder, cmdID)
fbs.CommandItemAddPayloadType(builder, fbs.CommandPayload(127))
fbs.CommandItemAddPayload(builder, commandPayload)
return fbs.CommandItemEnd(builder)
})
_, err := PayloadToOrder(payload)
if err == nil {
t.Fatal("expected error for unknown payload type")
}
if !strings.Contains(err.Error(), "unknown command payload type") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToOrderMissingPayload(t *testing.T) {
t.Parallel()
payload := buildSingleCommandOrderPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
cmdID := builder.CreateString("cmd-1")
fbs.CommandItemStart(builder)
fbs.CommandItemAddCmdId(builder, cmdID)
fbs.CommandItemAddPayloadType(builder, fbs.CommandPayloadCommandRaceQuit)
return fbs.CommandItemEnd(builder)
})
_, err := PayloadToOrder(payload)
if err == nil {
t.Fatal("expected error for missing payload")
}
if !strings.Contains(err.Error(), "payload is missing") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToOrderPayloadTypeNone(t *testing.T) {
t.Parallel()
payload := buildSingleCommandOrderPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
fbs.CommandRaceQuitStart(builder)
commandPayload := fbs.CommandRaceQuitEnd(builder)
cmdID := builder.CreateString("cmd-1")
fbs.CommandItemStart(builder)
fbs.CommandItemAddCmdId(builder, cmdID)
fbs.CommandItemAddPayload(builder, commandPayload)
return fbs.CommandItemEnd(builder)
})
_, err := PayloadToOrder(payload)
if err == nil {
t.Fatal("expected error for NONE payload type")
}
if !strings.Contains(err.Error(), "payload type is NONE") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToOrderUnknownEnum(t *testing.T) {
t.Parallel()
payload := buildSingleCommandOrderPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
acceptor := builder.CreateString("race-a")
fbs.CommandRaceRelationStart(builder)
fbs.CommandRaceRelationAddAcceptor(builder, acceptor)
fbs.CommandRaceRelationAddRelation(builder, fbs.RelationUNKNOWN)
commandPayload := fbs.CommandRaceRelationEnd(builder)
cmdID := builder.CreateString("cmd-1")
fbs.CommandItemStart(builder)
fbs.CommandItemAddCmdId(builder, cmdID)
fbs.CommandItemAddPayloadType(builder, fbs.CommandPayloadCommandRaceRelation)
fbs.CommandItemAddPayload(builder, commandPayload)
return fbs.CommandItemEnd(builder)
})
_, err := PayloadToOrder(payload)
if err == nil {
t.Fatal("expected error for UNKNOWN enum")
}
if !strings.Contains(err.Error(), "UNKNOWN") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToOrderOverflow(t *testing.T) {
t.Parallel()
if strconv.IntSize == 64 {
t.Skip("int overflow from int64 is not possible on 64-bit runtime")
}
maxInt := int(^uint(0) >> 1)
overflowValue := int64(maxInt) + 1
payload := buildSingleCommandOrderPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
name := builder.CreateString("planet-a")
fbs.CommandPlanetRenameStart(builder)
fbs.CommandPlanetRenameAddNumber(builder, overflowValue)
fbs.CommandPlanetRenameAddName(builder, name)
commandPayload := fbs.CommandPlanetRenameEnd(builder)
cmdID := builder.CreateString("cmd-1")
fbs.CommandItemStart(builder)
fbs.CommandItemAddCmdId(builder, cmdID)
fbs.CommandItemAddPayloadType(builder, fbs.CommandPayloadCommandPlanetRename)
fbs.CommandItemAddPayload(builder, commandPayload)
return fbs.CommandItemEnd(builder)
})
_, err := PayloadToOrder(payload)
if err == nil {
t.Fatal("expected overflow error")
}
if !strings.Contains(err.Error(), "overflows int") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestInt64ToInt(t *testing.T) {
t.Parallel()
value, err := int64ToInt(123, "field")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if value != 123 {
t.Fatalf("unexpected int value: %d", value)
}
if strconv.IntSize == 32 {
maxInt := int(^uint(0) >> 1)
_, err = int64ToInt(int64(maxInt)+1, "field")
if err == nil {
t.Fatal("expected overflow error")
}
}
}
type unsupportedCommand struct{}
func (unsupportedCommand) CommandID() string {
return "unsupported"
}
func (unsupportedCommand) CommandType() model.CommandType {
return model.CommandType("unsupported")
}
func commandMeta(id string, cmdType model.CommandType, applied *bool, errCode *int) model.CommandMeta {
return model.CommandMeta{
CmdType: cmdType,
CmdID: id,
CmdApplied: applied,
CmdErrCode: errCode,
}
}
func buildSingleCommandOrderPayload(itemBuilder func(*flatbuffers.Builder) flatbuffers.UOffsetT) []byte {
builder := flatbuffers.NewBuilder(256)
itemOffset := itemBuilder(builder)
fbs.OrderStartCommandsVector(builder, 1)
builder.PrependUOffsetT(itemOffset)
commands := builder.EndVector(1)
fbs.OrderStart(builder)
fbs.OrderAddUpdatedAt(builder, 1)
fbs.OrderAddCommands(builder, commands)
orderOffset := fbs.OrderEnd(builder)
fbs.FinishOrderBuffer(builder, orderOffset)
return builder.FinishedBytes()
}
File diff suppressed because it is too large Load Diff
+355
View File
@@ -0,0 +1,355 @@
package transcoder
import (
"bytes"
"reflect"
"strconv"
"strings"
"testing"
model "galaxy/model/report"
fbs "galaxy/schema/fbs/report"
flatbuffers "github.com/google/flatbuffers/go"
"github.com/google/uuid"
)
func TestReportToPayloadAndPayloadToReportRoundTrip(t *testing.T) {
t.Parallel()
source := sampleReport()
payload, err := ReportToPayload(source)
if err != nil {
t.Fatalf("encode report payload: %v", err)
}
decoded, err := PayloadToReport(payload)
if err != nil {
t.Fatalf("decode report payload: %v", err)
}
expected := reportWireClone(t, source)
if !reflect.DeepEqual(expected, decoded) {
t.Fatalf("round-trip mismatch\nexpected: %#v\ndecoded: %#v", expected, decoded)
}
}
func TestReportToPayloadNilReport(t *testing.T) {
t.Parallel()
_, err := ReportToPayload(nil)
if err == nil {
t.Fatal("expected error for nil report")
}
}
func TestPayloadToReportEmptyData(t *testing.T) {
t.Parallel()
_, err := PayloadToReport(nil)
if err == nil {
t.Fatal("expected error for empty payload")
}
}
func TestPayloadToReportGarbageDataDoesNotPanic(t *testing.T) {
t.Parallel()
_, err := PayloadToReport([]byte{0x01, 0x02, 0x03})
if err == nil {
t.Fatal("expected error for malformed payload")
}
}
func TestPayloadToReportMissingRequiredLocalGroupID(t *testing.T) {
t.Parallel()
payload := buildReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
class := builder.CreateString("frigate")
state := builder.CreateString("orbit")
fbs.LocalGroupStart(builder)
fbs.LocalGroupAddClass(builder, class)
fbs.LocalGroupAddState(builder, state)
localGroup := fbs.LocalGroupEnd(builder)
fbs.ReportStartLocalGroupVector(builder, 1)
builder.PrependUOffsetT(localGroup)
localGroups := builder.EndVector(1)
fbs.ReportStart(builder)
fbs.ReportAddLocalGroup(builder, localGroups)
return fbs.ReportEnd(builder)
})
_, err := PayloadToReport(payload)
if err == nil {
t.Fatal("expected error for missing local group id")
}
if !strings.Contains(err.Error(), "id is missing") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPayloadToReportOverflow(t *testing.T) {
t.Parallel()
if strconv.IntSize == 64 {
t.Skip("uint overflow from uint64 is not possible on 64-bit runtime")
}
maxUint := uint64(^uint(0))
overflowValue := maxUint + 1
payload := buildReportPayload(func(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
fbs.ReportStart(builder)
fbs.ReportAddTurn(builder, overflowValue)
return fbs.ReportEnd(builder)
})
_, err := PayloadToReport(payload)
if err == nil {
t.Fatal("expected overflow error")
}
if !strings.Contains(err.Error(), "overflows uint") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestReportToPayloadDeterministicMapEncoding(t *testing.T) {
t.Parallel()
report := sampleReport()
firstPayload, err := ReportToPayload(report)
if err != nil {
t.Fatalf("encode report payload: %v", err)
}
for i := 0; i < 20; i++ {
nextPayload, nextErr := ReportToPayload(report)
if nextErr != nil {
t.Fatalf("encode report payload #%d: %v", i+2, nextErr)
}
if !bytes.Equal(firstPayload, nextPayload) {
t.Fatalf("payload differs between runs at iteration %d", i+2)
}
}
}
func TestReportToPayloadFloat32Quantization(t *testing.T) {
t.Parallel()
source := &model.Report{
Race: "Terrans",
Votes: model.Float(0.123456789),
LocalScience: []model.Science{
{
Name: "science-alpha",
Drive: model.Float(0.123456789),
},
},
}
payload, err := ReportToPayload(source)
if err != nil {
t.Fatalf("encode report payload: %v", err)
}
decoded, err := PayloadToReport(payload)
if err != nil {
t.Fatalf("decode report payload: %v", err)
}
wantVotes := model.Float(float64(float32(source.Votes.F())))
if decoded.Votes != wantVotes {
t.Fatalf("unexpected votes value: got=%v want=%v", decoded.Votes, wantVotes)
}
wantDrive := model.Float(float64(float32(source.LocalScience[0].Drive.F())))
if decoded.LocalScience[0].Drive != wantDrive {
t.Fatalf("unexpected drive value: got=%v want=%v", decoded.LocalScience[0].Drive, wantDrive)
}
if decoded.Votes == source.Votes {
t.Fatal("expected quantization for votes value")
}
}
func sampleReport() *model.Report {
originA := uint(11)
originB := uint(17)
rangeA := model.Float(6.5)
rangeB := model.Float(3.5)
fleetName := "Fleet-1"
return &model.Report{
Version: 2,
Turn: 7,
Width: 64,
Height: 48,
PlanetCount: 21,
Race: "Terrans",
RaceID: uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
Votes: model.Float(7.5),
VoteFor: "Martians",
Player: []model.Player{
{
ID: uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
Name: "Terrans",
Drive: model.Float(1.5),
Weapons: model.Float(2.0),
Shields: model.Float(2.5),
Cargo: model.Float(3.0),
Population: model.Float(120.0),
Industry: model.Float(90.0),
Planets: 5,
Relation: "-",
Votes: model.Float(7.5),
Extinct: false,
},
},
LocalScience: []model.Science{
{Name: "fusion", Drive: model.Float(1.25), Weapons: model.Float(1.5), Shields: model.Float(1.75), Cargo: model.Float(2.0)},
},
OtherScience: []model.OtherScience{
{Race: "Martians", Science: model.Science{Name: "warp", Drive: model.Float(2.0), Weapons: model.Float(2.25), Shields: model.Float(2.5), Cargo: model.Float(2.75)}},
},
LocalShipClass: []model.ShipClass{
{Name: "frigate", Drive: model.Float(1.5), Armament: 4, Weapons: model.Float(2.0), Shields: model.Float(2.5), Cargo: model.Float(3.0), Mass: model.Float(9.5)},
},
OtherShipClass: []model.OthersShipClass{
{Race: "Martians", ShipClass: model.ShipClass{Name: "destroyer", Drive: model.Float(1.75), Armament: 6, Weapons: model.Float(2.25), Shields: model.Float(2.75), Cargo: model.Float(3.25), Mass: model.Float(10.5)}},
},
Battle: []uuid.UUID{
uuid.MustParse("11111111-1111-1111-1111-111111111111"),
uuid.MustParse("22222222-2222-2222-2222-222222222222"),
},
Bombing: []*model.Bombing{
{
PlanetOwnedID: uuid.MustParse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
Number: 9,
Planet: "Nova",
Owner: "Terrans",
Attacker: "Martians",
Production: "SHIP",
Industry: model.Float(10.5),
Population: model.Float(8.5),
Colonists: model.Float(7.5),
Capital: model.Float(6.5),
Material: model.Float(5.5),
AttackPower: model.Float(4.5),
Wiped: false,
},
},
IncomingGroup: []model.IncomingGroup{
{Origin: 1, Destination: 2, Distance: model.Float(10.0), Speed: model.Float(2.0), Mass: model.Float(20.0)},
},
LocalPlanet: []model.LocalPlanet{
{
UninhabitedPlanet: model.UninhabitedPlanet{
UnidentifiedPlanet: model.UnidentifiedPlanet{X: model.Float(1.0), Y: model.Float(2.0), Number: 3},
Size: model.Float(50.0),
Name: "Terra",
Resources: model.Float(7.0),
Capital: model.Float(8.0),
Material: model.Float(9.0),
},
Industry: model.Float(10.0),
Population: model.Float(11.0),
Colonists: model.Float(12.0),
Production: "SHIP",
FreeIndustry: model.Float(13.0),
},
},
ShipProduction: []model.ShipProduction{
{Planet: 3, Class: "frigate", Cost: model.Float(4.0), ProdUsed: model.Float(2.0), Percent: model.Float(50.0), Free: model.Float(6.0)},
},
Route: []model.Route{
{Planet: 3, Route: map[uint]string{9: "MAT", 2: "CAP", 5: "EMP"}},
},
OtherPlanet: []model.OtherPlanet{
{
Owner: "Martians",
LocalPlanet: model.LocalPlanet{
UninhabitedPlanet: model.UninhabitedPlanet{
UnidentifiedPlanet: model.UnidentifiedPlanet{X: model.Float(4.0), Y: model.Float(5.0), Number: 6},
Size: model.Float(40.0),
Name: "Ares",
Resources: model.Float(6.0),
Capital: model.Float(7.0),
Material: model.Float(8.0),
},
Industry: model.Float(9.0),
Population: model.Float(10.0),
Colonists: model.Float(11.0),
Production: "MAT",
FreeIndustry: model.Float(12.0),
},
},
},
UninhabitedPlanet: []model.UninhabitedPlanet{
{UnidentifiedPlanet: model.UnidentifiedPlanet{X: model.Float(6.0), Y: model.Float(7.0), Number: 8}, Size: model.Float(30.0), Name: "Nadir", Resources: model.Float(5.0), Capital: model.Float(4.0), Material: model.Float(3.0)},
},
UnidentifiedPlanet: []model.UnidentifiedPlanet{
{X: model.Float(8.0), Y: model.Float(9.0), Number: 10},
},
LocalFleet: []model.LocalFleet{
{Name: "Fleet-1", Groups: 2, Destination: 4, Origin: &originA, Range: &rangeA, Speed: model.Float(2.0), State: "moving"},
},
LocalGroup: []model.LocalGroup{
{
OtherGroup: model.OtherGroup{
Number: 1,
Class: "frigate",
Tech: map[string]model.Float{"WEAPONS": model.Float(2.0), "DRIVE": model.Float(1.5), "SHIELDS": model.Float(1.75)},
Cargo: "MAT",
Load: model.Float(4.0),
Destination: 4,
Origin: &originB,
Range: &rangeB,
Speed: model.Float(2.5),
Mass: model.Float(12.0),
},
ID: uuid.MustParse("33333333-3333-3333-3333-333333333333"),
State: "in_orbit",
Fleet: &fleetName,
},
},
OtherGroup: []model.OtherGroup{
{Number: 2, Class: "scout", Tech: map[string]model.Float{"CARGO": model.Float(1.25), "DRIVE": model.Float(1.75)}, Cargo: "CAP", Load: model.Float(3.5), Destination: 5, Speed: model.Float(2.25), Mass: model.Float(8.5)},
},
UnidentifiedGroup: []model.UnidentifiedGroup{
{X: model.Float(10.0), Y: model.Float(11.0)},
},
OnPlanetGroupCache: map[uint][]int{
1: {2, 3},
},
InSpaceGroupRangeCache: map[int]map[uint]float64{
1: {4: 12.5},
},
}
}
func reportWireClone(t *testing.T, source *model.Report) *model.Report {
t.Helper()
data, err := source.MarshalBinary()
if err != nil {
t.Fatalf("marshal source report: %v", err)
}
result := new(model.Report)
if err := result.UnmarshalBinary(data); err != nil {
t.Fatalf("unmarshal source report clone: %v", err)
}
return result
}
func buildReportPayload(build func(*flatbuffers.Builder) flatbuffers.UOffsetT) []byte {
builder := flatbuffers.NewBuilder(256)
offset := build(builder)
fbs.FinishReportBuffer(builder, offset)
return builder.FinishedBytes()
}