package domain import ( "cmp" "fmt" "math" "net/url" "slices" "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) 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() } 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 CreatePlayerParams struct { base BasePlayer serverKey string bestRank int bestRankAt time.Time mostPoints int mostPointsAt time.Time mostVillages int mostVillagesAt time.Time lastActivityAt time.Time } 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 { if res := cmp.Compare(a.ServerKey(), serverKey); res != 0 { return res } return 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 ) const PlayerListMaxLimit = 200 type ListPlayersParams struct { ids []int idGT NullInt serverKeys []string deleted NullBool sort []PlayerSort limit int offset int } const 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, 0, math.MaxInt); err != nil { return SliceElementValidationError{ Model: listPlayersParamsModelName, Field: "ids", Index: i, Err: err, } } } params.ids = ids return nil } func (params *ListPlayersParams) IDGT() NullInt { return params.idGT } func (params *ListPlayersParams) SetIDGT(idGT NullInt) error { if idGT.Valid { if err := validateIntInRange(idGT.Value, 0, math.MaxInt); err != nil { return ValidationError{ Model: listPlayersParamsModelName, Field: "idGT", Err: err, } } } params.idGT = idGT 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) 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 = 2 ) 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) 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 } func (params *ListPlayersParams) Offset() int { return params.offset } func (params *ListPlayersParams) SetOffset(offset int) error { if err := validateIntInRange(offset, 0, math.MaxInt); err != nil { return ValidationError{ Model: listPlayersParamsModelName, Field: "offset", Err: err, } } params.offset = offset return nil }