2024-01-01 07:49:47 +00:00
|
|
|
package domain
|
|
|
|
|
2024-01-01 10:20:30 +00:00
|
|
|
import (
|
|
|
|
"cmp"
|
|
|
|
"fmt"
|
|
|
|
"math"
|
|
|
|
"net/url"
|
|
|
|
"slices"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2024-01-01 07:49:47 +00:00
|
|
|
const (
|
|
|
|
playerNameMinLength = 1
|
|
|
|
playerNameMaxLength = 150
|
|
|
|
)
|
2024-01-01 10:20:30 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
const playerModelName = "Player"
|
|
|
|
|
2024-01-01 10:20:30 +00:00
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
if err := validateServerKey(serverKey); err != nil {
|
|
|
|
return Player{}, ValidationError{
|
|
|
|
Model: playerModelName,
|
|
|
|
Field: "serverKey",
|
|
|
|
Err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-01 10:20:30 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2024-01-16 06:28:03 +00:00
|
|
|
func (p Player) IsZero() bool {
|
|
|
|
return p == Player{}
|
|
|
|
}
|
|
|
|
|
2024-01-01 10:20:30 +00:00
|
|
|
type Players []Player
|
|
|
|
|
2024-01-06 09:26:56 +00:00
|
|
|
// 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.
|
2024-01-01 10:20:30 +00:00
|
|
|
// Both slices must be sorted in ascending order by ID.
|
2024-01-06 09:26:56 +00:00
|
|
|
// + 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
|
2024-01-01 10:20:30 +00:00
|
|
|
//nolint:prealloc
|
|
|
|
var toDelete []int
|
2024-01-06 09:26:56 +00:00
|
|
|
var params []CreateTribeChangeParams
|
2024-01-01 10:20:30 +00:00
|
|
|
|
|
|
|
for _, p := range ps {
|
2024-01-06 09:26:56 +00:00
|
|
|
if p.IsDeleted() || p.ServerKey() != serverKey {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-01-01 10:20:30 +00:00
|
|
|
_, 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())
|
2024-01-06 09:26:56 +00:00
|
|
|
|
|
|
|
if p.TribeID() > 0 {
|
|
|
|
p, err := NewCreateTribeChangeParams(serverKey, p.ID(), p.TribeID(), 0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
params = append(params, p)
|
|
|
|
}
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
2024-01-06 09:26:56 +00:00
|
|
|
return toDelete, params, nil
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2024-01-02 09:54:11 +00:00
|
|
|
// + if storedPlayers contains players from different servers. they must be sorted in ascending order by server key.
|
2024-01-01 10:20:30 +00:00
|
|
|
//
|
|
|
|
//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 {
|
2024-02-09 08:49:23 +00:00
|
|
|
return cmp.Or(
|
|
|
|
cmp.Compare(a.ServerKey(), serverKey),
|
|
|
|
cmp.Compare(a.ID(), b.ID()),
|
|
|
|
)
|
2024-01-01 10:20:30 +00:00
|
|
|
})
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
func (params CreatePlayerParams) Base() BasePlayer {
|
|
|
|
return params.base
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
func (params CreatePlayerParams) ServerKey() string {
|
|
|
|
return params.serverKey
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
func (params CreatePlayerParams) BestRank() int {
|
|
|
|
return params.bestRank
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
func (params CreatePlayerParams) BestRankAt() time.Time {
|
|
|
|
return params.bestRankAt
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
func (params CreatePlayerParams) MostPoints() int {
|
|
|
|
return params.mostPoints
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
func (params CreatePlayerParams) MostPointsAt() time.Time {
|
|
|
|
return params.mostPointsAt
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
func (params CreatePlayerParams) MostVillages() int {
|
|
|
|
return params.mostVillages
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
func (params CreatePlayerParams) MostVillagesAt() time.Time {
|
|
|
|
return params.mostVillagesAt
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 09:54:11 +00:00
|
|
|
func (params CreatePlayerParams) LastActivityAt() time.Time {
|
|
|
|
return params.lastActivityAt
|
2024-01-01 10:20:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type PlayerSort uint8
|
|
|
|
|
|
|
|
const (
|
|
|
|
PlayerSortIDASC PlayerSort = iota + 1
|
|
|
|
PlayerSortIDDESC
|
|
|
|
PlayerSortServerKeyASC
|
|
|
|
PlayerSortServerKeyDESC
|
|
|
|
)
|
|
|
|
|
|
|
|
type ListPlayersParams struct {
|
|
|
|
ids []int
|
|
|
|
idGT NullInt
|
|
|
|
serverKeys []string
|
|
|
|
deleted NullBool
|
|
|
|
sort []PlayerSort
|
|
|
|
limit int
|
|
|
|
offset int
|
|
|
|
}
|
|
|
|
|
2024-01-27 08:37:12 +00:00
|
|
|
const (
|
|
|
|
PlayerListMaxLimit = 200
|
|
|
|
listPlayersParamsModelName = "ListPlayersParams"
|
|
|
|
)
|
2024-01-01 10:20:30 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|