package domain import ( "cmp" "fmt" "math" "net/url" "slices" "strings" "time" ) const ( playerNameMinLength = 1 playerNameMaxLength = 150 ) type Player struct { id int serverKey string name string numVillages int points int rank int tribeID int od OpponentsDefeated profileURL *url.URL bestRank int bestRankAt time.Time mostPoints int mostPointsAt time.Time mostVillages int mostVillagesAt time.Time lastActivityAt time.Time createdAt time.Time deletedAt time.Time } const playerModelName = "Player" // UnmarshalPlayerFromDatabase unmarshals Player from the database. // // It should be used only for unmarshalling from the database! // You can't use UnmarshalPlayerFromDatabase as constructor - It may put domain into the invalid state! func UnmarshalPlayerFromDatabase( id int, serverKey string, name string, numVillages int, points int, rank int, tribeID int, od OpponentsDefeated, rawProfileURL string, bestRank int, bestRankAt time.Time, mostPoints int, mostPointsAt time.Time, mostVillages int, mostVillagesAt time.Time, lastActivityAt time.Time, createdAt time.Time, deletedAt time.Time, ) (Player, error) { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return Player{}, ValidationError{ Model: playerModelName, Field: "id", Err: err, } } if err := validateServerKey(serverKey); err != nil { return Player{}, ValidationError{ Model: playerModelName, Field: "serverKey", Err: err, } } profileURL, err := parseURL(rawProfileURL) if err != nil { return Player{}, ValidationError{ Model: playerModelName, Field: "profileURL", Err: err, } } return Player{ id: id, serverKey: serverKey, name: name, numVillages: numVillages, points: points, rank: rank, tribeID: tribeID, od: od, profileURL: profileURL, bestRank: bestRank, bestRankAt: bestRankAt, mostPoints: mostPoints, mostPointsAt: mostPointsAt, mostVillages: mostVillages, mostVillagesAt: mostVillagesAt, lastActivityAt: lastActivityAt, createdAt: createdAt, deletedAt: deletedAt, }, nil } func (p Player) ID() int { return p.id } func (p Player) ServerKey() string { return p.serverKey } func (p Player) Name() string { return p.name } func (p Player) NumVillages() int { return p.numVillages } func (p Player) Points() int { return p.points } func (p Player) Rank() int { return p.rank } func (p Player) TribeID() int { return p.tribeID } func (p Player) OD() OpponentsDefeated { return p.od } func (p Player) ProfileURL() *url.URL { return p.profileURL } func (p Player) BestRank() int { return p.bestRank } func (p Player) BestRankAt() time.Time { return p.bestRankAt } func (p Player) MostPoints() int { return p.mostPoints } func (p Player) MostPointsAt() time.Time { return p.mostPointsAt } func (p Player) MostVillages() int { return p.mostVillages } func (p Player) MostVillagesAt() time.Time { return p.mostVillagesAt } func (p Player) LastActivityAt() time.Time { return p.lastActivityAt } func (p Player) CreatedAt() time.Time { return p.createdAt } func (p Player) DeletedAt() time.Time { return p.deletedAt } func (p Player) WithRelations(tribe NullTribeMeta) PlayerWithRelations { return PlayerWithRelations{ player: p, tribe: tribe, } } func (p Player) Base() BasePlayer { return BasePlayer{ id: p.id, name: p.name, numVillages: p.numVillages, points: p.points, rank: p.rank, tribeID: p.tribeID, od: p.od, profileURL: p.profileURL, } } func (p Player) IsDeleted() bool { return !p.deletedAt.IsZero() } func (p Player) IsZero() bool { return p == Player{} } type Players []Player // Delete finds all players with the given serverKey that are not in the given slice with active players // and returns their ids + tribe changes that must be created. // Both slices must be sorted in ascending order by ID. // + if ps contains players from different servers. they must be sorted in ascending order by server key. func (ps Players) Delete(serverKey string, active BasePlayers) ([]int, []CreateTribeChangeParams, error) { // players are deleted now and then, there is no point in prereallocating these slices //nolint:prealloc var toDelete []int var params []CreateTribeChangeParams for _, p := range ps { if p.IsDeleted() || p.ServerKey() != serverKey { continue } _, found := slices.BinarySearchFunc(active, p, func(a BasePlayer, b Player) int { return cmp.Compare(a.ID(), b.ID()) }) if found { continue } toDelete = append(toDelete, p.ID()) if p.TribeID() > 0 { p, err := NewCreateTribeChangeParams(serverKey, p.ID(), p.TribeID(), 0) if err != nil { return nil, nil, err } params = append(params, p) } } return toDelete, params, nil } type PlayerWithRelations struct { player Player tribe NullTribeMeta } func (p PlayerWithRelations) Player() Player { return p.player } func (p PlayerWithRelations) Tribe() NullTribeMeta { return p.tribe } func (p PlayerWithRelations) IsZero() bool { return p.player.IsZero() && p.tribe.IsZero() } type CreatePlayerParams struct { base BasePlayer serverKey string bestRank int bestRankAt time.Time mostPoints int mostPointsAt time.Time mostVillages int mostVillagesAt time.Time lastActivityAt time.Time } type PlayersWithRelations []PlayerWithRelations const createPlayerParamsModelName = "CreatePlayerParams" // NewCreatePlayerParams 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. // //nolint:gocyclo func NewCreatePlayerParams(serverKey string, players BasePlayers, storedPlayers Players) ([]CreatePlayerParams, error) { if err := validateServerKey(serverKey); err != nil { return nil, ValidationError{ Model: createPlayerParamsModelName, Field: "serverKey", Err: err, } } params := make([]CreatePlayerParams, 0, len(players)) 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] } now := time.Now() p := CreatePlayerParams{ base: player, serverKey: serverKey, bestRank: player.Rank(), bestRankAt: now, mostPoints: player.Points(), mostPointsAt: now, mostVillages: player.NumVillages(), mostVillagesAt: now, lastActivityAt: now, } if old.ID() > 0 { if old.MostPoints() >= p.MostPoints() { p.mostPoints = old.MostPoints() p.mostPointsAt = old.MostPointsAt() } if old.MostVillages() >= p.MostVillages() { p.mostVillages = old.MostVillages() p.mostVillagesAt = old.MostVillagesAt() } if old.BestRank() <= p.BestRank() { p.bestRank = old.BestRank() p.bestRankAt = old.BestRankAt() } //nolint:lll if player.Points() <= old.Points() && player.NumVillages() <= old.NumVillages() && player.OD().ScoreAtt() <= old.OD().ScoreAtt() { p.lastActivityAt = old.LastActivityAt() } } params = append(params, p) } return params, nil } func (params CreatePlayerParams) Base() BasePlayer { return params.base } func (params CreatePlayerParams) ServerKey() string { return params.serverKey } func (params CreatePlayerParams) BestRank() int { return params.bestRank } func (params CreatePlayerParams) BestRankAt() time.Time { return params.bestRankAt } func (params CreatePlayerParams) MostPoints() int { return params.mostPoints } func (params CreatePlayerParams) MostPointsAt() time.Time { return params.mostPointsAt } func (params CreatePlayerParams) MostVillages() int { return params.mostVillages } func (params CreatePlayerParams) MostVillagesAt() time.Time { return params.mostVillagesAt } func (params CreatePlayerParams) LastActivityAt() time.Time { return params.lastActivityAt } type PlayerSort uint8 const ( PlayerSortIDASC PlayerSort = iota + 1 PlayerSortIDDESC PlayerSortServerKeyASC PlayerSortServerKeyDESC PlayerSortODScoreAttASC PlayerSortODScoreAttDESC PlayerSortODScoreDefASC PlayerSortODScoreDefDESC PlayerSortODScoreTotalASC PlayerSortODScoreTotalDESC PlayerSortPointsASC PlayerSortPointsDESC PlayerSortDeletedAtASC PlayerSortDeletedAtDESC ) //nolint:gocyclo func newPlayerSortFromString(s string) (PlayerSort, error) { switch strings.ToLower(s) { case "odscoreatt:asc": return PlayerSortODScoreAttASC, nil case "odscoreatt:desc": return PlayerSortODScoreAttDESC, nil case "odscoredef:asc": return PlayerSortODScoreDefASC, nil case "odscoredef:desc": return PlayerSortODScoreDefDESC, nil case "odscoretotal:asc": return PlayerSortODScoreTotalASC, nil case "odscoretotal:desc": return PlayerSortODScoreTotalDESC, nil case "points:asc": return PlayerSortPointsASC, nil case "points:desc": return PlayerSortPointsDESC, nil case "deletedat:asc": return PlayerSortDeletedAtASC, nil case "deletedat:desc": return PlayerSortDeletedAtDESC, nil default: return 0, UnsupportedSortStringError{ Sort: s, } } } type PlayerCursor struct { id int serverKey string odScoreAtt int odScoreDef int odScoreTotal int points int deletedAt time.Time } const playerCursorModelName = "PlayerCursor" func NewPlayerCursor( id int, serverKey string, odScoreAtt int, odScoreDef int, odScoreTotal int, points int, deletedAt time.Time, ) (PlayerCursor, error) { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return PlayerCursor{}, ValidationError{ Model: playerCursorModelName, Field: "id", Err: err, } } if err := validateServerKey(serverKey); err != nil { return PlayerCursor{}, ValidationError{ Model: playerCursorModelName, Field: "serverKey", Err: err, } } if err := validateIntInRange(odScoreAtt, 0, math.MaxInt); err != nil { return PlayerCursor{}, ValidationError{ Model: playerCursorModelName, Field: "odScoreAtt", Err: err, } } if err := validateIntInRange(odScoreDef, 0, math.MaxInt); err != nil { return PlayerCursor{}, ValidationError{ Model: playerCursorModelName, Field: "odScoreDef", Err: err, } } if err := validateIntInRange(odScoreTotal, 0, math.MaxInt); err != nil { return PlayerCursor{}, ValidationError{ Model: playerCursorModelName, Field: "odScoreTotal", Err: err, } } if err := validateIntInRange(points, 0, math.MaxInt); err != nil { return PlayerCursor{}, ValidationError{ Model: playerCursorModelName, Field: "points", Err: err, } } return PlayerCursor{ id: id, serverKey: serverKey, odScoreAtt: odScoreAtt, odScoreDef: odScoreDef, odScoreTotal: odScoreTotal, points: points, deletedAt: deletedAt, }, nil } //nolint:gocyclo func decodePlayerCursor(encoded string) (PlayerCursor, error) { m, err := decodeCursor(encoded) if err != nil { return PlayerCursor{}, err } id, err := m.int("id") if err != nil { return PlayerCursor{}, ErrInvalidCursor } serverKey, err := m.string("serverKey") if err != nil { return PlayerCursor{}, ErrInvalidCursor } odScoreAtt, err := m.int("odScoreAtt") if err != nil { return PlayerCursor{}, ErrInvalidCursor } odScoreDef, err := m.int("odScoreDef") if err != nil { return PlayerCursor{}, ErrInvalidCursor } odScoreTotal, err := m.int("odScoreTotal") if err != nil { return PlayerCursor{}, ErrInvalidCursor } points, err := m.int("points") if err != nil { return PlayerCursor{}, ErrInvalidCursor } deletedAt, err := m.time("deletedAt") if err != nil { return PlayerCursor{}, ErrInvalidCursor } pc, err := NewPlayerCursor( id, serverKey, odScoreAtt, odScoreDef, odScoreTotal, points, deletedAt, ) if err != nil { return PlayerCursor{}, ErrInvalidCursor } return pc, nil } func (pc PlayerCursor) ID() int { return pc.id } func (pc PlayerCursor) ServerKey() string { return pc.serverKey } func (pc PlayerCursor) ODScoreAtt() int { return pc.odScoreAtt } func (pc PlayerCursor) ODScoreDef() int { return pc.odScoreDef } func (pc PlayerCursor) ODScoreTotal() int { return pc.odScoreTotal } func (pc PlayerCursor) Points() int { return pc.points } func (pc PlayerCursor) DeletedAt() time.Time { return pc.deletedAt } func (pc PlayerCursor) IsZero() bool { return pc == PlayerCursor{} } func (pc PlayerCursor) Encode() string { if pc.IsZero() { return "" } return encodeCursor([]keyValuePair{ {"id", pc.id}, {"serverKey", pc.serverKey}, {"odScoreAtt", pc.odScoreAtt}, {"odScoreDef", pc.odScoreDef}, {"odScoreTotal", pc.odScoreTotal}, {"points", pc.points}, {"deletedAt", pc.deletedAt}, }) } type ListPlayersParams struct { ids []int serverKeys []string names []string deleted NullBool sort []PlayerSort cursor PlayerCursor limit int } const ( PlayerListMaxLimit = 200 listPlayersParamsModelName = "ListPlayersParams" ) func NewListPlayersParams() ListPlayersParams { return ListPlayersParams{ sort: []PlayerSort{ PlayerSortServerKeyASC, PlayerSortIDASC, }, limit: PlayerListMaxLimit, } } func (params *ListPlayersParams) IDs() []int { return params.ids } func (params *ListPlayersParams) SetIDs(ids []int) error { for i, id := range ids { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return SliceElementValidationError{ Model: listPlayersParamsModelName, Field: "ids", Index: i, Err: err, } } } params.ids = ids return nil } func (params *ListPlayersParams) ServerKeys() []string { return params.serverKeys } func (params *ListPlayersParams) SetServerKeys(serverKeys []string) error { params.serverKeys = serverKeys return nil } func (params *ListPlayersParams) Names() []string { return params.names } const ( playerNamesMinLength = 1 playerNamesMaxLength = 100 ) func (params *ListPlayersParams) SetNames(names []string) error { if err := validateSliceLen(names, playerNamesMinLength, playerNamesMaxLength); err != nil { return ValidationError{ Model: listPlayersParamsModelName, Field: "names", Err: err, } } for i, n := range names { if err := validateStringLen(n, playerNameMinLength, playerNameMaxLength); err != nil { return SliceElementValidationError{ Model: listPlayersParamsModelName, Field: "names", Index: i, Err: err, } } } params.names = names return nil } func (params *ListPlayersParams) Deleted() NullBool { return params.deleted } func (params *ListPlayersParams) SetDeleted(deleted NullBool) error { params.deleted = deleted return nil } func (params *ListPlayersParams) Sort() []PlayerSort { return params.sort } const ( playerSortMinLength = 1 playerSortMaxLength = 3 ) func (params *ListPlayersParams) SetSort(sort []PlayerSort) error { if err := validateSliceLen(sort, playerSortMinLength, playerSortMaxLength); err != nil { return ValidationError{ Model: listPlayersParamsModelName, Field: "sort", Err: err, } } params.sort = sort return nil } func (params *ListPlayersParams) PrependSortString(sort []string) error { if err := validateSliceLen(sort, playerSortMinLength, max(playerSortMaxLength-len(params.sort), 0)); err != nil { return ValidationError{ Model: listPlayersParamsModelName, Field: "sort", Err: err, } } for i := len(sort) - 1; i >= 0; i-- { converted, err := newPlayerSortFromString(sort[i]) if err != nil { return SliceElementValidationError{ Model: listPlayersParamsModelName, Field: "sort", Index: i, Err: err, } } params.sort = append([]PlayerSort{converted}, params.sort...) } return nil } func (params *ListPlayersParams) Cursor() PlayerCursor { return params.cursor } func (params *ListPlayersParams) SetCursor(cursor PlayerCursor) error { params.cursor = cursor return nil } func (params *ListPlayersParams) SetEncodedCursor(encoded string) error { decoded, err := decodePlayerCursor(encoded) if err != nil { return ValidationError{ Model: listPlayersParamsModelName, Field: "cursor", Err: err, } } params.cursor = decoded return nil } func (params *ListPlayersParams) Limit() int { return params.limit } func (params *ListPlayersParams) SetLimit(limit int) error { if err := validateIntInRange(limit, 1, PlayerListMaxLimit); err != nil { return ValidationError{ Model: listPlayersParamsModelName, Field: "limit", Err: err, } } params.limit = limit return nil } type ListPlayersResult struct { players Players self PlayerCursor next PlayerCursor } const listPlayersResultModelName = "ListPlayersResult" func NewListPlayersResult(players Players, next Player) (ListPlayersResult, error) { var err error res := ListPlayersResult{ players: players, } if len(players) > 0 { od := players[0].OD() res.self, err = NewPlayerCursor( players[0].ID(), players[0].ServerKey(), od.ScoreAtt(), od.ScoreDef(), od.ScoreTotal(), players[0].Points(), players[0].DeletedAt(), ) if err != nil { return ListPlayersResult{}, ValidationError{ Model: listPlayersResultModelName, Field: "self", Err: err, } } } if !next.IsZero() { od := next.OD() res.next, err = NewPlayerCursor( next.ID(), next.ServerKey(), od.ScoreAtt(), od.ScoreDef(), od.ScoreTotal(), next.Points(), next.DeletedAt(), ) if err != nil { return ListPlayersResult{}, ValidationError{ Model: listPlayersResultModelName, Field: "next", Err: err, } } } return res, nil } func (res ListPlayersResult) Players() Players { return res.players } func (res ListPlayersResult) Self() PlayerCursor { return res.self } func (res ListPlayersResult) Next() PlayerCursor { return res.next } type ListPlayersWithRelationsResult struct { players PlayersWithRelations self PlayerCursor next PlayerCursor } const listPlayersWithRelationsResultModelName = "ListPlayersWithRelationsResult" func NewListPlayersWithRelationsResult( players PlayersWithRelations, next PlayerWithRelations, ) (ListPlayersWithRelationsResult, error) { var err error res := ListPlayersWithRelationsResult{ players: players, } if len(players) > 0 { player := players[0].Player() od := player.OD() res.self, err = NewPlayerCursor( player.ID(), player.ServerKey(), od.ScoreAtt(), od.ScoreDef(), od.ScoreTotal(), player.Points(), player.DeletedAt(), ) if err != nil { return ListPlayersWithRelationsResult{}, ValidationError{ Model: listPlayersWithRelationsResultModelName, Field: "self", Err: err, } } } if !next.IsZero() { fmt.Println(next.IsZero()) player := next.Player() od := player.OD() res.next, err = NewPlayerCursor( player.ID(), player.ServerKey(), od.ScoreAtt(), od.ScoreDef(), od.ScoreTotal(), player.Points(), player.DeletedAt(), ) if err != nil { return ListPlayersWithRelationsResult{}, ValidationError{ Model: listPlayersWithRelationsResultModelName, Field: "next", Err: err, } } } return res, nil } func (res ListPlayersWithRelationsResult) Players() PlayersWithRelations { return res.players } func (res ListPlayersWithRelationsResult) Self() PlayerCursor { return res.self } func (res ListPlayersWithRelationsResult) Next() PlayerCursor { return res.next } type PlayerNotFoundError struct { ID int ServerKey string } var _ ErrorWithParams = PlayerNotFoundError{} func (e PlayerNotFoundError) Error() string { return fmt.Sprintf("player with id %d and server key %s not found", e.ID, e.ServerKey) } func (e PlayerNotFoundError) Type() ErrorType { return ErrorTypeNotFound } func (e PlayerNotFoundError) Code() string { return "player-not-found" } func (e PlayerNotFoundError) Params() map[string]any { return map[string]any{ "ID": e.ID, "ServerKey": e.ServerKey, } }