package repo /* /state.json /0001/state.json /0001/meta.json /0000/order/{UUID}.json /0001/bombing.json /0001/battle/{UUID}.json /0001/report/{UUID}.json */ import ( "encoding/json" "fmt" "galaxy/model/order" "galaxy/model/report" "galaxy/game/internal/model/game" "github.com/google/uuid" ) const ( statePath = "state.json" metaPath = "meta.json" ) type storedOrder struct { GameID uuid.UUID `json:"game_id"` UpdatedAt int64 `json:"updatedAt"` Commands []json.RawMessage `json:"cmd"` } func (o storedOrder) MarshalBinary() (data []byte, err error) { return json.Marshal(&o) } func (o *storedOrder) UnmarshalBinary(data []byte) error { return json.Unmarshal(data, o) } func (r *repo) SaveNewTurn(t uint, g *game.Game) error { return saveNewTurn(r.s, t, g) } func saveNewTurn(s Storage, t uint, g *game.Game) error { path := fmt.Sprintf("%s/state.json", TurnDir(t)) exist, err := s.Exists(path) if err != nil { return NewStorageError(err) } if exist { return NewStateError(fmt.Sprintf("turn %d already saved at %s", t, path)) } if err := s.Write(path, g); err != nil { return NewStorageError(err) } return saveLastState(s, g) } func (r *repo) SaveLastState(g *game.Game) error { return saveLastState(r.s, g) } func saveLastState(s Storage, g *game.Game) error { if err := s.Write(statePath, g); err != nil { return NewStorageError(err) } return nil } func (r *repo) LoadState() (*game.Game, error) { return loadState(r.s, true) } func (r *repo) LoadStateSafe() (*game.Game, error) { return loadState(r.s, false) } func loadState(s Storage, locked bool) (*game.Game, error) { var result *game.Game = new(game.Game) path := statePath exist, err := s.Exists(path) if err != nil { return nil, NewStorageError(err) } if !exist { return nil, NewGameNotInitializedError() } if locked { if err := s.Read(path, result); err != nil { return nil, NewStorageError(err) } } else { if err := s.ReadSafe(path, result); err != nil { return nil, NewStorageError(err) } } return result, nil } func loadMeta(s Storage) (*game.GameMeta, error) { var result *game.GameMeta = new(game.GameMeta) path := metaPath exist, err := s.Exists(path) if err != nil { return nil, NewStorageError(err) } if !exist { return result, nil } if err := s.ReadSafe(path, result); err != nil { return nil, NewStorageError(err) } return result, nil } func saveMeta(s Storage, t uint, gm *game.GameMeta) error { // save turn's meta path := fmt.Sprintf("%s/%s", TurnDir(t), metaPath) if err := s.Write(path, gm); err != nil { return NewStorageError(err) } // also save as latest meta path = metaPath if err := s.Write(path, gm); err != nil { return NewStorageError(err) } return nil } func (r *repo) SaveBattle(t uint, b *report.BattleReport, m *game.BattleMeta) error { meta, err := loadMeta(r.s) if err != nil { return err } err = saveBattle(r.s, t, b) if err != nil { return err } meta.Battles = append(meta.Battles, *m) return saveMeta(r.s, t, meta) } func saveBattle(s Storage, t uint, b *report.BattleReport) error { path := fmt.Sprintf("%s/battle/%s.json", TurnDir(t), b.ID.String()) exist, err := s.Exists(path) if err != nil { return NewStorageError(err) } if exist { return NewStateError(fmt.Sprintf("battle %v for turn %d already has been saved", b.ID, t)) } if err := s.Write(path, b); err != nil { return NewStorageError(err) } return nil } func (r *repo) SaveBombings(t uint, b []*game.Bombing) error { meta, err := loadMeta(r.s) if err != nil { return err } for i := range b { meta.Bombings = append(meta.Bombings, *b[i]) } return saveMeta(r.s, t, meta) } func (r *repo) SaveReport(t uint, rep *report.Report) error { return saveReport(r.s, t, rep) } func saveReport(s Storage, t uint, v *report.Report) error { path := ReportDir(t, v.RaceID) if err := s.Write(path, v); err != nil { return NewStorageError(err) } return nil } func (r *repo) LoadReport(t uint, id uuid.UUID) (*report.Report, error) { return loadReport(r.s, t, id) } func loadReport(s Storage, t uint, id uuid.UUID) (*report.Report, error) { path := ReportDir(t, id) result := new(report.Report) exist, err := s.Exists(path) if err != nil { return nil, NewStorageError(err) } if !exist { return nil, NewReportNotFoundError() } if err := s.ReadSafe(path, result); err != nil { return nil, NewStorageError(err) } return result, nil } func (r *repo) SaveOrder(t uint, id uuid.UUID, o *order.UserGamesOrder) error { return saveOrder(r.s, t, id, o) } func saveOrder(s Storage, t uint, id uuid.UUID, o *order.UserGamesOrder) error { path := OrderDir(t, id) if err := s.WriteSafe(path, o); err != nil { return NewStorageError(err) } return nil } func (r *repo) LoadOrder(t uint, id uuid.UUID) (*order.UserGamesOrder, bool, error) { return loadOrder(r.s, t, id) } func loadOrder(s Storage, t uint, id uuid.UUID) (*order.UserGamesOrder, bool, error) { path := OrderDir(t, id) exist, err := s.Exists(path) if err != nil { return nil, false, NewStorageError(err) } if !exist { return nil, false, nil } stored := new(storedOrder) if err := s.ReadSafe(path, stored); err != nil { return nil, false, NewStorageError(err) } // An empty stored batch is a valid state — the player either // cleared their draft or never added a command yet. We round- // trip it as `(*UserGamesOrder, true, nil)` with an empty // `Commands` slice so callers can distinguish "no order yet" // (ok=false) from "order exists but is empty" (ok=true). result := &order.UserGamesOrder{ GameID: stored.GameID, UpdatedAt: stored.UpdatedAt, Commands: make([]order.DecodableCommand, len(stored.Commands)), } for i := range stored.Commands { command, err := ParseOrder(stored.Commands[i], nil) if err != nil { return nil, false, err } result.Commands[i] = command } return result, true, nil } // Helper funcs func OrderDir(t uint, id uuid.UUID) string { return fmt.Sprintf("%s/order/%s.json", TurnDir(t), id.String()) } func ReportDir(t uint, id uuid.UUID) string { return fmt.Sprintf("%s/report/%s.json", TurnDir(t), id.String()) } func TurnDir(t uint) string { return fmt.Sprintf("%04d", t) } func ParseOrder(c json.RawMessage, validator func(order.DecodableCommand) error) (order.DecodableCommand, error) { meta := new(order.CommandMeta) if err := json.Unmarshal(c, meta); err != nil { return nil, err } switch t := meta.CmdType; t { case order.CommandTypeRaceQuit: return decodeCommand(c, new(order.CommandRaceQuit), validator) case order.CommandTypeRaceVote: return decodeCommand(c, new(order.CommandRaceVote), validator) case order.CommandTypeRaceRelation: return decodeCommand(c, new(order.CommandRaceRelation), validator) case order.CommandTypeShipClassCreate: return decodeCommand(c, new(order.CommandShipClassCreate), validator) case order.CommandTypeShipClassMerge: return decodeCommand(c, new(order.CommandShipClassMerge), validator) case order.CommandTypeShipClassRemove: return decodeCommand(c, new(order.CommandShipClassRemove), validator) case order.CommandTypeShipGroupBreak: return decodeCommand(c, new(order.CommandShipGroupBreak), validator) case order.CommandTypeShipGroupLoad: return decodeCommand(c, new(order.CommandShipGroupLoad), validator) case order.CommandTypeShipGroupUnload: return decodeCommand(c, new(order.CommandShipGroupUnload), validator) case order.CommandTypeShipGroupSend: return decodeCommand(c, new(order.CommandShipGroupSend), validator) case order.CommandTypeShipGroupUpgrade: return decodeCommand(c, new(order.CommandShipGroupUpgrade), validator) case order.CommandTypeShipGroupMerge: return decodeCommand(c, new(order.CommandShipGroupMerge), validator) case order.CommandTypeShipGroupDismantle: return decodeCommand(c, new(order.CommandShipGroupDismantle), validator) case order.CommandTypeShipGroupTransfer: return decodeCommand(c, new(order.CommandShipGroupTransfer), validator) case order.CommandTypeShipGroupJoinFleet: return decodeCommand(c, new(order.CommandShipGroupJoinFleet), validator) case order.CommandTypeFleetMerge: return decodeCommand(c, new(order.CommandFleetMerge), validator) case order.CommandTypeFleetSend: return decodeCommand(c, new(order.CommandFleetSend), validator) case order.CommandTypeScienceCreate: return decodeCommand(c, new(order.CommandScienceCreate), validator) case order.CommandTypeScienceRemove: return decodeCommand(c, new(order.CommandScienceRemove), validator) case order.CommandTypePlanetRename: return decodeCommand(c, new(order.CommandPlanetRename), validator) case order.CommandTypePlanetProduce: return decodeCommand(c, new(order.CommandPlanetProduce), validator) case order.CommandTypePlanetRouteSet: return decodeCommand(c, new(order.CommandPlanetRouteSet), validator) case order.CommandTypePlanetRouteRemove: return decodeCommand(c, new(order.CommandPlanetRouteRemove), validator) default: return nil, fmt.Errorf("unknown comman type: %s", t) } } func decodeCommand(m json.RawMessage, c order.DecodableCommand, validator func(order.DecodableCommand) error) (order.DecodableCommand, error) { v, err := unmarshallCommand(m, c) if err != nil { return nil, err } if validator != nil { err = validator(v) } if err != nil { return nil, err } return v, nil } func unmarshallCommand[T order.DecodableCommand](c json.RawMessage, v T) (T, error) { if err := json.Unmarshal(c, v); err != nil { return v, err } return v, nil }