From f8c9bdb321f14aa230ac2ee6ad19bd2b8f379ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Tue, 7 May 2024 06:15:37 +0000 Subject: [PATCH] feat: add more stats to the Server model (#47) Reviewed-on: https://gitea.dwysokinski.me/twhelp/core/pulls/47 --- api/openapi3.yml | 14 + .../adapter/publisher_watermill_player.go | 1 + internal/adapter/publisher_watermill_tribe.go | 1 + internal/adapter/repository_bun_player.go | 23 ++ internal/adapter/repository_bun_server.go | 16 ++ internal/adapter/repository_bun_tribe.go | 23 ++ internal/adapter/repository_player_test.go | 72 +++++ internal/adapter/repository_server_test.go | 20 ++ internal/adapter/repository_test.go | 2 + internal/adapter/repository_tribe_test.go | 72 +++++ internal/app/service_player.go | 14 +- internal/app/service_server.go | 24 ++ internal/app/service_tribe.go | 14 +- internal/bun/bunmodel/server.go | 10 +- .../20240506051128_servers_add_columns.go | 44 +++ ...20240506053957_servers_fill_num_columns.go | 48 ++++ internal/domain/domaintest/server.go | 4 + internal/domain/message_payloads.go | 38 +++ internal/domain/message_payloads_test.go | 8 + internal/domain/player.go | 31 +++ internal/domain/player_test.go | 54 ++++ internal/domain/server.go | 114 +++++++- internal/domain/server_test.go | 252 ++++++++++++++++++ internal/domain/tribe.go | 31 +++ internal/domain/tribe_test.go | 54 ++++ internal/port/consumer_data_sync_test.go | 10 +- internal/port/consumer_watermill_server.go | 2 + internal/port/internal/apimodel/server.go | 12 +- .../datasync/stage1/expected/servers.json | 2 +- .../datasync/stage2/expected/servers.json | 2 +- internal/watermill/watermillmsg/player.go | 1 + internal/watermill/watermillmsg/tribe.go | 1 + 32 files changed, 1000 insertions(+), 14 deletions(-) create mode 100644 internal/bun/migrations/20240506051128_servers_add_columns.go create mode 100644 internal/bun/migrations/20240506053957_servers_fill_num_columns.go diff --git a/api/openapi3.yml b/api/openapi3.yml index da3798b..744556a 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -583,8 +583,12 @@ components: - key - open - url + - numPlayers - numActivePlayers + - numInactivePlayers + - numTribes - numActiveTribes + - numInactiveTribes - numVillages - numBarbarianVillages - numBonusVillages @@ -599,13 +603,23 @@ components: type: string format: uri example: https://en138.tribalwars.net + numPlayers: + type: integer + description: numActivePlayers+numInactivePlayers numActivePlayers: type: integer + numInactivePlayers: + type: integer playerDataSyncedAt: type: string format: date-time + numTribes: + type: integer + description: numActiveTribes+numInactiveTribes numActiveTribes: type: integer + numInactiveTribes: + type: integer tribeDataSyncedAt: type: string format: date-time diff --git a/internal/adapter/publisher_watermill_player.go b/internal/adapter/publisher_watermill_player.go index 51319c9..0929054 100644 --- a/internal/adapter/publisher_watermill_player.go +++ b/internal/adapter/publisher_watermill_player.go @@ -38,6 +38,7 @@ func (pub *PlayerWatermillPublisher) EventSynced( ServerKey: p.ServerKey(), VersionCode: p.VersionCode(), ServerURL: p.ServerURL(), + NumPlayers: p.NumPlayers(), NumActivePlayers: p.NumActivePlayers(), }) if err != nil { diff --git a/internal/adapter/publisher_watermill_tribe.go b/internal/adapter/publisher_watermill_tribe.go index 83d66db..7d986fc 100644 --- a/internal/adapter/publisher_watermill_tribe.go +++ b/internal/adapter/publisher_watermill_tribe.go @@ -38,6 +38,7 @@ func (pub *TribeWatermillPublisher) EventSynced( ServerKey: p.ServerKey(), VersionCode: p.VersionCode(), ServerURL: p.ServerURL(), + NumTribes: p.NumTribes(), NumActiveTribes: p.NumActiveTribes(), }) if err != nil { diff --git a/internal/adapter/repository_bun_player.go b/internal/adapter/repository_bun_player.go index 4554f03..68f9bc8 100644 --- a/internal/adapter/repository_bun_player.go +++ b/internal/adapter/repository_bun_player.go @@ -131,6 +131,17 @@ func (repo *PlayerBunRepository) ListWithRelations( return domain.NewListPlayersWithRelationsResult(separateListResultAndNext(converted, params.Limit())) } +func (repo *PlayerBunRepository) Count(ctx context.Context, params domain.CountPlayersParams) (int, error) { + cnt, err := repo.db.NewSelect(). + Model((*bunmodel.Player)(nil)). + Apply(countPlayersParamsApplier{params: params}.apply). + Count(ctx) + if err != nil { + return 0, fmt.Errorf("couldnt count players: %w", err) + } + return cnt, nil +} + func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error { if len(ids) == 0 { return nil @@ -323,3 +334,15 @@ func (a listPlayersParamsApplier) sortToColumnAndDirection( return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue) } } + +type countPlayersParamsApplier struct { + params domain.CountPlayersParams +} + +func (a countPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { + if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 { + q = q.Where("player.server_key IN (?)", bun.In(serverKeys)) + } + + return q +} diff --git a/internal/adapter/repository_bun_server.go b/internal/adapter/repository_bun_server.go index 8c83faa..976fd53 100644 --- a/internal/adapter/repository_bun_server.go +++ b/internal/adapter/repository_bun_server.go @@ -126,18 +126,34 @@ func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery { q = q.Set("building_info = ?", bunmodel.NewBuildingInfo(buildingInfo.V)) } + if numTribes := a.params.NumTribes(); numTribes.Valid { + q = q.Set("num_tribes = ?", numTribes.V) + } + if numActiveTribes := a.params.NumActiveTribes(); numActiveTribes.Valid { q = q.Set("num_active_tribes = ?", numActiveTribes.V) } + if numInactiveTribes := a.params.NumInactiveTribes(); numInactiveTribes.Valid { + q = q.Set("num_inactive_tribes = ?", numInactiveTribes.V) + } + if tribeDataSyncedAt := a.params.TribeDataSyncedAt(); tribeDataSyncedAt.Valid { q = q.Set("tribe_data_synced_at = ?", tribeDataSyncedAt.V) } + if numPlayers := a.params.NumPlayers(); numPlayers.Valid { + q = q.Set("num_players = ?", numPlayers.V) + } + if numActivePlayers := a.params.NumActivePlayers(); numActivePlayers.Valid { q = q.Set("num_active_players = ?", numActivePlayers.V) } + if numInactivePlayers := a.params.NumInactivePlayers(); numInactivePlayers.Valid { + q = q.Set("num_inactive_players = ?", numInactivePlayers.V) + } + if playerDataSyncedAt := a.params.PlayerDataSyncedAt(); playerDataSyncedAt.Valid { q = q.Set("player_data_synced_at = ?", playerDataSyncedAt.V) } diff --git a/internal/adapter/repository_bun_tribe.go b/internal/adapter/repository_bun_tribe.go index e8162c0..5f2b458 100644 --- a/internal/adapter/repository_bun_tribe.go +++ b/internal/adapter/repository_bun_tribe.go @@ -132,6 +132,17 @@ func (repo *TribeBunRepository) List( return domain.NewListTribesResult(separateListResultAndNext(converted, params.Limit())) } +func (repo *TribeBunRepository) Count(ctx context.Context, params domain.CountTribesParams) (int, error) { + cnt, err := repo.db.NewSelect(). + Model((*bunmodel.Tribe)(nil)). + Apply(countTribesParamsApplier{params: params}.apply). + Count(ctx) + if err != nil { + return 0, fmt.Errorf("couldnt count tribes: %w", err) + } + return cnt, nil +} + func (repo *TribeBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error { if len(ids) == 0 { return nil @@ -287,3 +298,15 @@ func (a listTribesParamsApplier) sortToColumnAndDirection( return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue) } } + +type countTribesParamsApplier struct { + params domain.CountTribesParams +} + +func (a countTribesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { + if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 { + q = q.Where("tribe.server_key IN (?)", bun.In(serverKeys)) + } + + return q +} diff --git a/internal/adapter/repository_player_test.go b/internal/adapter/repository_player_test.go index 1939fa6..5ca54a7 100644 --- a/internal/adapter/repository_player_test.go +++ b/internal/adapter/repository_player_test.go @@ -664,6 +664,78 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories } }) + t.Run("Count", func(t *testing.T) { + t.Parallel() + + repos := newRepos(t) + + tests := []struct { + name string + params func(t *testing.T) domain.CountPlayersParams + assertResult func(t *testing.T, params domain.CountPlayersParams, count int) + assertError func(t *testing.T, err error) + }{ + { + name: "OK: default params", + params: func(t *testing.T) domain.CountPlayersParams { + t.Helper() + return domain.NewCountPlayersParams() + }, + assertResult: func(t *testing.T, _ domain.CountPlayersParams, count int) { + t.Helper() + res, err := repos.player.List(ctx, domain.NewListPlayersParams()) + require.NoError(t, err) + assert.Len(t, res.Players(), count) + }, + }, + { + name: "OK: serverKeys", + params: func(t *testing.T) domain.CountPlayersParams { + t.Helper() + + res, err := repos.player.List(ctx, domain.NewListPlayersParams()) + require.NoError(t, err) + + params := domain.NewCountPlayersParams() + require.NoError(t, params.SetServerKeys([]string{res.Players()[0].ServerKey()})) + + return params + }, + assertResult: func(t *testing.T, params domain.CountPlayersParams, count int) { + t.Helper() + + listParams := domain.NewListPlayersParams() + require.NoError(t, listParams.SetServerKeys(params.ServerKeys())) + + res, err := repos.player.List(ctx, listParams) + require.NoError(t, err) + + assert.Len(t, res.Players(), count) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + assertError := tt.assertError + if assertError == nil { + assertError = func(t *testing.T, err error) { + t.Helper() + require.NoError(t, err) + } + } + + params := tt.params(t) + + cnt, err := repos.player.Count(ctx, params) + assertError(t, err) + tt.assertResult(t, params, cnt) + }) + } + }) + t.Run("Delete", func(t *testing.T) { t.Parallel() diff --git a/internal/adapter/repository_server_test.go b/internal/adapter/repository_server_test.go index 40f1038..52e4f06 100644 --- a/internal/adapter/repository_server_test.go +++ b/internal/adapter/repository_server_test.go @@ -494,18 +494,34 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories V: domaintest.NewBuildingInfo(t), Valid: true, })) + require.NoError(t, updateParams.SetNumTribes(domain.NullInt{ + V: gofakeit.IntRange(0, math.MaxInt), + Valid: true, + })) require.NoError(t, updateParams.SetNumActiveTribes(domain.NullInt{ V: gofakeit.IntRange(0, math.MaxInt), Valid: true, })) + require.NoError(t, updateParams.SetNumInactiveTribes(domain.NullInt{ + V: gofakeit.IntRange(0, math.MaxInt), + Valid: true, + })) require.NoError(t, updateParams.SetTribeDataSyncedAt(domain.NullTime{ V: time.Now(), Valid: true, })) + require.NoError(t, updateParams.SetNumPlayers(domain.NullInt{ + V: gofakeit.IntRange(0, math.MaxInt), + Valid: true, + })) require.NoError(t, updateParams.SetNumActivePlayers(domain.NullInt{ V: gofakeit.IntRange(0, math.MaxInt), Valid: true, })) + require.NoError(t, updateParams.SetNumInactivePlayers(domain.NullInt{ + V: gofakeit.IntRange(0, math.MaxInt), + Valid: true, + })) require.NoError(t, updateParams.SetPlayerDataSyncedAt(domain.NullTime{ V: time.Now(), Valid: true, @@ -556,14 +572,18 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories assert.Equal(t, updateParams.Config().V, serverAfterUpdate.Config()) assert.Equal(t, updateParams.UnitInfo().V, serverAfterUpdate.UnitInfo()) assert.Equal(t, updateParams.BuildingInfo().V, serverAfterUpdate.BuildingInfo()) + assert.Equal(t, updateParams.NumTribes().V, serverAfterUpdate.NumTribes()) assert.Equal(t, updateParams.NumActiveTribes().V, serverAfterUpdate.NumActiveTribes()) + assert.Equal(t, updateParams.NumInactiveTribes().V, serverAfterUpdate.NumInactiveTribes()) assert.WithinDuration( t, updateParams.TribeDataSyncedAt().V, serverAfterUpdate.TribeDataSyncedAt(), time.Minute, ) + assert.Equal(t, updateParams.NumPlayers().V, serverAfterUpdate.NumPlayers()) assert.Equal(t, updateParams.NumActivePlayers().V, serverAfterUpdate.NumActivePlayers()) + assert.Equal(t, updateParams.NumInactivePlayers().V, serverAfterUpdate.NumInactivePlayers()) assert.WithinDuration( t, updateParams.PlayerDataSyncedAt().V, diff --git a/internal/adapter/repository_test.go b/internal/adapter/repository_test.go index d66eec5..6af7d51 100644 --- a/internal/adapter/repository_test.go +++ b/internal/adapter/repository_test.go @@ -26,6 +26,7 @@ type tribeRepository interface { CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error) + Count(ctx context.Context, params domain.CountTribesParams) (int, error) Delete(ctx context.Context, serverKey string, ids ...int) error } @@ -33,6 +34,7 @@ type playerRepository interface { CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error) ListWithRelations(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersWithRelationsResult, error) + Count(ctx context.Context, params domain.CountPlayersParams) (int, error) Delete(ctx context.Context, serverKey string, ids ...int) error } diff --git a/internal/adapter/repository_tribe_test.go b/internal/adapter/repository_tribe_test.go index ad978ba..14aa32f 100644 --- a/internal/adapter/repository_tribe_test.go +++ b/internal/adapter/repository_tribe_test.go @@ -718,6 +718,78 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories) } }) + t.Run("Count", func(t *testing.T) { + t.Parallel() + + repos := newRepos(t) + + tests := []struct { + name string + params func(t *testing.T) domain.CountTribesParams + assertResult func(t *testing.T, params domain.CountTribesParams, count int) + assertError func(t *testing.T, err error) + }{ + { + name: "OK: default params", + params: func(t *testing.T) domain.CountTribesParams { + t.Helper() + return domain.NewCountTribesParams() + }, + assertResult: func(t *testing.T, _ domain.CountTribesParams, count int) { + t.Helper() + res, err := repos.tribe.List(ctx, domain.NewListTribesParams()) + require.NoError(t, err) + assert.Len(t, res.Tribes(), count) + }, + }, + { + name: "OK: serverKeys", + params: func(t *testing.T) domain.CountTribesParams { + t.Helper() + + res, err := repos.tribe.List(ctx, domain.NewListTribesParams()) + require.NoError(t, err) + + params := domain.NewCountTribesParams() + require.NoError(t, params.SetServerKeys([]string{res.Tribes()[0].ServerKey()})) + + return params + }, + assertResult: func(t *testing.T, params domain.CountTribesParams, count int) { + t.Helper() + + listParams := domain.NewListTribesParams() + require.NoError(t, listParams.SetServerKeys(params.ServerKeys())) + + res, err := repos.tribe.List(ctx, listParams) + require.NoError(t, err) + + assert.Len(t, res.Tribes(), count) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + assertError := tt.assertError + if assertError == nil { + assertError = func(t *testing.T, err error) { + t.Helper() + require.NoError(t, err) + } + } + + params := tt.params(t) + + cnt, err := repos.tribe.Count(ctx, params) + assertError(t, err) + tt.assertResult(t, params, cnt) + }) + } + }) + t.Run("Delete", func(t *testing.T) { t.Parallel() diff --git a/internal/app/service_player.go b/internal/app/service_player.go index 680be13..4070600 100644 --- a/internal/app/service_player.go +++ b/internal/app/service_player.go @@ -11,6 +11,7 @@ type PlayerRepository interface { CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error) ListWithRelations(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersWithRelationsResult, error) + Count(ctx context.Context, params domain.CountPlayersParams) (int, error) // Delete marks players with the given serverKey and ids as deleted (sets deleted at to now). // In addition, Delete sets TribeID to null. // @@ -51,10 +52,21 @@ func (svc *PlayerService) Sync(ctx context.Context, serverSyncedPayload domain.S return fmt.Errorf("%s: couldn't delete players: %w", serverKey, err) } + countParams := domain.NewCountPlayersParams() + if err = countParams.SetServerKeys([]string{serverKey}); err != nil { + return fmt.Errorf("%s: %w", serverKey, err) + } + + numPlayers, err := svc.repo.Count(ctx, countParams) + if err != nil { + return fmt.Errorf("%s: %w", serverKey, err) + } + payload, err := domain.NewPlayersSyncedEventPayload( serverKey, serverURL, serverSyncedPayload.VersionCode(), + numPlayers, len(players), ) if err != nil { @@ -101,7 +113,7 @@ func (svc *PlayerService) createOrUpdateChunk(ctx context.Context, serverKey str if err := listParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil { return err } - if err := listParams.SetLimit(domain.PlayerListMaxLimit); err != nil { + if err := listParams.SetLimit(len(ids)); err != nil { return err } diff --git a/internal/app/service_server.go b/internal/app/service_server.go index bbb487c..5a95788 100644 --- a/internal/app/service_server.go +++ b/internal/app/service_server.go @@ -183,12 +183,24 @@ func (svc *ServerService) UpdateNumTribes(ctx context.Context, payload domain.Tr key := payload.ServerKey() var updateParams domain.UpdateServerParams + if err := updateParams.SetNumTribes(domain.NullInt{ + V: payload.NumTribes(), + Valid: true, + }); err != nil { + return fmt.Errorf("%s: %w", key, err) + } if err := updateParams.SetNumActiveTribes(domain.NullInt{ V: payload.NumActiveTribes(), Valid: true, }); err != nil { return fmt.Errorf("%s: %w", key, err) } + if err := updateParams.SetNumInactiveTribes(domain.NullInt{ + V: payload.NumInactiveTribes(), + Valid: true, + }); err != nil { + return fmt.Errorf("%s: %w", key, err) + } if err := updateParams.SetTribeDataSyncedAt(domain.NullTime{ V: time.Now(), Valid: true, @@ -203,12 +215,24 @@ func (svc *ServerService) UpdateNumPlayers(ctx context.Context, payload domain.P key := payload.ServerKey() var updateParams domain.UpdateServerParams + if err := updateParams.SetNumPlayers(domain.NullInt{ + V: payload.NumPlayers(), + Valid: true, + }); err != nil { + return fmt.Errorf("%s: %w", key, err) + } if err := updateParams.SetNumActivePlayers(domain.NullInt{ V: payload.NumActivePlayers(), Valid: true, }); err != nil { return fmt.Errorf("%s: %w", key, err) } + if err := updateParams.SetNumInactivePlayers(domain.NullInt{ + V: payload.NumInactivePlayers(), + Valid: true, + }); err != nil { + return fmt.Errorf("%s: %w", key, err) + } if err := updateParams.SetPlayerDataSyncedAt(domain.NullTime{ V: time.Now(), Valid: true, diff --git a/internal/app/service_tribe.go b/internal/app/service_tribe.go index 05a1997..7be23e3 100644 --- a/internal/app/service_tribe.go +++ b/internal/app/service_tribe.go @@ -11,6 +11,7 @@ type TribeRepository interface { CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error) + Count(ctx context.Context, params domain.CountTribesParams) (int, error) // Delete marks players with the given serverKey and ids as deleted (sets deleted at to now). // // https://en.wiktionary.org/wiki/soft_deletion @@ -44,10 +45,21 @@ func (svc *TribeService) Sync(ctx context.Context, serverSyncedPayload domain.Se return fmt.Errorf("%s: couldn't delete tribes: %w", serverKey, err) } + countParams := domain.NewCountTribesParams() + if err = countParams.SetServerKeys([]string{serverKey}); err != nil { + return fmt.Errorf("%s: %w", serverKey, err) + } + + numTribes, err := svc.repo.Count(ctx, countParams) + if err != nil { + return fmt.Errorf("%s: %w", serverKey, err) + } + payload, err := domain.NewTribesSyncedEventPayload( serverKey, serverURL, serverSyncedPayload.VersionCode(), + numTribes, len(tribes), ) if err != nil { @@ -94,7 +106,7 @@ func (svc *TribeService) createOrUpdateChunk(ctx context.Context, serverKey stri if err := listParams.SetSort([]domain.TribeSort{domain.TribeSortIDASC}); err != nil { return err } - if err := listParams.SetLimit(domain.TribeListMaxLimit); err != nil { + if err := listParams.SetLimit(len(ids)); err != nil { return err } diff --git a/internal/bun/bunmodel/server.go b/internal/bun/bunmodel/server.go index 8e16747..5c4ae8c 100644 --- a/internal/bun/bunmodel/server.go +++ b/internal/bun/bunmodel/server.go @@ -16,8 +16,12 @@ type Server struct { URL string `bun:"url,nullzero"` Open bool `bun:"open"` Special bool `bun:"special"` + NumPlayers int `bun:"num_players"` NumActivePlayers int `bun:"num_active_players"` + NumInactivePlayers int `bun:"num_inactive_players"` + NumTribes int `bun:"num_tribes"` NumActiveTribes int `bun:"num_active_tribes"` + NumInactiveTribes int `bun:"num_inactive_tribes"` NumVillages int `bun:"num_villages"` NumPlayerVillages int `bun:"num_player_villages"` NumBarbarianVillages int `bun:"num_barbarian_villages"` @@ -57,8 +61,12 @@ func (s Server) ToDomain() (domain.Server, error) { s.URL, s.Open, s.Special, + s.NumPlayers, s.NumActivePlayers, + s.NumInactivePlayers, + s.NumTribes, s.NumActiveTribes, + s.NumInactiveTribes, s.NumVillages, s.NumPlayerVillages, s.NumBarbarianVillages, @@ -84,7 +92,7 @@ func (s Server) ToDomain() (domain.Server, error) { func (s Server) ToMeta() (domain.ServerMeta, error) { converted, err := domain.UnmarshalServerMetaFromDatabase(s.Key, s.URL, s.Open) if err != nil { - return domain.ServerMeta{}, fmt.Errorf("couldn't construct domain.Server (key=%s): %w", s.Key, err) + return domain.ServerMeta{}, fmt.Errorf("couldn't construct domain.ServerMeta (key=%s): %w", s.Key, err) } return converted, nil } diff --git a/internal/bun/migrations/20240506051128_servers_add_columns.go b/internal/bun/migrations/20240506051128_servers_add_columns.go new file mode 100644 index 0000000..8f3d790 --- /dev/null +++ b/internal/bun/migrations/20240506051128_servers_add_columns.go @@ -0,0 +1,44 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/uptrace/bun" +) + +func init() { + migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { + for _, column := range getColumnsToAdd20240506051128() { + _, err := tx.ExecContext(ctx, "ALTER TABLE servers ADD ? bigint DEFAULT 0", bun.Safe(column)) + if err != nil { + return fmt.Errorf("%s: %w", column, err) + } + } + + return nil + }) + }, func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { + for _, column := range getColumnsToAdd20240506051128() { + _, err := tx.ExecContext(ctx, "ALTER TABLE servers DROP COLUMN ?", bun.Safe(column)) + if err != nil { + return fmt.Errorf("%s: %w", column, err) + } + } + + return nil + }) + }) +} + +func getColumnsToAdd20240506051128() []string { + return []string{ + "num_players", + "num_inactive_players", + "num_tribes", + "num_inactive_tribes", + } +} diff --git a/internal/bun/migrations/20240506053957_servers_fill_num_columns.go b/internal/bun/migrations/20240506053957_servers_fill_num_columns.go new file mode 100644 index 0000000..31d6d84 --- /dev/null +++ b/internal/bun/migrations/20240506053957_servers_fill_num_columns.go @@ -0,0 +1,48 @@ +package migrations + +import ( + "context" + "fmt" + + "gitea.dwysokinski.me/twhelp/core/internal/bun/bunmodel" + "github.com/uptrace/bun" +) + +func init() { + migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + var servers bunmodel.Servers + + if err := db.NewSelect().Model(&servers).Where("special = false").Scan(ctx); err != nil { + return fmt.Errorf("couldn't select servers from the db: %w", err) + } + + for _, s := range servers { + numPlayers, err := db.NewSelect().Model((*bunmodel.Player)(nil)).Where("server_key = ?", s.Key).Count(ctx) + if err != nil { + return fmt.Errorf("%s: numPlayers: %w", s.Key, err) + } + + numTribes, err := db.NewSelect().Model((*bunmodel.Tribe)(nil)).Where("server_key = ?", s.Key).Count(ctx) + if err != nil { + return fmt.Errorf("%s: numTribes: %w", s.Key, err) + } + + _, err = db.NewUpdate(). + Model((*bunmodel.Server)(nil)). + Returning("NULL"). + Where("key = ?", s.Key). + Set("num_players = ?", numPlayers). + Set("num_inactive_players = ?", numPlayers-s.NumActivePlayers). + Set("num_tribes = ?", numTribes). + Set("num_inactive_tribes = ?", numTribes-s.NumActiveTribes). + Exec(ctx) + if err != nil { + return fmt.Errorf("%s: %w", s.Key, err) + } + } + + return nil + }, func(_ context.Context, _ *bun.DB) error { + return nil + }) +} diff --git a/internal/domain/domaintest/server.go b/internal/domain/domaintest/server.go index af90e3d..7f90961 100644 --- a/internal/domain/domaintest/server.go +++ b/internal/domain/domaintest/server.go @@ -89,6 +89,10 @@ func NewServer(tb TestingTB, opts ...func(cfg *ServerConfig)) domain.Server { 0, 0, 0, + 0, + 0, + 0, + 0, NewServerConfig(tb), NewBuildingInfo(tb), NewUnitInfo(tb), diff --git a/internal/domain/message_payloads.go b/internal/domain/message_payloads.go index 6f61969..aa5e2f8 100644 --- a/internal/domain/message_payloads.go +++ b/internal/domain/message_payloads.go @@ -112,6 +112,7 @@ type TribesSyncedEventPayload struct { serverKey string serverURL *url.URL versionCode string + numTribes int numActiveTribes int } @@ -121,6 +122,7 @@ func NewTribesSyncedEventPayload( serverKey string, serverURL *url.URL, versionCode string, + numTribes int, numActiveTribes int, ) (TribesSyncedEventPayload, error) { if serverKey == "" { @@ -147,6 +149,14 @@ func NewTribesSyncedEventPayload( } } + if err := validateIntInRange(numTribes, 0, math.MaxInt); err != nil { + return TribesSyncedEventPayload{}, ValidationError{ + Model: tribesSyncedEventPayloadModelName, + Field: "numTribes", + Err: err, + } + } + if err := validateIntInRange(numActiveTribes, 0, math.MaxInt); err != nil { return TribesSyncedEventPayload{}, ValidationError{ Model: tribesSyncedEventPayloadModelName, @@ -159,6 +169,7 @@ func NewTribesSyncedEventPayload( serverKey: serverKey, serverURL: serverURL, versionCode: versionCode, + numTribes: numTribes, numActiveTribes: numActiveTribes, }, nil } @@ -175,14 +186,23 @@ func (p TribesSyncedEventPayload) VersionCode() string { return p.versionCode } +func (p TribesSyncedEventPayload) NumTribes() int { + return p.numTribes +} + func (p TribesSyncedEventPayload) NumActiveTribes() int { return p.numActiveTribes } +func (p TribesSyncedEventPayload) NumInactiveTribes() int { + return p.numTribes - p.numActiveTribes +} + type PlayersSyncedEventPayload struct { serverKey string serverURL *url.URL versionCode string + numPlayers int numActivePlayers int } @@ -192,6 +212,7 @@ func NewPlayersSyncedEventPayload( serverKey string, serverURL *url.URL, versionCode string, + numPlayers int, numActivePlayers int, ) (PlayersSyncedEventPayload, error) { if serverKey == "" { @@ -218,6 +239,14 @@ func NewPlayersSyncedEventPayload( } } + if err := validateIntInRange(numPlayers, 0, math.MaxInt); err != nil { + return PlayersSyncedEventPayload{}, ValidationError{ + Model: playersSyncedEventPayloadModelName, + Field: "numPlayers", + Err: err, + } + } + if err := validateIntInRange(numActivePlayers, 0, math.MaxInt); err != nil { return PlayersSyncedEventPayload{}, ValidationError{ Model: playersSyncedEventPayloadModelName, @@ -230,6 +259,7 @@ func NewPlayersSyncedEventPayload( serverKey: serverKey, serverURL: serverURL, versionCode: versionCode, + numPlayers: numPlayers, numActivePlayers: numActivePlayers, }, nil } @@ -246,10 +276,18 @@ func (p PlayersSyncedEventPayload) VersionCode() string { return p.versionCode } +func (p PlayersSyncedEventPayload) NumPlayers() int { + return p.numPlayers +} + func (p PlayersSyncedEventPayload) NumActivePlayers() int { return p.numActivePlayers } +func (p PlayersSyncedEventPayload) NumInactivePlayers() int { + return p.numPlayers - p.numActivePlayers +} + type VillagesSyncedEventPayload struct { serverKey string serverURL *url.URL diff --git a/internal/domain/message_payloads_test.go b/internal/domain/message_payloads_test.go index b292e13..d76f6c4 100644 --- a/internal/domain/message_payloads_test.go +++ b/internal/domain/message_payloads_test.go @@ -58,38 +58,46 @@ func TestNewTribesSyncedEventPayload(t *testing.T) { t.Parallel() server := domaintest.NewServer(t) + numTribes := gofakeit.IntRange(0, math.MaxInt) numActiveTribes := gofakeit.IntRange(0, math.MaxInt) payload, err := domain.NewTribesSyncedEventPayload( server.Key(), server.URL(), server.VersionCode(), + numTribes, numActiveTribes, ) require.NoError(t, err) assert.Equal(t, server.Key(), payload.ServerKey()) assert.Equal(t, server.URL(), payload.ServerURL()) assert.Equal(t, server.VersionCode(), payload.VersionCode()) + assert.Equal(t, numTribes, payload.NumTribes()) assert.Equal(t, numActiveTribes, payload.NumActiveTribes()) + assert.Equal(t, numTribes-numActiveTribes, payload.NumInactiveTribes()) } func TestNewPlayersSyncedEventPayload(t *testing.T) { t.Parallel() server := domaintest.NewServer(t) + numPlayers := gofakeit.IntRange(0, math.MaxInt) numActivePlayers := gofakeit.IntRange(0, math.MaxInt) payload, err := domain.NewPlayersSyncedEventPayload( server.Key(), server.URL(), server.VersionCode(), + numPlayers, numActivePlayers, ) require.NoError(t, err) assert.Equal(t, server.Key(), payload.ServerKey()) assert.Equal(t, server.URL(), payload.ServerURL()) assert.Equal(t, server.VersionCode(), payload.VersionCode()) + assert.Equal(t, numPlayers, payload.NumPlayers()) assert.Equal(t, numActivePlayers, payload.NumActivePlayers()) + assert.Equal(t, numPlayers-numActivePlayers, payload.NumInactivePlayers()) } func TestNewVillagesSyncedEventPayload(t *testing.T) { diff --git a/internal/domain/player.go b/internal/domain/player.go index 91e06a9..34e792c 100644 --- a/internal/domain/player.go +++ b/internal/domain/player.go @@ -1180,6 +1180,37 @@ func (res ListPlayersWithRelationsResult) Next() PlayerCursor { return res.next } +type CountPlayersParams struct { + serverKeys []string +} + +const countPlayersParamsModelName = "CountPlayersParams" + +func NewCountPlayersParams() CountPlayersParams { + return CountPlayersParams{} +} + +func (params *CountPlayersParams) ServerKeys() []string { + return params.serverKeys +} + +func (params *CountPlayersParams) SetServerKeys(serverKeys []string) error { + for i, sk := range serverKeys { + if err := validateServerKey(sk); err != nil { + return SliceElementValidationError{ + Model: countPlayersParamsModelName, + Field: "serverKeys", + Index: i, + Err: err, + } + } + } + + params.serverKeys = serverKeys + + return nil +} + type PlayerNotFoundError struct { ID int ServerKey string diff --git a/internal/domain/player_test.go b/internal/domain/player_test.go index d1c8bcf..cf8fcf8 100644 --- a/internal/domain/player_test.go +++ b/internal/domain/player_test.go @@ -1614,3 +1614,57 @@ func TestNewListPlayersWithRelationsResult(t *testing.T) { assert.True(t, res.Next().IsZero()) }) } + +func TestCountPlayersParams_SetServerKeys(t *testing.T) { + t.Parallel() + + type args struct { + serverKeys []string + } + + type test struct { + name string + args args + expectedErr error + } + + tests := []test{ + { + name: "OK", + args: args{ + serverKeys: []string{ + domaintest.RandServerKey(), + }, + }, + }, + } + + for _, serverKeyTest := range newServerKeyValidationTests() { + tests = append(tests, test{ + name: serverKeyTest.name, + args: args{ + serverKeys: []string{serverKeyTest.key}, + }, + expectedErr: domain.SliceElementValidationError{ + Model: "CountPlayersParams", + Field: "serverKeys", + Index: 0, + Err: serverKeyTest.expectedErr, + }, + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + params := domain.NewCountPlayersParams() + + require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.serverKeys, params.ServerKeys()) + }) + } +} diff --git a/internal/domain/server.go b/internal/domain/server.go index 8759dc7..acf93f8 100644 --- a/internal/domain/server.go +++ b/internal/domain/server.go @@ -14,8 +14,12 @@ type Server struct { url *url.URL open bool special bool + numPlayers int numActivePlayers int + numInactivePlayers int + numTribes int numActiveTribes int + numInactiveTribes int numVillages int numPlayerVillages int numBarbarianVillages int @@ -44,8 +48,12 @@ func UnmarshalServerFromDatabase( rawURL string, open bool, special bool, + numPlayers int, numActivePlayers int, + numInactivePlayers int, + numTribes int, numActiveTribes int, + numInactiveTribes int, numVillages int, numPlayerVillages int, numBarbarianVillages int, @@ -88,11 +96,16 @@ func UnmarshalServerFromDatabase( return Server{ key: key, + versionCode: versionCode, url: u, open: open, special: special, + numPlayers: numPlayers, numActivePlayers: numActivePlayers, + numInactivePlayers: numInactivePlayers, + numTribes: numTribes, numActiveTribes: numActiveTribes, + numInactiveTribes: numInactiveTribes, numVillages: numVillages, numPlayerVillages: numPlayerVillages, numBarbarianVillages: numBarbarianVillages, @@ -107,7 +120,6 @@ func UnmarshalServerFromDatabase( tribeSnapshotsCreatedAt: tribeSnapshotsCreatedAt, villageDataSyncedAt: villageDataSyncedAt, ennoblementDataSyncedAt: ennoblementDataSyncedAt, - versionCode: versionCode, }, nil } @@ -131,14 +143,30 @@ func (s Server) Special() bool { return s.special } +func (s Server) NumPlayers() int { + return s.numPlayers +} + func (s Server) NumActivePlayers() int { return s.numActivePlayers } +func (s Server) NumInactivePlayers() int { + return s.numInactivePlayers +} + +func (s Server) NumTribes() int { + return s.numTribes +} + func (s Server) NumActiveTribes() int { return s.numActiveTribes } +func (s Server) NumInactiveTribes() int { + return s.numInactiveTribes +} + func (s Server) NumVillages() int { return s.numVillages } @@ -377,9 +405,13 @@ type UpdateServerParams struct { config NullServerConfig buildingInfo NullBuildingInfo unitInfo NullUnitInfo + numTribes NullInt numActiveTribes NullInt + numInactiveTribes NullInt tribeDataSyncedAt NullTime + numPlayers NullInt numActivePlayers NullInt + numInactivePlayers NullInt playerDataSyncedAt NullTime numVillages NullInt numPlayerVillages NullInt @@ -420,6 +452,26 @@ func (params *UpdateServerParams) SetUnitInfo(unitInfo NullUnitInfo) error { return nil } +func (params *UpdateServerParams) NumTribes() NullInt { + return params.numTribes +} + +func (params *UpdateServerParams) SetNumTribes(num NullInt) error { + if num.Valid { + if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil { + return ValidationError{ + Model: updateServerParamsModelName, + Field: "numTribes", + Err: err, + } + } + } + + params.numTribes = num + + return nil +} + func (params *UpdateServerParams) NumActiveTribes() NullInt { return params.numActiveTribes } @@ -440,6 +492,26 @@ func (params *UpdateServerParams) SetNumActiveTribes(num NullInt) error { return nil } +func (params *UpdateServerParams) NumInactiveTribes() NullInt { + return params.numInactiveTribes +} + +func (params *UpdateServerParams) SetNumInactiveTribes(num NullInt) error { + if num.Valid { + if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil { + return ValidationError{ + Model: updateServerParamsModelName, + Field: "numInactiveTribes", + Err: err, + } + } + } + + params.numInactiveTribes = num + + return nil +} + func (params *UpdateServerParams) TribeDataSyncedAt() NullTime { return params.tribeDataSyncedAt } @@ -449,6 +521,26 @@ func (params *UpdateServerParams) SetTribeDataSyncedAt(tribeDataSyncedAt NullTim return nil } +func (params *UpdateServerParams) NumPlayers() NullInt { + return params.numPlayers +} + +func (params *UpdateServerParams) SetNumPlayers(num NullInt) error { + if num.Valid { + if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil { + return ValidationError{ + Model: updateServerParamsModelName, + Field: "numPlayers", + Err: err, + } + } + } + + params.numPlayers = num + + return nil +} + func (params *UpdateServerParams) NumActivePlayers() NullInt { return params.numActivePlayers } @@ -469,6 +561,26 @@ func (params *UpdateServerParams) SetNumActivePlayers(num NullInt) error { return nil } +func (params *UpdateServerParams) NumInactivePlayers() NullInt { + return params.numInactivePlayers +} + +func (params *UpdateServerParams) SetNumInactivePlayers(num NullInt) error { + if num.Valid { + if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil { + return ValidationError{ + Model: updateServerParamsModelName, + Field: "numInactivePlayers", + Err: err, + } + } + } + + params.numInactivePlayers = num + + return nil +} + func (params *UpdateServerParams) PlayerDataSyncedAt() NullTime { return params.playerDataSyncedAt } diff --git a/internal/domain/server_test.go b/internal/domain/server_test.go index 58234a9..08f97d1 100644 --- a/internal/domain/server_test.go +++ b/internal/domain/server_test.go @@ -161,6 +161,69 @@ func TestNewCreateServerParams(t *testing.T) { } } +func TestUpdateServerParams_SetNumTribes(t *testing.T) { + t.Parallel() + + type args struct { + numTribes domain.NullInt + } + + tests := []struct { + name string + args args + expectedErr error + }{ + { + name: "OK", + args: args{ + numTribes: domain.NullInt{ + V: gofakeit.IntRange(0, math.MaxInt), + Valid: true, + }, + }, + }, + { + name: "OK: null value", + args: args{ + numTribes: domain.NullInt{ + Valid: false, + }, + }, + }, + { + name: "ERR: numTribes < 0", + args: args{ + numTribes: domain.NullInt{ + V: -1, + Valid: true, + }, + }, + expectedErr: domain.ValidationError{ + Model: "UpdateServerParams", + Field: "numTribes", + Err: domain.MinGreaterEqualError{ + Min: 0, + Current: -1, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var params domain.UpdateServerParams + + require.ErrorIs(t, params.SetNumTribes(tt.args.numTribes), tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.numTribes, params.NumTribes()) + }) + } +} + func TestUpdateServerParams_SetNumActiveTribes(t *testing.T) { t.Parallel() @@ -224,6 +287,132 @@ func TestUpdateServerParams_SetNumActiveTribes(t *testing.T) { } } +func TestUpdateServerParams_SetNumInactiveTribes(t *testing.T) { + t.Parallel() + + type args struct { + numInactiveTribes domain.NullInt + } + + tests := []struct { + name string + args args + expectedErr error + }{ + { + name: "OK", + args: args{ + numInactiveTribes: domain.NullInt{ + V: gofakeit.IntRange(0, math.MaxInt), + Valid: true, + }, + }, + }, + { + name: "OK: null value", + args: args{ + numInactiveTribes: domain.NullInt{ + Valid: false, + }, + }, + }, + { + name: "ERR: numInactiveTribes < 0", + args: args{ + numInactiveTribes: domain.NullInt{ + V: -1, + Valid: true, + }, + }, + expectedErr: domain.ValidationError{ + Model: "UpdateServerParams", + Field: "numInactiveTribes", + Err: domain.MinGreaterEqualError{ + Min: 0, + Current: -1, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var params domain.UpdateServerParams + + require.ErrorIs(t, params.SetNumInactiveTribes(tt.args.numInactiveTribes), tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.numInactiveTribes, params.NumInactiveTribes()) + }) + } +} + +func TestUpdateServerParams_SetNumPlayers(t *testing.T) { + t.Parallel() + + type args struct { + numPlayers domain.NullInt + } + + tests := []struct { + name string + args args + expectedErr error + }{ + { + name: "OK", + args: args{ + numPlayers: domain.NullInt{ + V: gofakeit.IntRange(0, math.MaxInt), + Valid: true, + }, + }, + }, + { + name: "OK: null value", + args: args{ + numPlayers: domain.NullInt{ + Valid: false, + }, + }, + }, + { + name: "ERR: numPlayers < 0", + args: args{ + numPlayers: domain.NullInt{ + V: -1, + Valid: true, + }, + }, + expectedErr: domain.ValidationError{ + Model: "UpdateServerParams", + Field: "numPlayers", + Err: domain.MinGreaterEqualError{ + Min: 0, + Current: -1, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var params domain.UpdateServerParams + + require.ErrorIs(t, params.SetNumPlayers(tt.args.numPlayers), tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.numPlayers, params.NumPlayers()) + }) + } +} + func TestUpdateServerParams_SetNumActivePlayers(t *testing.T) { t.Parallel() @@ -287,6 +476,69 @@ func TestUpdateServerParams_SetNumActivePlayers(t *testing.T) { } } +func TestUpdateServerParams_SetNumInactivePlayers(t *testing.T) { + t.Parallel() + + type args struct { + numInactivePlayers domain.NullInt + } + + tests := []struct { + name string + args args + expectedErr error + }{ + { + name: "OK", + args: args{ + numInactivePlayers: domain.NullInt{ + V: gofakeit.IntRange(0, math.MaxInt), + Valid: true, + }, + }, + }, + { + name: "OK: null value", + args: args{ + numInactivePlayers: domain.NullInt{ + Valid: false, + }, + }, + }, + { + name: "ERR: numInactivePlayers < 0", + args: args{ + numInactivePlayers: domain.NullInt{ + V: -1, + Valid: true, + }, + }, + expectedErr: domain.ValidationError{ + Model: "UpdateServerParams", + Field: "numInactivePlayers", + Err: domain.MinGreaterEqualError{ + Min: 0, + Current: -1, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var params domain.UpdateServerParams + + require.ErrorIs(t, params.SetNumInactivePlayers(tt.args.numInactivePlayers), tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.numInactivePlayers, params.NumInactivePlayers()) + }) + } +} + func TestUpdateServerParams_SetNumVillages(t *testing.T) { t.Parallel() diff --git a/internal/domain/tribe.go b/internal/domain/tribe.go index 374101d..4fa9c4d 100644 --- a/internal/domain/tribe.go +++ b/internal/domain/tribe.go @@ -969,6 +969,37 @@ func (res ListTribesResult) Next() TribeCursor { return res.next } +type CountTribesParams struct { + serverKeys []string +} + +const countTribesParamsModelName = "CountTribesParams" + +func NewCountTribesParams() CountTribesParams { + return CountTribesParams{} +} + +func (params *CountTribesParams) ServerKeys() []string { + return params.serverKeys +} + +func (params *CountTribesParams) SetServerKeys(serverKeys []string) error { + for i, sk := range serverKeys { + if err := validateServerKey(sk); err != nil { + return SliceElementValidationError{ + Model: countTribesParamsModelName, + Field: "serverKeys", + Index: i, + Err: err, + } + } + } + + params.serverKeys = serverKeys + + return nil +} + type TribeNotFoundError struct { ID int ServerKey string diff --git a/internal/domain/tribe_test.go b/internal/domain/tribe_test.go index 8546441..d23ca75 100644 --- a/internal/domain/tribe_test.go +++ b/internal/domain/tribe_test.go @@ -1205,3 +1205,57 @@ func TestNewListTribesResult(t *testing.T) { assert.True(t, res.Next().IsZero()) }) } + +func TestCountTribesParams_SetServerKeys(t *testing.T) { + t.Parallel() + + type args struct { + serverKeys []string + } + + type test struct { + name string + args args + expectedErr error + } + + tests := []test{ + { + name: "OK", + args: args{ + serverKeys: []string{ + domaintest.RandServerKey(), + }, + }, + }, + } + + for _, serverKeyTest := range newServerKeyValidationTests() { + tests = append(tests, test{ + name: serverKeyTest.name, + args: args{ + serverKeys: []string{serverKeyTest.key}, + }, + expectedErr: domain.SliceElementValidationError{ + Model: "CountTribesParams", + Field: "serverKeys", + Index: 0, + Err: serverKeyTest.expectedErr, + }, + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + params := domain.NewCountTribesParams() + + require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.args.serverKeys, params.ServerKeys()) + }) + } +} diff --git a/internal/port/consumer_data_sync_test.go b/internal/port/consumer_data_sync_test.go index 5822835..bc452f3 100644 --- a/internal/port/consumer_data_sync_test.go +++ b/internal/port/consumer_data_sync_test.go @@ -248,8 +248,12 @@ func TestDataSync(t *testing.T) { assert.Equal(collect, expected["URL"], actual.URL().String(), msg) assert.Equal(collect, expected["Open"], actual.Open(), msg) assert.Equal(collect, expected["VersionCode"], actual.VersionCode(), msg) + assert.EqualValues(collect, expected["NumPlayers"], actual.NumPlayers(), msg) assert.EqualValues(collect, expected["NumActivePlayers"], actual.NumActivePlayers(), msg) + assert.EqualValues(collect, expected["NumInactivePlayers"], actual.NumInactivePlayers(), msg) + assert.EqualValues(collect, expected["NumTribes"], actual.NumTribes(), msg) assert.EqualValues(collect, expected["NumActiveTribes"], actual.NumActiveTribes(), msg) + assert.EqualValues(collect, expected["NumInactiveTribes"], actual.NumInactiveTribes(), msg) assert.EqualValues(collect, expected["NumVillages"], actual.NumVillages(), msg) assert.EqualValues(collect, expected["NumPlayerVillages"], actual.NumPlayerVillages(), msg) assert.EqualValues(collect, expected["NumBonusVillages"], actual.NumBonusVillages(), msg) @@ -257,8 +261,7 @@ func TestDataSync(t *testing.T) { collect, expected["NumBarbarianVillages"], actual.NumBarbarianVillages(), - "Key=%s", - expected["Key"], + msg, ) assert.WithinDuration(collect, time.Now(), actual.PlayerDataSyncedAt(), time.Minute, msg) assert.WithinDuration(collect, time.Now(), actual.TribeDataSyncedAt(), time.Minute, msg) @@ -267,8 +270,7 @@ func TestDataSync(t *testing.T) { collect, string(marshalJSON(collect, expected["Config"])), string(marshalJSON(collect, serverConfigToMap(actual.Config()))), - "Key=%s", - expected["Key"], + msg, ) assert.JSONEq( collect, diff --git a/internal/port/consumer_watermill_server.go b/internal/port/consumer_watermill_server.go index 454d023..3de84bc 100644 --- a/internal/port/consumer_watermill_server.go +++ b/internal/port/consumer_watermill_server.go @@ -155,6 +155,7 @@ func (c *ServerWatermillConsumer) updateNumTribes(msg *message.Message) error { rawPayload.ServerKey, rawPayload.ServerURL, rawPayload.VersionCode, + rawPayload.NumTribes, rawPayload.NumActiveTribes, ) if err != nil { @@ -181,6 +182,7 @@ func (c *ServerWatermillConsumer) updateNumPlayers(msg *message.Message) error { rawPayload.ServerKey, rawPayload.ServerURL, rawPayload.VersionCode, + rawPayload.NumPlayers, rawPayload.NumActivePlayers, ) if err != nil { diff --git a/internal/port/internal/apimodel/server.go b/internal/port/internal/apimodel/server.go index 5e30b2d..446d0b5 100644 --- a/internal/port/internal/apimodel/server.go +++ b/internal/port/internal/apimodel/server.go @@ -6,13 +6,17 @@ import ( func NewServer(s domain.Server) Server { converted := Server{ - Key: s.Key(), CreatedAt: s.CreatedAt(), - NumBarbarianVillages: s.NumBarbarianVillages(), - NumBonusVillages: s.NumBonusVillages(), - NumPlayerVillages: s.NumPlayerVillages(), + Key: s.Key(), NumActivePlayers: s.NumActivePlayers(), NumActiveTribes: s.NumActiveTribes(), + NumBarbarianVillages: s.NumBarbarianVillages(), + NumBonusVillages: s.NumBonusVillages(), + NumInactivePlayers: s.NumInactivePlayers(), + NumInactiveTribes: s.NumInactiveTribes(), + NumPlayerVillages: s.NumPlayerVillages(), + NumPlayers: s.NumPlayers(), + NumTribes: s.NumTribes(), NumVillages: s.NumVillages(), Open: s.Open(), Url: s.URL().String(), diff --git a/internal/port/testdata/datasync/stage1/expected/servers.json b/internal/port/testdata/datasync/stage1/expected/servers.json index 36df944..c90b373 100644 --- a/internal/port/testdata/datasync/stage1/expected/servers.json +++ b/internal/port/testdata/datasync/stage1/expected/servers.json @@ -1 +1 @@ -[{"Key":"pl181","URL":"https://pl181.plemiona.pl","Open":true,"Special":false,"NumActivePlayers":3129,"NumActiveTribes":283,"NumVillages":57633,"NumPlayerVillages":57107,"NumBarbarianVillages":526,"NumBonusVillages":3601,"Config":{"Speed":1,"UnitSpeed":1,"Moral":1,"Build":{"Destroy":1},"Misc":{"KillRanking":2,"Tutorial":5,"TradeCancelTime":300},"Commands":{"MillisArrival":1,"CommandCancelTime":600},"Newbie":{"Days":7,"RatioDays":60,"Ratio":20,"RemoveNewbieVillages":1},"Game":{"BuildtimeFormula":2,"Knight":3,"KnightNewItems":0,"Archer":1,"Tech":2,"FarmLimit":0,"Church":0,"Watchtower":0,"Stronghold":1,"FakeLimit":1,"BarbarianRise":0.003,"BarbarianShrink":1,"BarbarianMaxPoints":2000,"Scavenging":1,"Hauls":1,"HaulsBase":1000,"HaulsMax":100000,"BaseProduction":30,"Event":10,"SuppressEvents":0},"Buildings":{"CustomMain":-1,"CustomFarm":-1,"CustomStorage":-1,"CustomPlace":-1,"CustomBarracks":-1,"CustomChurch":-1,"CustomSmith":-1,"CustomWood":-1,"CustomStone":-1,"CustomIron":-1,"CustomMarket":-1,"CustomStable":-1,"CustomWall":-1,"CustomGarage":-1,"CustomHide":-1,"CustomSnob":-1,"CustomStatue":-1,"CustomWatchtower":-1},"Snob":{"Gold":1,"CheapRebuild":0,"Rise":2,"MaxDist":1000,"Factor":0.9,"CoinWood":28000,"CoinStone":30000,"CoinIron":25000,"NoBarbConquer":0},"Ally":{"NoHarm":0,"NoOtherSupport":1,"NoOtherSupportType":0,"AllytimeSupport":0,"NoLeave":0,"NoJoin":0,"Limit":50,"FixedAllies":0,"PointsMemberCount":0,"WarsMemberRequirement":5,"WarsPointsRequirement":15000,"WarsAutoacceptDays":7,"Levels":1,"XpRequirements":"v1"},"Coord":{"MapSize":1000,"Func":4,"EmptyVillages":70,"BonusVillages":10,"BonusNew":0,"Inner":8287,"SelectStart":1,"VillageMoveWait":336,"NobleRestart":1,"StartVillages":1},"Sitter":{"Allow":1},"Sleep":{"Active":0,"Delay":60,"Min":6,"Max":10,"MinAwake":12,"MaxAwake":36,"WarnTime":10},"Night":{"Active":1,"StartHour":23,"EndHour":8,"DefFactor":2,"Duration":14},"Win":{"Check":5}},"BuildingInfo":{"Main":{"MaxLevel":30,"MinLevel":1,"Wood":90,"Stone":80,"Iron":70,"Pop":5,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":900,"BuildTimeFactor":1.2},"Barracks":{"MaxLevel":25,"MinLevel":0,"Wood":200,"Stone":170,"Iron":90,"Pop":7,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":1800,"BuildTimeFactor":1.2},"Stable":{"MaxLevel":20,"MinLevel":0,"Wood":270,"Stone":240,"Iron":260,"Pop":8,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Garage":{"MaxLevel":15,"MinLevel":0,"Wood":300,"Stone":240,"Iron":260,"Pop":8,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Watchtower":{"MaxLevel":0,"MinLevel":0,"Wood":0,"Stone":0,"Iron":0,"Pop":0,"WoodFactor":0,"StoneFactor":0,"IronFactor":0,"PopFactor":0,"BuildTime":0,"BuildTimeFactor":0},"Snob":{"MaxLevel":1,"MinLevel":0,"Wood":15000,"Stone":25000,"Iron":10000,"Pop":80,"WoodFactor":2,"StoneFactor":2,"IronFactor":2,"PopFactor":1.17,"BuildTime":586800,"BuildTimeFactor":1.2},"Smith":{"MaxLevel":20,"MinLevel":0,"Wood":220,"Stone":180,"Iron":240,"Pop":20,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Place":{"MaxLevel":1,"MinLevel":0,"Wood":10,"Stone":40,"Iron":30,"Pop":0,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":10860,"BuildTimeFactor":1.2},"Statue":{"MaxLevel":1,"MinLevel":0,"Wood":220,"Stone":220,"Iron":220,"Pop":10,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":1500,"BuildTimeFactor":1.2},"Market":{"MaxLevel":25,"MinLevel":0,"Wood":100,"Stone":100,"Iron":100,"Pop":20,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":2700,"BuildTimeFactor":1.2},"Wood":{"MaxLevel":30,"MinLevel":0,"Wood":50,"Stone":60,"Iron":40,"Pop":5,"WoodFactor":1.25,"StoneFactor":1.275,"IronFactor":1.245,"PopFactor":1.155,"BuildTime":900,"BuildTimeFactor":1.2},"Stone":{"MaxLevel":30,"MinLevel":0,"Wood":65,"Stone":50,"Iron":40,"Pop":10,"WoodFactor":1.27,"StoneFactor":1.265,"IronFactor":1.24,"PopFactor":1.14,"BuildTime":900,"BuildTimeFactor":1.2},"Iron":{"MaxLevel":30,"MinLevel":0,"Wood":75,"Stone":65,"Iron":70,"Pop":10,"WoodFactor":1.252,"StoneFactor":1.275,"IronFactor":1.24,"PopFactor":1.17,"BuildTime":1080,"BuildTimeFactor":1.2},"Farm":{"MaxLevel":30,"MinLevel":1,"Wood":45,"Stone":40,"Iron":30,"Pop":0,"WoodFactor":1.3,"StoneFactor":1.32,"IronFactor":1.29,"PopFactor":1,"BuildTime":1200,"BuildTimeFactor":1.2},"Storage":{"MaxLevel":30,"MinLevel":1,"Wood":60,"Stone":50,"Iron":40,"Pop":0,"WoodFactor":1.265,"StoneFactor":1.27,"IronFactor":1.245,"PopFactor":1.15,"BuildTime":1020,"BuildTimeFactor":1.2},"Hide":{"MaxLevel":10,"MinLevel":0,"Wood":50,"Stone":60,"Iron":50,"Pop":2,"WoodFactor":1.25,"StoneFactor":1.25,"IronFactor":1.25,"PopFactor":1.17,"BuildTime":1800,"BuildTimeFactor":1.2},"Wall":{"MaxLevel":20,"MinLevel":0,"Wood":50,"Stone":100,"Iron":20,"Pop":5,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":3600,"BuildTimeFactor":1.2}},"UnitInfo":{"Spear":{"BuildTime":1020,"Pop":1,"Speed":18,"Attack":10,"Defense":15,"DefenseCavalry":45,"DefenseArcher":20,"Carry":25},"Sword":{"BuildTime":1500,"Pop":1,"Speed":22,"Attack":25,"Defense":50,"DefenseCavalry":15,"DefenseArcher":40,"Carry":15},"Axe":{"BuildTime":1320,"Pop":1,"Speed":18,"Attack":40,"Defense":10,"DefenseCavalry":5,"DefenseArcher":10,"Carry":10},"Archer":{"BuildTime":1800,"Pop":1,"Speed":18,"Attack":15,"Defense":50,"DefenseCavalry":40,"DefenseArcher":5,"Carry":10},"Spy":{"BuildTime":900,"Pop":2,"Speed":9,"Attack":0,"Defense":2,"DefenseCavalry":1,"DefenseArcher":2,"Carry":0},"Light":{"BuildTime":1800,"Pop":4,"Speed":10,"Attack":130,"Defense":30,"DefenseCavalry":40,"DefenseArcher":30,"Carry":80},"Marcher":{"BuildTime":2700,"Pop":5,"Speed":10,"Attack":120,"Defense":40,"DefenseCavalry":30,"DefenseArcher":50,"Carry":50},"Heavy":{"BuildTime":3600,"Pop":6,"Speed":11,"Attack":150,"Defense":200,"DefenseCavalry":80,"DefenseArcher":180,"Carry":50},"Ram":{"BuildTime":4800,"Pop":5,"Speed":30,"Attack":2,"Defense":20,"DefenseCavalry":50,"DefenseArcher":20,"Carry":0},"Catapult":{"BuildTime":7200,"Pop":8,"Speed":30,"Attack":100,"Defense":100,"DefenseCavalry":50,"DefenseArcher":100,"Carry":0},"Knight":{"BuildTime":21600,"Pop":10,"Speed":10,"Attack":150,"Defense":250,"DefenseCavalry":400,"DefenseArcher":150,"Carry":100},"Snob":{"BuildTime":18000,"Pop":100,"Speed":35,"Attack":30,"Defense":100,"DefenseCavalry":50,"DefenseArcher":100,"Carry":0},"Militia":{"BuildTime":1,"Pop":0,"Speed":0.016666666666667,"Attack":0,"Defense":15,"DefenseCavalry":45,"DefenseArcher":25,"Carry":0}},"CreatedAt":"2023-02-15T06:22:53.222293Z","PlayerDataUpdatedAt":"2023-02-15T06:22:53.960985Z","PlayerSnapshotsCreatedAt":"0001-01-01T00:00:00Z","TribeDataUpdatedAt":"2023-02-15T06:22:53.276482Z","TribeSnapshotsCreatedAt":"0001-01-01T00:00:00Z","VillageDataUpdatedAt":"2023-02-15T06:22:55.28473Z","EnnoblementDataUpdatedAt":"0001-01-01T00:00:00Z","VersionCode":"pl"}] +[{"Key":"pl181","URL":"https://pl181.plemiona.pl","Open":true,"Special":false,"NumPlayers":3129,"NumActivePlayers":3129,"NumInactivePlayers":0,"NumTribes":283,"NumActiveTribes":283,"NumInactiveTribes":0,"NumVillages":57633,"NumPlayerVillages":57107,"NumBarbarianVillages":526,"NumBonusVillages":3601,"Config":{"Speed":1,"UnitSpeed":1,"Moral":1,"Build":{"Destroy":1},"Misc":{"KillRanking":2,"Tutorial":5,"TradeCancelTime":300},"Commands":{"MillisArrival":1,"CommandCancelTime":600},"Newbie":{"Days":7,"RatioDays":60,"Ratio":20,"RemoveNewbieVillages":1},"Game":{"BuildtimeFormula":2,"Knight":3,"KnightNewItems":0,"Archer":1,"Tech":2,"FarmLimit":0,"Church":0,"Watchtower":0,"Stronghold":1,"FakeLimit":1,"BarbarianRise":0.003,"BarbarianShrink":1,"BarbarianMaxPoints":2000,"Scavenging":1,"Hauls":1,"HaulsBase":1000,"HaulsMax":100000,"BaseProduction":30,"Event":10,"SuppressEvents":0},"Buildings":{"CustomMain":-1,"CustomFarm":-1,"CustomStorage":-1,"CustomPlace":-1,"CustomBarracks":-1,"CustomChurch":-1,"CustomSmith":-1,"CustomWood":-1,"CustomStone":-1,"CustomIron":-1,"CustomMarket":-1,"CustomStable":-1,"CustomWall":-1,"CustomGarage":-1,"CustomHide":-1,"CustomSnob":-1,"CustomStatue":-1,"CustomWatchtower":-1},"Snob":{"Gold":1,"CheapRebuild":0,"Rise":2,"MaxDist":1000,"Factor":0.9,"CoinWood":28000,"CoinStone":30000,"CoinIron":25000,"NoBarbConquer":0},"Ally":{"NoHarm":0,"NoOtherSupport":1,"NoOtherSupportType":0,"AllytimeSupport":0,"NoLeave":0,"NoJoin":0,"Limit":50,"FixedAllies":0,"PointsMemberCount":0,"WarsMemberRequirement":5,"WarsPointsRequirement":15000,"WarsAutoacceptDays":7,"Levels":1,"XpRequirements":"v1"},"Coord":{"MapSize":1000,"Func":4,"EmptyVillages":70,"BonusVillages":10,"BonusNew":0,"Inner":8287,"SelectStart":1,"VillageMoveWait":336,"NobleRestart":1,"StartVillages":1},"Sitter":{"Allow":1},"Sleep":{"Active":0,"Delay":60,"Min":6,"Max":10,"MinAwake":12,"MaxAwake":36,"WarnTime":10},"Night":{"Active":1,"StartHour":23,"EndHour":8,"DefFactor":2,"Duration":14},"Win":{"Check":5}},"BuildingInfo":{"Main":{"MaxLevel":30,"MinLevel":1,"Wood":90,"Stone":80,"Iron":70,"Pop":5,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":900,"BuildTimeFactor":1.2},"Barracks":{"MaxLevel":25,"MinLevel":0,"Wood":200,"Stone":170,"Iron":90,"Pop":7,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":1800,"BuildTimeFactor":1.2},"Stable":{"MaxLevel":20,"MinLevel":0,"Wood":270,"Stone":240,"Iron":260,"Pop":8,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Garage":{"MaxLevel":15,"MinLevel":0,"Wood":300,"Stone":240,"Iron":260,"Pop":8,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Watchtower":{"MaxLevel":0,"MinLevel":0,"Wood":0,"Stone":0,"Iron":0,"Pop":0,"WoodFactor":0,"StoneFactor":0,"IronFactor":0,"PopFactor":0,"BuildTime":0,"BuildTimeFactor":0},"Snob":{"MaxLevel":1,"MinLevel":0,"Wood":15000,"Stone":25000,"Iron":10000,"Pop":80,"WoodFactor":2,"StoneFactor":2,"IronFactor":2,"PopFactor":1.17,"BuildTime":586800,"BuildTimeFactor":1.2},"Smith":{"MaxLevel":20,"MinLevel":0,"Wood":220,"Stone":180,"Iron":240,"Pop":20,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Place":{"MaxLevel":1,"MinLevel":0,"Wood":10,"Stone":40,"Iron":30,"Pop":0,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":10860,"BuildTimeFactor":1.2},"Statue":{"MaxLevel":1,"MinLevel":0,"Wood":220,"Stone":220,"Iron":220,"Pop":10,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":1500,"BuildTimeFactor":1.2},"Market":{"MaxLevel":25,"MinLevel":0,"Wood":100,"Stone":100,"Iron":100,"Pop":20,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":2700,"BuildTimeFactor":1.2},"Wood":{"MaxLevel":30,"MinLevel":0,"Wood":50,"Stone":60,"Iron":40,"Pop":5,"WoodFactor":1.25,"StoneFactor":1.275,"IronFactor":1.245,"PopFactor":1.155,"BuildTime":900,"BuildTimeFactor":1.2},"Stone":{"MaxLevel":30,"MinLevel":0,"Wood":65,"Stone":50,"Iron":40,"Pop":10,"WoodFactor":1.27,"StoneFactor":1.265,"IronFactor":1.24,"PopFactor":1.14,"BuildTime":900,"BuildTimeFactor":1.2},"Iron":{"MaxLevel":30,"MinLevel":0,"Wood":75,"Stone":65,"Iron":70,"Pop":10,"WoodFactor":1.252,"StoneFactor":1.275,"IronFactor":1.24,"PopFactor":1.17,"BuildTime":1080,"BuildTimeFactor":1.2},"Farm":{"MaxLevel":30,"MinLevel":1,"Wood":45,"Stone":40,"Iron":30,"Pop":0,"WoodFactor":1.3,"StoneFactor":1.32,"IronFactor":1.29,"PopFactor":1,"BuildTime":1200,"BuildTimeFactor":1.2},"Storage":{"MaxLevel":30,"MinLevel":1,"Wood":60,"Stone":50,"Iron":40,"Pop":0,"WoodFactor":1.265,"StoneFactor":1.27,"IronFactor":1.245,"PopFactor":1.15,"BuildTime":1020,"BuildTimeFactor":1.2},"Hide":{"MaxLevel":10,"MinLevel":0,"Wood":50,"Stone":60,"Iron":50,"Pop":2,"WoodFactor":1.25,"StoneFactor":1.25,"IronFactor":1.25,"PopFactor":1.17,"BuildTime":1800,"BuildTimeFactor":1.2},"Wall":{"MaxLevel":20,"MinLevel":0,"Wood":50,"Stone":100,"Iron":20,"Pop":5,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":3600,"BuildTimeFactor":1.2}},"UnitInfo":{"Spear":{"BuildTime":1020,"Pop":1,"Speed":18,"Attack":10,"Defense":15,"DefenseCavalry":45,"DefenseArcher":20,"Carry":25},"Sword":{"BuildTime":1500,"Pop":1,"Speed":22,"Attack":25,"Defense":50,"DefenseCavalry":15,"DefenseArcher":40,"Carry":15},"Axe":{"BuildTime":1320,"Pop":1,"Speed":18,"Attack":40,"Defense":10,"DefenseCavalry":5,"DefenseArcher":10,"Carry":10},"Archer":{"BuildTime":1800,"Pop":1,"Speed":18,"Attack":15,"Defense":50,"DefenseCavalry":40,"DefenseArcher":5,"Carry":10},"Spy":{"BuildTime":900,"Pop":2,"Speed":9,"Attack":0,"Defense":2,"DefenseCavalry":1,"DefenseArcher":2,"Carry":0},"Light":{"BuildTime":1800,"Pop":4,"Speed":10,"Attack":130,"Defense":30,"DefenseCavalry":40,"DefenseArcher":30,"Carry":80},"Marcher":{"BuildTime":2700,"Pop":5,"Speed":10,"Attack":120,"Defense":40,"DefenseCavalry":30,"DefenseArcher":50,"Carry":50},"Heavy":{"BuildTime":3600,"Pop":6,"Speed":11,"Attack":150,"Defense":200,"DefenseCavalry":80,"DefenseArcher":180,"Carry":50},"Ram":{"BuildTime":4800,"Pop":5,"Speed":30,"Attack":2,"Defense":20,"DefenseCavalry":50,"DefenseArcher":20,"Carry":0},"Catapult":{"BuildTime":7200,"Pop":8,"Speed":30,"Attack":100,"Defense":100,"DefenseCavalry":50,"DefenseArcher":100,"Carry":0},"Knight":{"BuildTime":21600,"Pop":10,"Speed":10,"Attack":150,"Defense":250,"DefenseCavalry":400,"DefenseArcher":150,"Carry":100},"Snob":{"BuildTime":18000,"Pop":100,"Speed":35,"Attack":30,"Defense":100,"DefenseCavalry":50,"DefenseArcher":100,"Carry":0},"Militia":{"BuildTime":1,"Pop":0,"Speed":0.016666666666667,"Attack":0,"Defense":15,"DefenseCavalry":45,"DefenseArcher":25,"Carry":0}},"CreatedAt":"2023-02-15T06:22:53.222293Z","PlayerDataUpdatedAt":"2023-02-15T06:22:53.960985Z","PlayerSnapshotsCreatedAt":"0001-01-01T00:00:00Z","TribeDataUpdatedAt":"2023-02-15T06:22:53.276482Z","TribeSnapshotsCreatedAt":"0001-01-01T00:00:00Z","VillageDataUpdatedAt":"2023-02-15T06:22:55.28473Z","EnnoblementDataUpdatedAt":"0001-01-01T00:00:00Z","VersionCode":"pl"}] diff --git a/internal/port/testdata/datasync/stage2/expected/servers.json b/internal/port/testdata/datasync/stage2/expected/servers.json index f9109c5..03b71ef 100644 --- a/internal/port/testdata/datasync/stage2/expected/servers.json +++ b/internal/port/testdata/datasync/stage2/expected/servers.json @@ -1 +1 @@ -[{"Key":"pl181","URL":"https://pl181.plemiona.pl","Open":true,"Special":false,"NumActivePlayers":3096,"NumActiveTribes":282,"NumVillages":57694,"NumPlayerVillages":57190,"NumBarbarianVillages":504,"NumBonusVillages":3606,"Config":{"Speed":1,"UnitSpeed":1,"Moral":1,"Build":{"Destroy":1},"Misc":{"KillRanking":2,"Tutorial":5,"TradeCancelTime":300},"Commands":{"MillisArrival":1,"CommandCancelTime":600},"Newbie":{"Days":7,"RatioDays":60,"Ratio":20,"RemoveNewbieVillages":1},"Game":{"BuildtimeFormula":2,"Knight":3,"KnightNewItems":0,"Archer":1,"Tech":2,"FarmLimit":0,"Church":0,"Watchtower":0,"Stronghold":1,"FakeLimit":1,"BarbarianRise":0.003,"BarbarianShrink":1,"BarbarianMaxPoints":2000,"Scavenging":1,"Hauls":1,"HaulsBase":1000,"HaulsMax":100000,"BaseProduction":30,"Event":10,"SuppressEvents":0},"Buildings":{"CustomMain":-1,"CustomFarm":-1,"CustomStorage":-1,"CustomPlace":-1,"CustomBarracks":-1,"CustomChurch":-1,"CustomSmith":-1,"CustomWood":-1,"CustomStone":-1,"CustomIron":-1,"CustomMarket":-1,"CustomStable":-1,"CustomWall":-1,"CustomGarage":-1,"CustomHide":-1,"CustomSnob":-1,"CustomStatue":-1,"CustomWatchtower":-1},"Snob":{"Gold":1,"CheapRebuild":0,"Rise":2,"MaxDist":1000,"Factor":0.9,"CoinWood":28000,"CoinStone":30000,"CoinIron":25000,"NoBarbConquer":0},"Ally":{"NoHarm":0,"NoOtherSupport":1,"NoOtherSupportType":0,"AllytimeSupport":0,"NoLeave":0,"NoJoin":0,"Limit":50,"FixedAllies":0,"PointsMemberCount":0,"WarsMemberRequirement":5,"WarsPointsRequirement":15000,"WarsAutoacceptDays":7,"Levels":1,"XpRequirements":"v1"},"Coord":{"MapSize":1000,"Func":4,"EmptyVillages":70,"BonusVillages":10,"BonusNew":0,"Inner":8287,"SelectStart":1,"VillageMoveWait":336,"NobleRestart":1,"StartVillages":1},"Sitter":{"Allow":1},"Sleep":{"Active":0,"Delay":60,"Min":6,"Max":10,"MinAwake":12,"MaxAwake":36,"WarnTime":10},"Night":{"Active":1,"StartHour":23,"EndHour":8,"DefFactor":2,"Duration":14},"Win":{"Check":5}},"BuildingInfo":{"Main":{"MaxLevel":30,"MinLevel":1,"Wood":90,"Stone":80,"Iron":70,"Pop":5,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":900,"BuildTimeFactor":1.2},"Barracks":{"MaxLevel":25,"MinLevel":0,"Wood":200,"Stone":170,"Iron":90,"Pop":7,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":1800,"BuildTimeFactor":1.2},"Stable":{"MaxLevel":20,"MinLevel":0,"Wood":270,"Stone":240,"Iron":260,"Pop":8,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Garage":{"MaxLevel":15,"MinLevel":0,"Wood":300,"Stone":240,"Iron":260,"Pop":8,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Watchtower":{"MaxLevel":0,"MinLevel":0,"Wood":0,"Stone":0,"Iron":0,"Pop":0,"WoodFactor":0,"StoneFactor":0,"IronFactor":0,"PopFactor":0,"BuildTime":0,"BuildTimeFactor":0},"Snob":{"MaxLevel":1,"MinLevel":0,"Wood":15000,"Stone":25000,"Iron":10000,"Pop":80,"WoodFactor":2,"StoneFactor":2,"IronFactor":2,"PopFactor":1.17,"BuildTime":586800,"BuildTimeFactor":1.2},"Smith":{"MaxLevel":20,"MinLevel":0,"Wood":220,"Stone":180,"Iron":240,"Pop":20,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Place":{"MaxLevel":1,"MinLevel":0,"Wood":10,"Stone":40,"Iron":30,"Pop":0,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":10860,"BuildTimeFactor":1.2},"Statue":{"MaxLevel":1,"MinLevel":0,"Wood":220,"Stone":220,"Iron":220,"Pop":10,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":1500,"BuildTimeFactor":1.2},"Market":{"MaxLevel":25,"MinLevel":0,"Wood":100,"Stone":100,"Iron":100,"Pop":20,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":2700,"BuildTimeFactor":1.2},"Wood":{"MaxLevel":30,"MinLevel":0,"Wood":50,"Stone":60,"Iron":40,"Pop":5,"WoodFactor":1.25,"StoneFactor":1.275,"IronFactor":1.245,"PopFactor":1.155,"BuildTime":900,"BuildTimeFactor":1.2},"Stone":{"MaxLevel":30,"MinLevel":0,"Wood":65,"Stone":50,"Iron":40,"Pop":10,"WoodFactor":1.27,"StoneFactor":1.265,"IronFactor":1.24,"PopFactor":1.14,"BuildTime":900,"BuildTimeFactor":1.2},"Iron":{"MaxLevel":30,"MinLevel":0,"Wood":75,"Stone":65,"Iron":70,"Pop":10,"WoodFactor":1.252,"StoneFactor":1.275,"IronFactor":1.24,"PopFactor":1.17,"BuildTime":1080,"BuildTimeFactor":1.2},"Farm":{"MaxLevel":30,"MinLevel":1,"Wood":45,"Stone":40,"Iron":30,"Pop":0,"WoodFactor":1.3,"StoneFactor":1.32,"IronFactor":1.29,"PopFactor":1,"BuildTime":1200,"BuildTimeFactor":1.2},"Storage":{"MaxLevel":30,"MinLevel":1,"Wood":60,"Stone":50,"Iron":40,"Pop":0,"WoodFactor":1.265,"StoneFactor":1.27,"IronFactor":1.245,"PopFactor":1.15,"BuildTime":1020,"BuildTimeFactor":1.2},"Hide":{"MaxLevel":10,"MinLevel":0,"Wood":50,"Stone":60,"Iron":50,"Pop":2,"WoodFactor":1.25,"StoneFactor":1.25,"IronFactor":1.25,"PopFactor":1.17,"BuildTime":1800,"BuildTimeFactor":1.2},"Wall":{"MaxLevel":20,"MinLevel":0,"Wood":50,"Stone":100,"Iron":20,"Pop":5,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":3600,"BuildTimeFactor":1.2}},"UnitInfo":{"Spear":{"BuildTime":1020,"Pop":1,"Speed":18,"Attack":10,"Defense":15,"DefenseCavalry":45,"DefenseArcher":20,"Carry":25},"Sword":{"BuildTime":1500,"Pop":1,"Speed":22,"Attack":25,"Defense":50,"DefenseCavalry":15,"DefenseArcher":40,"Carry":15},"Axe":{"BuildTime":1320,"Pop":1,"Speed":18,"Attack":40,"Defense":10,"DefenseCavalry":5,"DefenseArcher":10,"Carry":10},"Archer":{"BuildTime":1800,"Pop":1,"Speed":18,"Attack":15,"Defense":50,"DefenseCavalry":40,"DefenseArcher":5,"Carry":10},"Spy":{"BuildTime":900,"Pop":2,"Speed":9,"Attack":0,"Defense":2,"DefenseCavalry":1,"DefenseArcher":2,"Carry":0},"Light":{"BuildTime":1800,"Pop":4,"Speed":10,"Attack":130,"Defense":30,"DefenseCavalry":40,"DefenseArcher":30,"Carry":80},"Marcher":{"BuildTime":2700,"Pop":5,"Speed":10,"Attack":120,"Defense":40,"DefenseCavalry":30,"DefenseArcher":50,"Carry":50},"Heavy":{"BuildTime":3600,"Pop":6,"Speed":11,"Attack":150,"Defense":200,"DefenseCavalry":80,"DefenseArcher":180,"Carry":50},"Ram":{"BuildTime":4800,"Pop":5,"Speed":30,"Attack":2,"Defense":20,"DefenseCavalry":50,"DefenseArcher":20,"Carry":0},"Catapult":{"BuildTime":7200,"Pop":8,"Speed":30,"Attack":100,"Defense":100,"DefenseCavalry":50,"DefenseArcher":100,"Carry":0},"Knight":{"BuildTime":21600,"Pop":10,"Speed":10,"Attack":150,"Defense":250,"DefenseCavalry":400,"DefenseArcher":150,"Carry":100},"Snob":{"BuildTime":18000,"Pop":100,"Speed":35,"Attack":30,"Defense":100,"DefenseCavalry":50,"DefenseArcher":100,"Carry":0},"Militia":{"BuildTime":1,"Pop":0,"Speed":0.016666666666667,"Attack":0,"Defense":15,"DefenseCavalry":45,"DefenseArcher":25,"Carry":0}},"CreatedAt":"2023-02-16T05:25:48.890567Z","PlayerDataUpdatedAt":"2023-02-16T05:25:56.474057Z","PlayerSnapshotsCreatedAt":"0001-01-01T00:00:00Z","TribeDataUpdatedAt":"2023-02-16T05:25:56.229396Z","TribeSnapshotsCreatedAt":"0001-01-01T00:00:00Z","VillageDataUpdatedAt":"2023-02-16T05:25:58.24375Z","EnnoblementDataUpdatedAt":"0001-01-01T00:00:00Z","VersionCode":"pl"}] +[{"Key":"pl181","URL":"https://pl181.plemiona.pl","Open":true,"Special":false,"NumPlayers":3151,"NumActivePlayers":3096,"NumInactivePlayers": 55,"NumTribes":284,"NumActiveTribes":282,"NumInactiveTribes":2,"NumVillages":57694,"NumPlayerVillages":57190,"NumBarbarianVillages":504,"NumBonusVillages":3606,"Config":{"Speed":1,"UnitSpeed":1,"Moral":1,"Build":{"Destroy":1},"Misc":{"KillRanking":2,"Tutorial":5,"TradeCancelTime":300},"Commands":{"MillisArrival":1,"CommandCancelTime":600},"Newbie":{"Days":7,"RatioDays":60,"Ratio":20,"RemoveNewbieVillages":1},"Game":{"BuildtimeFormula":2,"Knight":3,"KnightNewItems":0,"Archer":1,"Tech":2,"FarmLimit":0,"Church":0,"Watchtower":0,"Stronghold":1,"FakeLimit":1,"BarbarianRise":0.003,"BarbarianShrink":1,"BarbarianMaxPoints":2000,"Scavenging":1,"Hauls":1,"HaulsBase":1000,"HaulsMax":100000,"BaseProduction":30,"Event":10,"SuppressEvents":0},"Buildings":{"CustomMain":-1,"CustomFarm":-1,"CustomStorage":-1,"CustomPlace":-1,"CustomBarracks":-1,"CustomChurch":-1,"CustomSmith":-1,"CustomWood":-1,"CustomStone":-1,"CustomIron":-1,"CustomMarket":-1,"CustomStable":-1,"CustomWall":-1,"CustomGarage":-1,"CustomHide":-1,"CustomSnob":-1,"CustomStatue":-1,"CustomWatchtower":-1},"Snob":{"Gold":1,"CheapRebuild":0,"Rise":2,"MaxDist":1000,"Factor":0.9,"CoinWood":28000,"CoinStone":30000,"CoinIron":25000,"NoBarbConquer":0},"Ally":{"NoHarm":0,"NoOtherSupport":1,"NoOtherSupportType":0,"AllytimeSupport":0,"NoLeave":0,"NoJoin":0,"Limit":50,"FixedAllies":0,"PointsMemberCount":0,"WarsMemberRequirement":5,"WarsPointsRequirement":15000,"WarsAutoacceptDays":7,"Levels":1,"XpRequirements":"v1"},"Coord":{"MapSize":1000,"Func":4,"EmptyVillages":70,"BonusVillages":10,"BonusNew":0,"Inner":8287,"SelectStart":1,"VillageMoveWait":336,"NobleRestart":1,"StartVillages":1},"Sitter":{"Allow":1},"Sleep":{"Active":0,"Delay":60,"Min":6,"Max":10,"MinAwake":12,"MaxAwake":36,"WarnTime":10},"Night":{"Active":1,"StartHour":23,"EndHour":8,"DefFactor":2,"Duration":14},"Win":{"Check":5}},"BuildingInfo":{"Main":{"MaxLevel":30,"MinLevel":1,"Wood":90,"Stone":80,"Iron":70,"Pop":5,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":900,"BuildTimeFactor":1.2},"Barracks":{"MaxLevel":25,"MinLevel":0,"Wood":200,"Stone":170,"Iron":90,"Pop":7,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":1800,"BuildTimeFactor":1.2},"Stable":{"MaxLevel":20,"MinLevel":0,"Wood":270,"Stone":240,"Iron":260,"Pop":8,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Garage":{"MaxLevel":15,"MinLevel":0,"Wood":300,"Stone":240,"Iron":260,"Pop":8,"WoodFactor":1.26,"StoneFactor":1.28,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Watchtower":{"MaxLevel":0,"MinLevel":0,"Wood":0,"Stone":0,"Iron":0,"Pop":0,"WoodFactor":0,"StoneFactor":0,"IronFactor":0,"PopFactor":0,"BuildTime":0,"BuildTimeFactor":0},"Snob":{"MaxLevel":1,"MinLevel":0,"Wood":15000,"Stone":25000,"Iron":10000,"Pop":80,"WoodFactor":2,"StoneFactor":2,"IronFactor":2,"PopFactor":1.17,"BuildTime":586800,"BuildTimeFactor":1.2},"Smith":{"MaxLevel":20,"MinLevel":0,"Wood":220,"Stone":180,"Iron":240,"Pop":20,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":6000,"BuildTimeFactor":1.2},"Place":{"MaxLevel":1,"MinLevel":0,"Wood":10,"Stone":40,"Iron":30,"Pop":0,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":10860,"BuildTimeFactor":1.2},"Statue":{"MaxLevel":1,"MinLevel":0,"Wood":220,"Stone":220,"Iron":220,"Pop":10,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":1500,"BuildTimeFactor":1.2},"Market":{"MaxLevel":25,"MinLevel":0,"Wood":100,"Stone":100,"Iron":100,"Pop":20,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":2700,"BuildTimeFactor":1.2},"Wood":{"MaxLevel":30,"MinLevel":0,"Wood":50,"Stone":60,"Iron":40,"Pop":5,"WoodFactor":1.25,"StoneFactor":1.275,"IronFactor":1.245,"PopFactor":1.155,"BuildTime":900,"BuildTimeFactor":1.2},"Stone":{"MaxLevel":30,"MinLevel":0,"Wood":65,"Stone":50,"Iron":40,"Pop":10,"WoodFactor":1.27,"StoneFactor":1.265,"IronFactor":1.24,"PopFactor":1.14,"BuildTime":900,"BuildTimeFactor":1.2},"Iron":{"MaxLevel":30,"MinLevel":0,"Wood":75,"Stone":65,"Iron":70,"Pop":10,"WoodFactor":1.252,"StoneFactor":1.275,"IronFactor":1.24,"PopFactor":1.17,"BuildTime":1080,"BuildTimeFactor":1.2},"Farm":{"MaxLevel":30,"MinLevel":1,"Wood":45,"Stone":40,"Iron":30,"Pop":0,"WoodFactor":1.3,"StoneFactor":1.32,"IronFactor":1.29,"PopFactor":1,"BuildTime":1200,"BuildTimeFactor":1.2},"Storage":{"MaxLevel":30,"MinLevel":1,"Wood":60,"Stone":50,"Iron":40,"Pop":0,"WoodFactor":1.265,"StoneFactor":1.27,"IronFactor":1.245,"PopFactor":1.15,"BuildTime":1020,"BuildTimeFactor":1.2},"Hide":{"MaxLevel":10,"MinLevel":0,"Wood":50,"Stone":60,"Iron":50,"Pop":2,"WoodFactor":1.25,"StoneFactor":1.25,"IronFactor":1.25,"PopFactor":1.17,"BuildTime":1800,"BuildTimeFactor":1.2},"Wall":{"MaxLevel":20,"MinLevel":0,"Wood":50,"Stone":100,"Iron":20,"Pop":5,"WoodFactor":1.26,"StoneFactor":1.275,"IronFactor":1.26,"PopFactor":1.17,"BuildTime":3600,"BuildTimeFactor":1.2}},"UnitInfo":{"Spear":{"BuildTime":1020,"Pop":1,"Speed":18,"Attack":10,"Defense":15,"DefenseCavalry":45,"DefenseArcher":20,"Carry":25},"Sword":{"BuildTime":1500,"Pop":1,"Speed":22,"Attack":25,"Defense":50,"DefenseCavalry":15,"DefenseArcher":40,"Carry":15},"Axe":{"BuildTime":1320,"Pop":1,"Speed":18,"Attack":40,"Defense":10,"DefenseCavalry":5,"DefenseArcher":10,"Carry":10},"Archer":{"BuildTime":1800,"Pop":1,"Speed":18,"Attack":15,"Defense":50,"DefenseCavalry":40,"DefenseArcher":5,"Carry":10},"Spy":{"BuildTime":900,"Pop":2,"Speed":9,"Attack":0,"Defense":2,"DefenseCavalry":1,"DefenseArcher":2,"Carry":0},"Light":{"BuildTime":1800,"Pop":4,"Speed":10,"Attack":130,"Defense":30,"DefenseCavalry":40,"DefenseArcher":30,"Carry":80},"Marcher":{"BuildTime":2700,"Pop":5,"Speed":10,"Attack":120,"Defense":40,"DefenseCavalry":30,"DefenseArcher":50,"Carry":50},"Heavy":{"BuildTime":3600,"Pop":6,"Speed":11,"Attack":150,"Defense":200,"DefenseCavalry":80,"DefenseArcher":180,"Carry":50},"Ram":{"BuildTime":4800,"Pop":5,"Speed":30,"Attack":2,"Defense":20,"DefenseCavalry":50,"DefenseArcher":20,"Carry":0},"Catapult":{"BuildTime":7200,"Pop":8,"Speed":30,"Attack":100,"Defense":100,"DefenseCavalry":50,"DefenseArcher":100,"Carry":0},"Knight":{"BuildTime":21600,"Pop":10,"Speed":10,"Attack":150,"Defense":250,"DefenseCavalry":400,"DefenseArcher":150,"Carry":100},"Snob":{"BuildTime":18000,"Pop":100,"Speed":35,"Attack":30,"Defense":100,"DefenseCavalry":50,"DefenseArcher":100,"Carry":0},"Militia":{"BuildTime":1,"Pop":0,"Speed":0.016666666666667,"Attack":0,"Defense":15,"DefenseCavalry":45,"DefenseArcher":25,"Carry":0}},"CreatedAt":"2023-02-16T05:25:48.890567Z","PlayerDataUpdatedAt":"2023-02-16T05:25:56.474057Z","PlayerSnapshotsCreatedAt":"0001-01-01T00:00:00Z","TribeDataUpdatedAt":"2023-02-16T05:25:56.229396Z","TribeSnapshotsCreatedAt":"0001-01-01T00:00:00Z","VillageDataUpdatedAt":"2023-02-16T05:25:58.24375Z","EnnoblementDataUpdatedAt":"0001-01-01T00:00:00Z","VersionCode":"pl"}] diff --git a/internal/watermill/watermillmsg/player.go b/internal/watermill/watermillmsg/player.go index 0dd50f5..0a65e4e 100644 --- a/internal/watermill/watermillmsg/player.go +++ b/internal/watermill/watermillmsg/player.go @@ -6,5 +6,6 @@ type PlayersSyncedEventPayload struct { ServerKey string `json:"serverKey"` ServerURL *url.URL `json:"serverUrl"` VersionCode string `json:"versionCode"` + NumPlayers int `json:"numPlayers"` NumActivePlayers int `json:"numActivePlayers"` } diff --git a/internal/watermill/watermillmsg/tribe.go b/internal/watermill/watermillmsg/tribe.go index 2cc69c5..229ce92 100644 --- a/internal/watermill/watermillmsg/tribe.go +++ b/internal/watermill/watermillmsg/tribe.go @@ -6,5 +6,6 @@ type TribesSyncedEventPayload struct { ServerKey string `json:"serverKey"` ServerURL *url.URL `json:"serverUrl"` VersionCode string `json:"versionCode"` + NumTribes int `json:"numTribes"` NumActiveTribes int `json:"numActiveTribes"` }