package domain import ( "cmp" "fmt" "math" "slices" "time" ) type TribeChange struct { id int serverKey string playerID int oldTribeID int newTribeID int createdAt time.Time } const tribeChangeModelName = "TribeChange" // UnmarshalTribeChangeFromDatabase unmarshals TribeChange from the database. // // It should be used only for unmarshalling from the database! // You can't use UnmarshalTribeChangeFromDatabase as constructor - It may put domain into the invalid state! func UnmarshalTribeChangeFromDatabase( id int, serverKey string, playerID int, oldTribeID int, newTribeID int, createdAt time.Time, ) (TribeChange, error) { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return TribeChange{}, ValidationError{ Model: tribeChangeModelName, Field: "id", Err: err, } } if err := validateServerKey(serverKey); err != nil { return TribeChange{}, ValidationError{ Model: tribeChangeModelName, Field: "serverKey", Err: err, } } return TribeChange{ id: id, playerID: playerID, oldTribeID: oldTribeID, newTribeID: newTribeID, serverKey: serverKey, createdAt: createdAt, }, nil } func (tc TribeChange) ID() int { return tc.id } func (tc TribeChange) PlayerID() int { return tc.playerID } func (tc TribeChange) OldTribeID() int { return tc.oldTribeID } func (tc TribeChange) NewTribeID() int { return tc.newTribeID } func (tc TribeChange) ServerKey() string { return tc.serverKey } func (tc TribeChange) CreatedAt() time.Time { return tc.createdAt } func (tc TribeChange) ToCursor() (TribeChangeCursor, error) { return NewTribeChangeCursor(tc.id, tc.serverKey, tc.createdAt) } func (tc TribeChange) IsZero() bool { return tc == TribeChange{} } type TribeChanges []TribeChange type CreateTribeChangeParams struct { serverKey string playerID int newTribeID int oldTribeID int } const createTribeChangeParamsModelName = "CreateTribeChangeParams" func NewCreateTribeChangeParams( serverKey string, playerID int, oldTribeID int, newTribeID int, ) (CreateTribeChangeParams, error) { if err := validateServerKey(serverKey); err != nil { return CreateTribeChangeParams{}, ValidationError{ Model: createTribeChangeParamsModelName, Field: "serverKey", Err: err, } } if err := validateIntInRange(playerID, 1, math.MaxInt); err != nil { return CreateTribeChangeParams{}, ValidationError{ Model: createTribeChangeParamsModelName, Field: "playerID", Err: err, } } if err := validateIntInRange(oldTribeID, 0, math.MaxInt); err != nil { return CreateTribeChangeParams{}, ValidationError{ Model: createTribeChangeParamsModelName, Field: "oldTribeID", Err: err, } } if err := validateIntInRange(newTribeID, 0, math.MaxInt); err != nil { return CreateTribeChangeParams{}, ValidationError{ Model: createTribeChangeParamsModelName, Field: "newTribeID", Err: err, } } return CreateTribeChangeParams{ serverKey: serverKey, playerID: playerID, oldTribeID: oldTribeID, newTribeID: newTribeID, }, nil } // NewCreateTribeChangeParamsFromPlayers constructs a slice of CreatePlayerParams based on the given parameters. // Both slices must be sorted in ascending order by ID // + if storedPlayers contains players from different servers. they must be sorted in ascending order by server key. func NewCreateTribeChangeParamsFromPlayers( serverKey string, players BasePlayers, storedPlayers Players, ) ([]CreateTribeChangeParams, error) { // tribe changes happens now and then, there is no point in prereallocating this slice //nolint:prealloc var params []CreateTribeChangeParams for i, player := range players { if player.IsZero() { return nil, fmt.Errorf("players[%d] is an empty struct", i) } var old Player idx, found := slices.BinarySearchFunc(storedPlayers, player, func(a Player, b BasePlayer) int { return cmp.Or( cmp.Compare(a.ServerKey(), serverKey), cmp.Compare(a.ID(), b.ID()), ) }) if found { old = storedPlayers[idx] } if (old.ID() > 0 && old.TribeID() == player.TribeID()) || (old.ID() == 0 && player.TribeID() == 0) { continue } p, err := NewCreateTribeChangeParams( serverKey, player.ID(), old.TribeID(), player.TribeID(), ) if err != nil { return nil, err } params = append(params, p) } return params, nil } func (params CreateTribeChangeParams) ServerKey() string { return params.serverKey } func (params CreateTribeChangeParams) PlayerID() int { return params.playerID } func (params CreateTribeChangeParams) OldTribeID() int { return params.oldTribeID } func (params CreateTribeChangeParams) NewTribeID() int { return params.newTribeID } type TribeChangeSort uint8 const ( TribeChangeSortCreatedAtASC TribeChangeSort = iota + 1 TribeChangeSortCreatedAtDESC TribeChangeSortIDASC TribeChangeSortIDDESC TribeChangeSortServerKeyASC TribeChangeSortServerKeyDESC ) // IsInConflict returns true if two sorts can't be used together (e.g. TribeChangeSortIDASC and TribeChangeSortIDDESC). func (s TribeChangeSort) IsInConflict(s2 TribeChangeSort) bool { ss := []TribeChangeSort{s, s2} slices.Sort(ss) // ASC is always an odd number, DESC is always an even number return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1] } //nolint:gocyclo func (s TribeChangeSort) String() string { switch s { case TribeChangeSortCreatedAtASC: return "createdAt:ASC" case TribeChangeSortCreatedAtDESC: return "createdAt:DESC" case TribeChangeSortIDASC: return "id:ASC" case TribeChangeSortIDDESC: return "id:DESC" case TribeChangeSortServerKeyASC: return "serverKey:ASC" case TribeChangeSortServerKeyDESC: return "serverKey:DESC" default: return "unknown tribe change sort" } } type TribeChangeCursor struct { id int serverKey string createdAt time.Time } const tribeChangeCursorModelName = "TribeChangeCursor" func NewTribeChangeCursor(id int, serverKey string, createdAt time.Time) (TribeChangeCursor, error) { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return TribeChangeCursor{}, ValidationError{ Model: tribeChangeCursorModelName, Field: "id", Err: err, } } if err := validateServerKey(serverKey); err != nil { return TribeChangeCursor{}, ValidationError{ Model: tribeChangeCursorModelName, Field: "serverKey", Err: err, } } return TribeChangeCursor{ id: id, serverKey: serverKey, createdAt: createdAt, }, nil } //nolint:gocyclo func decodeTribeChangeCursor(encoded string) (TribeChangeCursor, error) { m, err := decodeCursor(encoded) if err != nil { return TribeChangeCursor{}, err } id, err := m.int("id") if err != nil { return TribeChangeCursor{}, ErrInvalidCursor } serverKey, err := m.string("serverKey") if err != nil { return TribeChangeCursor{}, ErrInvalidCursor } createdAt, err := m.time("createdAt") if err != nil { return TribeChangeCursor{}, ErrInvalidCursor } ec, err := NewTribeChangeCursor( id, serverKey, createdAt, ) if err != nil { return TribeChangeCursor{}, ErrInvalidCursor } return ec, nil } func (tcc TribeChangeCursor) ID() int { return tcc.id } func (tcc TribeChangeCursor) ServerKey() string { return tcc.serverKey } func (tcc TribeChangeCursor) CreatedAt() time.Time { return tcc.createdAt } func (tcc TribeChangeCursor) IsZero() bool { return tcc == TribeChangeCursor{} } func (tcc TribeChangeCursor) Encode() string { if tcc.IsZero() { return "" } return encodeCursor([]keyValuePair{ {"id", tcc.id}, {"serverKey", tcc.serverKey}, {"createdAt", tcc.createdAt}, }) } type ListTribeChangesParams struct { serverKeys []string sort []TribeChangeSort cursor TribeChangeCursor limit int } const ( TribeChangeListMaxLimit = 500 listTribeChangesParamsModelName = "ListTribeChangesParams" ) func NewListTribeChangesParams() ListTribeChangesParams { return ListTribeChangesParams{ sort: []TribeChangeSort{ TribeChangeSortServerKeyASC, TribeChangeSortCreatedAtASC, TribeChangeSortIDASC, }, limit: TribeChangeListMaxLimit, } } func (params *ListTribeChangesParams) ServerKeys() []string { return params.serverKeys } func (params *ListTribeChangesParams) SetServerKeys(serverKeys []string) error { for i, sk := range serverKeys { if err := validateServerKey(sk); err != nil { return SliceElementValidationError{ Model: listTribeChangesParamsModelName, Field: "serverKeys", Index: i, Err: err, } } } params.serverKeys = serverKeys return nil } func (params *ListTribeChangesParams) Sort() []TribeChangeSort { return params.sort } const ( tribeChangeSortMinLength = 1 tribeChangeSortMaxLength = 3 ) func (params *ListTribeChangesParams) SetSort(sort []TribeChangeSort) error { if err := validateSort(sort, tribeChangeSortMinLength, tribeChangeSortMaxLength); err != nil { return ValidationError{ Model: listTribeChangesParamsModelName, Field: "sort", Err: err, } } params.sort = sort return nil } func (params *ListTribeChangesParams) Cursor() TribeChangeCursor { return params.cursor } func (params *ListTribeChangesParams) SetCursor(cursor TribeChangeCursor) error { params.cursor = cursor return nil } func (params *ListTribeChangesParams) SetEncodedCursor(encoded string) error { decoded, err := decodeTribeChangeCursor(encoded) if err != nil { return ValidationError{ Model: listTribeChangesParamsModelName, Field: "cursor", Err: err, } } params.cursor = decoded return nil } func (params *ListTribeChangesParams) Limit() int { return params.limit } func (params *ListTribeChangesParams) SetLimit(limit int) error { if err := validateIntInRange(limit, 1, TribeChangeListMaxLimit); err != nil { return ValidationError{ Model: listTribeChangesParamsModelName, Field: "limit", Err: err, } } params.limit = limit return nil } type ListTribeChangesResult struct { tribeChanges TribeChanges self TribeChangeCursor next TribeChangeCursor } const listTribeChangesResultModelName = "ListTribeChangesResult" func NewListTribeChangesResult(ennoblements TribeChanges, next TribeChange) (ListTribeChangesResult, error) { var err error res := ListTribeChangesResult{ tribeChanges: ennoblements, } if len(ennoblements) > 0 { res.self, err = ennoblements[0].ToCursor() if err != nil { return ListTribeChangesResult{}, ValidationError{ Model: listTribeChangesResultModelName, Field: "self", Err: err, } } } if !next.IsZero() { res.next, err = next.ToCursor() if err != nil { return ListTribeChangesResult{}, ValidationError{ Model: listTribeChangesResultModelName, Field: "next", Err: err, } } } return res, nil } func (res ListTribeChangesResult) TribeChanges() TribeChanges { return res.tribeChanges } func (res ListTribeChangesResult) Self() TribeChangeCursor { return res.self } func (res ListTribeChangesResult) Next() TribeChangeCursor { return res.next }