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) }