From 476bf9b31c5707ef1f14c9ffb3e8f6c478f18c68 Mon Sep 17 00:00:00 2001 From: Malte <42342921+ccev@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:44:57 +0200 Subject: [PATCH 1/2] feat: add raid seed, cpm and stamina to gym records --- decoder/api_gym.go | 170 ++++++++++++++++++---------------- decoder/api_gym_test.go | 17 ++-- decoder/gym.go | 110 +++++++++++++--------- decoder/gym_decode.go | 4 + decoder/gym_state.go | 14 ++- decoder/writebehind_batch.go | 9 +- sql/55_gym_raid_fields.up.sql | 4 + 7 files changed, 189 insertions(+), 139 deletions(-) create mode 100644 sql/55_gym_raid_fields.up.sql diff --git a/decoder/api_gym.go b/decoder/api_gym.go index f704b580..f069f40c 100644 --- a/decoder/api_gym.go +++ b/decoder/api_gym.go @@ -14,92 +14,98 @@ import ( // represented as pointers (nil => JSON null) without omitempty so every key is // always present. type ApiGymResult struct { - Id string `json:"id" doc:"Fort ID of the gym"` - Lat float64 `json:"lat" doc:"Latitude of the gym"` - Lon float64 `json:"lon" doc:"Longitude of the gym"` - Name *string `json:"name" doc:"Name of the gym"` - Url *string `json:"url" doc:"Image URL of the gym"` - LastModifiedTimestamp *int64 `json:"last_modified_timestamp" doc:"Unix timestamp when the gym was last modified in-game"` - RaidEndTimestamp *int64 `json:"raid_end_timestamp" doc:"Unix timestamp when the current raid ends"` - RaidSpawnTimestamp *int64 `json:"raid_spawn_timestamp" doc:"Unix timestamp when the current raid egg spawned"` - RaidBattleTimestamp *int64 `json:"raid_battle_timestamp" doc:"Unix timestamp when the current raid battle begins"` - Updated int64 `json:"updated" doc:"Unix timestamp when the record was last updated"` - RaidPokemonId *int64 `json:"raid_pokemon_id" doc:"Pokedex ID of the raid boss"` - GuardingPokemonId *int64 `json:"guarding_pokemon_id" doc:"Pokedex ID of the pokemon guarding the gym"` - GuardingPokemonDisplay *string `json:"guarding_pokemon_display" doc:"Display details of the guarding pokemon"` - AvailableSlots *int64 `json:"available_slots" doc:"Number of open defender slots"` - TeamId *int64 `json:"team_id" doc:"ID of the team controlling the gym"` - RaidLevel *int64 `json:"raid_level" doc:"Level/tier of the current raid"` - Enabled *int64 `json:"enabled" doc:"Whether the gym is enabled"` - ExRaidEligible *int64 `json:"ex_raid_eligible" doc:"Whether the gym is eligible for EX raids"` - InBattle *int64 `json:"in_battle" doc:"Whether the gym is currently in battle"` - RaidPokemonMove1 *int64 `json:"raid_pokemon_move_1" doc:"Fast move ID of the raid boss"` - RaidPokemonMove2 *int64 `json:"raid_pokemon_move_2" doc:"Charge move ID of the raid boss"` - RaidPokemonForm *int64 `json:"raid_pokemon_form" doc:"Form ID of the raid boss"` - RaidPokemonAlignment *int64 `json:"raid_pokemon_alignment" doc:"Alignment of the raid boss"` - RaidPokemonCp *int64 `json:"raid_pokemon_cp" doc:"Combat power of the raid boss"` - RaidIsExclusive *int64 `json:"raid_is_exclusive" doc:"Whether the current raid is exclusive (EX)"` - CellId *int64 `json:"cell_id" doc:"S2 cell ID the gym belongs to"` - Deleted bool `json:"deleted" doc:"Whether the gym has been deleted"` - TotalCp *int64 `json:"total_cp" doc:"Total combat power of the gym defenders"` - FirstSeenTimestamp int64 `json:"first_seen_timestamp" doc:"Unix timestamp when the gym was first seen"` - RaidPokemonGender *int64 `json:"raid_pokemon_gender" doc:"Gender of the raid boss"` - SponsorId *int64 `json:"sponsor_id" doc:"Sponsor ID of the gym, if sponsored"` - PartnerId *string `json:"partner_id" doc:"Partner ID of the gym, if partnered"` - RaidPokemonCostume *int64 `json:"raid_pokemon_costume" doc:"Costume ID of the raid boss"` - RaidPokemonEvolution *int64 `json:"raid_pokemon_evolution" doc:"Evolution ID of the raid boss (e.g. mega)"` - ArScanEligible *int64 `json:"ar_scan_eligible" doc:"Whether the gym is eligible for AR scanning"` - PowerUpLevel *int64 `json:"power_up_level" doc:"Power-up level of the gym"` - PowerUpPoints *int64 `json:"power_up_points" doc:"Power-up points accumulated for the gym"` - PowerUpEndTimestamp *int64 `json:"power_up_end_timestamp" doc:"Unix timestamp when the power-up ends"` - Description *string `json:"description" doc:"Description of the gym"` - Defenders *string `json:"defenders" doc:"Serialized defender pokemon data"` - Rsvps *string `json:"rsvps" doc:"Serialized raid RSVP data"` + Id string `json:"id" doc:"Fort ID of the gym"` + Lat float64 `json:"lat" doc:"Latitude of the gym"` + Lon float64 `json:"lon" doc:"Longitude of the gym"` + Name *string `json:"name" doc:"Name of the gym"` + Url *string `json:"url" doc:"Image URL of the gym"` + LastModifiedTimestamp *int64 `json:"last_modified_timestamp" doc:"Unix timestamp when the gym was last modified in-game"` + RaidEndTimestamp *int64 `json:"raid_end_timestamp" doc:"Unix timestamp when the current raid ends"` + RaidSpawnTimestamp *int64 `json:"raid_spawn_timestamp" doc:"Unix timestamp when the current raid egg spawned"` + RaidBattleTimestamp *int64 `json:"raid_battle_timestamp" doc:"Unix timestamp when the current raid battle begins"` + Updated int64 `json:"updated" doc:"Unix timestamp when the record was last updated"` + RaidPokemonId *int64 `json:"raid_pokemon_id" doc:"Pokedex ID of the raid boss"` + GuardingPokemonId *int64 `json:"guarding_pokemon_id" doc:"Pokedex ID of the pokemon guarding the gym"` + GuardingPokemonDisplay *string `json:"guarding_pokemon_display" doc:"Display details of the guarding pokemon"` + AvailableSlots *int64 `json:"available_slots" doc:"Number of open defender slots"` + TeamId *int64 `json:"team_id" doc:"ID of the team controlling the gym"` + RaidLevel *int64 `json:"raid_level" doc:"Level/tier of the current raid"` + Enabled *int64 `json:"enabled" doc:"Whether the gym is enabled"` + ExRaidEligible *int64 `json:"ex_raid_eligible" doc:"Whether the gym is eligible for EX raids"` + InBattle *int64 `json:"in_battle" doc:"Whether the gym is currently in battle"` + RaidPokemonMove1 *int64 `json:"raid_pokemon_move_1" doc:"Fast move ID of the raid boss"` + RaidPokemonMove2 *int64 `json:"raid_pokemon_move_2" doc:"Charge move ID of the raid boss"` + RaidPokemonForm *int64 `json:"raid_pokemon_form" doc:"Form ID of the raid boss"` + RaidPokemonAlignment *int64 `json:"raid_pokemon_alignment" doc:"Alignment of the raid boss"` + RaidPokemonCp *int64 `json:"raid_pokemon_cp" doc:"Combat power of the raid boss"` + RaidIsExclusive *int64 `json:"raid_is_exclusive" doc:"Whether the current raid is exclusive (EX)"` + CellId *int64 `json:"cell_id" doc:"S2 cell ID the gym belongs to"` + Deleted bool `json:"deleted" doc:"Whether the gym has been deleted"` + TotalCp *int64 `json:"total_cp" doc:"Total combat power of the gym defenders"` + FirstSeenTimestamp int64 `json:"first_seen_timestamp" doc:"Unix timestamp when the gym was first seen"` + RaidPokemonGender *int64 `json:"raid_pokemon_gender" doc:"Gender of the raid boss"` + SponsorId *int64 `json:"sponsor_id" doc:"Sponsor ID of the gym, if sponsored"` + PartnerId *string `json:"partner_id" doc:"Partner ID of the gym, if partnered"` + RaidPokemonCostume *int64 `json:"raid_pokemon_costume" doc:"Costume ID of the raid boss"` + RaidPokemonEvolution *int64 `json:"raid_pokemon_evolution" doc:"Evolution ID of the raid boss (e.g. mega)"` + RaidSeed *int64 `json:"raid_seed" doc:"Raid seed for the current raid"` + RaidPokemonStamina *int64 `json:"raid_pokemon_stamina" doc:"Stamina of the raid boss"` + RaidPokemonCpMultiplier *float64 `json:"raid_pokemon_cp_multiplier" doc:"CP multiplier of the raid boss"` + ArScanEligible *int64 `json:"ar_scan_eligible" doc:"Whether the gym is eligible for AR scanning"` + PowerUpLevel *int64 `json:"power_up_level" doc:"Power-up level of the gym"` + PowerUpPoints *int64 `json:"power_up_points" doc:"Power-up points accumulated for the gym"` + PowerUpEndTimestamp *int64 `json:"power_up_end_timestamp" doc:"Unix timestamp when the power-up ends"` + Description *string `json:"description" doc:"Description of the gym"` + Defenders *string `json:"defenders" doc:"Serialized defender pokemon data"` + Rsvps *string `json:"rsvps" doc:"Serialized raid RSVP data"` } func buildGymResult(gym *Gym) ApiGymResult { return ApiGymResult{ - Id: gym.Id, - Lat: gym.Lat, - Lon: gym.Lon, - Name: gym.Name.Ptr(), - Url: gym.Url.Ptr(), - LastModifiedTimestamp: gym.LastModifiedTimestamp.Ptr(), - RaidEndTimestamp: gym.RaidEndTimestamp.Ptr(), - RaidSpawnTimestamp: gym.RaidSpawnTimestamp.Ptr(), - RaidBattleTimestamp: gym.RaidBattleTimestamp.Ptr(), - Updated: gym.Updated, - RaidPokemonId: gym.RaidPokemonId.Ptr(), - GuardingPokemonId: gym.GuardingPokemonId.Ptr(), - GuardingPokemonDisplay: gym.GuardingPokemonDisplay.Ptr(), - AvailableSlots: gym.AvailableSlots.Ptr(), - TeamId: gym.TeamId.Ptr(), - RaidLevel: gym.RaidLevel.Ptr(), - Enabled: gym.Enabled.Ptr(), - ExRaidEligible: gym.ExRaidEligible.Ptr(), - InBattle: gym.InBattle.Ptr(), - RaidPokemonMove1: gym.RaidPokemonMove1.Ptr(), - RaidPokemonMove2: gym.RaidPokemonMove2.Ptr(), - RaidPokemonForm: gym.RaidPokemonForm.Ptr(), - RaidPokemonAlignment: gym.RaidPokemonAlignment.Ptr(), - RaidPokemonCp: gym.RaidPokemonCp.Ptr(), - RaidIsExclusive: gym.RaidIsExclusive.Ptr(), - CellId: gym.CellId.Ptr(), - Deleted: gym.Deleted, - TotalCp: gym.TotalCp.Ptr(), - FirstSeenTimestamp: gym.FirstSeenTimestamp, - RaidPokemonGender: gym.RaidPokemonGender.Ptr(), - SponsorId: gym.SponsorId.Ptr(), - PartnerId: gym.PartnerId.Ptr(), - RaidPokemonCostume: gym.RaidPokemonCostume.Ptr(), - RaidPokemonEvolution: gym.RaidPokemonEvolution.Ptr(), - ArScanEligible: gym.ArScanEligible.Ptr(), - PowerUpLevel: gym.PowerUpLevel.Ptr(), - PowerUpPoints: gym.PowerUpPoints.Ptr(), - PowerUpEndTimestamp: gym.PowerUpEndTimestamp.Ptr(), - Description: gym.Description.Ptr(), - Defenders: gym.Defenders.Ptr(), - Rsvps: gym.Rsvps.Ptr(), + Id: gym.Id, + Lat: gym.Lat, + Lon: gym.Lon, + Name: gym.Name.Ptr(), + Url: gym.Url.Ptr(), + LastModifiedTimestamp: gym.LastModifiedTimestamp.Ptr(), + RaidEndTimestamp: gym.RaidEndTimestamp.Ptr(), + RaidSpawnTimestamp: gym.RaidSpawnTimestamp.Ptr(), + RaidBattleTimestamp: gym.RaidBattleTimestamp.Ptr(), + Updated: gym.Updated, + RaidPokemonId: gym.RaidPokemonId.Ptr(), + GuardingPokemonId: gym.GuardingPokemonId.Ptr(), + GuardingPokemonDisplay: gym.GuardingPokemonDisplay.Ptr(), + AvailableSlots: gym.AvailableSlots.Ptr(), + TeamId: gym.TeamId.Ptr(), + RaidLevel: gym.RaidLevel.Ptr(), + Enabled: gym.Enabled.Ptr(), + ExRaidEligible: gym.ExRaidEligible.Ptr(), + InBattle: gym.InBattle.Ptr(), + RaidPokemonMove1: gym.RaidPokemonMove1.Ptr(), + RaidPokemonMove2: gym.RaidPokemonMove2.Ptr(), + RaidPokemonForm: gym.RaidPokemonForm.Ptr(), + RaidPokemonAlignment: gym.RaidPokemonAlignment.Ptr(), + RaidPokemonCp: gym.RaidPokemonCp.Ptr(), + RaidIsExclusive: gym.RaidIsExclusive.Ptr(), + CellId: gym.CellId.Ptr(), + Deleted: gym.Deleted, + TotalCp: gym.TotalCp.Ptr(), + FirstSeenTimestamp: gym.FirstSeenTimestamp, + RaidPokemonGender: gym.RaidPokemonGender.Ptr(), + SponsorId: gym.SponsorId.Ptr(), + PartnerId: gym.PartnerId.Ptr(), + RaidPokemonCostume: gym.RaidPokemonCostume.Ptr(), + RaidPokemonEvolution: gym.RaidPokemonEvolution.Ptr(), + RaidSeed: gym.RaidSeed.Ptr(), + RaidPokemonStamina: gym.RaidPokemonStamina.Ptr(), + RaidPokemonCpMultiplier: gym.RaidPokemonCpMultiplier.Ptr(), + ArScanEligible: gym.ArScanEligible.Ptr(), + PowerUpLevel: gym.PowerUpLevel.Ptr(), + PowerUpPoints: gym.PowerUpPoints.Ptr(), + PowerUpEndTimestamp: gym.PowerUpEndTimestamp.Ptr(), + Description: gym.Description.Ptr(), + Defenders: gym.Defenders.Ptr(), + Rsvps: gym.Rsvps.Ptr(), } } diff --git a/decoder/api_gym_test.go b/decoder/api_gym_test.go index 9768c1c4..bf064121 100644 --- a/decoder/api_gym_test.go +++ b/decoder/api_gym_test.go @@ -43,12 +43,15 @@ func goldenSnapshotGym() *Gym { FirstSeenTimestamp: 1699990000, RaidPokemonGender: null.IntFrom(1), // SponsorId intentionally left null - PartnerId: null.StringFrom("partner-1"), - RaidPokemonCostume: null.IntFrom(0), - RaidPokemonEvolution: null.IntFrom(0), - ArScanEligible: null.IntFrom(1), - PowerUpLevel: null.IntFrom(2), - PowerUpPoints: null.IntFrom(50), + PartnerId: null.StringFrom("partner-1"), + RaidPokemonCostume: null.IntFrom(0), + RaidPokemonEvolution: null.IntFrom(0), + RaidSeed: null.IntFrom(123456789012345), + RaidPokemonStamina: null.IntFrom(15000), + RaidPokemonCpMultiplier: null.FloatFrom(0.79), + ArScanEligible: null.IntFrom(1), + PowerUpLevel: null.IntFrom(2), + PowerUpPoints: null.IntFrom(50), // PowerUpEndTimestamp intentionally left null Description: null.StringFrom("A test gym"), // Defenders intentionally left null @@ -67,7 +70,7 @@ func TestBuildGymResult_GoldenSnapshot(t *testing.T) { t.Fatalf("marshal: %v", err) } - const want = `{"id":"gym-abc","lat":12.3456,"lon":-65.4321,"name":"Test Gym","url":"https://example.com/gym.png","last_modified_timestamp":1699990000,"raid_end_timestamp":1700003600,"raid_spawn_timestamp":null,"raid_battle_timestamp":1700000000,"updated":1699999999,"raid_pokemon_id":150,"guarding_pokemon_id":143,"guarding_pokemon_display":null,"available_slots":3,"team_id":2,"raid_level":5,"enabled":1,"ex_raid_eligible":0,"in_battle":0,"raid_pokemon_move_1":216,"raid_pokemon_move_2":94,"raid_pokemon_form":0,"raid_pokemon_alignment":0,"raid_pokemon_cp":3500,"raid_is_exclusive":0,"cell_id":1234567890123,"deleted":false,"total_cp":12000,"first_seen_timestamp":1699990000,"raid_pokemon_gender":1,"sponsor_id":null,"partner_id":"partner-1","raid_pokemon_costume":0,"raid_pokemon_evolution":0,"ar_scan_eligible":1,"power_up_level":2,"power_up_points":50,"power_up_end_timestamp":null,"description":"A test gym","defenders":null,"rsvps":"[]"}` + const want = `{"id":"gym-abc","lat":12.3456,"lon":-65.4321,"name":"Test Gym","url":"https://example.com/gym.png","last_modified_timestamp":1699990000,"raid_end_timestamp":1700003600,"raid_spawn_timestamp":null,"raid_battle_timestamp":1700000000,"updated":1699999999,"raid_pokemon_id":150,"guarding_pokemon_id":143,"guarding_pokemon_display":null,"available_slots":3,"team_id":2,"raid_level":5,"enabled":1,"ex_raid_eligible":0,"in_battle":0,"raid_pokemon_move_1":216,"raid_pokemon_move_2":94,"raid_pokemon_form":0,"raid_pokemon_alignment":0,"raid_pokemon_cp":3500,"raid_is_exclusive":0,"cell_id":1234567890123,"deleted":false,"total_cp":12000,"first_seen_timestamp":1699990000,"raid_pokemon_gender":1,"sponsor_id":null,"partner_id":"partner-1","raid_pokemon_costume":0,"raid_pokemon_evolution":0,"raid_seed":123456789012345,"raid_pokemon_stamina":15000,"raid_pokemon_cp_multiplier":0.79,"ar_scan_eligible":1,"power_up_level":2,"power_up_points":50,"power_up_end_timestamp":null,"description":"A test gym","defenders":null,"rsvps":"[]"}` if string(got) != want { t.Errorf("wire format changed.\n got: %s\nwant: %s", got, want) diff --git a/decoder/gym.go b/decoder/gym.go index b723b68a..9a317138 100644 --- a/decoder/gym.go +++ b/decoder/gym.go @@ -11,47 +11,50 @@ import ( // GymData contains all database-persisted fields for a Gym. // This struct is copyable and used for write-behind queue snapshots. type GymData struct { - Id string `db:"id"` - Lat float64 `db:"lat"` - Lon float64 `db:"lon"` - Name null.String `db:"name"` - Url null.String `db:"url"` - LastModifiedTimestamp null.Int `db:"last_modified_timestamp"` - RaidEndTimestamp null.Int `db:"raid_end_timestamp"` - RaidSpawnTimestamp null.Int `db:"raid_spawn_timestamp"` - RaidBattleTimestamp null.Int `db:"raid_battle_timestamp"` - Updated int64 `db:"updated"` - RaidPokemonId null.Int `db:"raid_pokemon_id"` - GuardingPokemonId null.Int `db:"guarding_pokemon_id"` - GuardingPokemonDisplay null.String `db:"guarding_pokemon_display"` - AvailableSlots null.Int `db:"available_slots"` - TeamId null.Int `db:"team_id"` - RaidLevel null.Int `db:"raid_level"` - Enabled null.Int `db:"enabled"` - ExRaidEligible null.Int `db:"ex_raid_eligible"` - InBattle null.Int `db:"in_battle"` - RaidPokemonMove1 null.Int `db:"raid_pokemon_move_1"` - RaidPokemonMove2 null.Int `db:"raid_pokemon_move_2"` - RaidPokemonForm null.Int `db:"raid_pokemon_form"` - RaidPokemonAlignment null.Int `db:"raid_pokemon_alignment"` - RaidPokemonCp null.Int `db:"raid_pokemon_cp"` - RaidIsExclusive null.Int `db:"raid_is_exclusive"` - CellId null.Int `db:"cell_id"` - Deleted bool `db:"deleted"` - TotalCp null.Int `db:"total_cp"` - FirstSeenTimestamp int64 `db:"first_seen_timestamp"` - RaidPokemonGender null.Int `db:"raid_pokemon_gender"` - SponsorId null.Int `db:"sponsor_id"` - PartnerId null.String `db:"partner_id"` - RaidPokemonCostume null.Int `db:"raid_pokemon_costume"` - RaidPokemonEvolution null.Int `db:"raid_pokemon_evolution"` - ArScanEligible null.Int `db:"ar_scan_eligible"` - PowerUpLevel null.Int `db:"power_up_level"` - PowerUpPoints null.Int `db:"power_up_points"` - PowerUpEndTimestamp null.Int `db:"power_up_end_timestamp"` - Description null.String `db:"description"` - Defenders null.String `db:"defenders"` - Rsvps null.String `db:"rsvps"` + Id string `db:"id"` + Lat float64 `db:"lat"` + Lon float64 `db:"lon"` + Name null.String `db:"name"` + Url null.String `db:"url"` + LastModifiedTimestamp null.Int `db:"last_modified_timestamp"` + RaidEndTimestamp null.Int `db:"raid_end_timestamp"` + RaidSpawnTimestamp null.Int `db:"raid_spawn_timestamp"` + RaidBattleTimestamp null.Int `db:"raid_battle_timestamp"` + Updated int64 `db:"updated"` + RaidPokemonId null.Int `db:"raid_pokemon_id"` + GuardingPokemonId null.Int `db:"guarding_pokemon_id"` + GuardingPokemonDisplay null.String `db:"guarding_pokemon_display"` + AvailableSlots null.Int `db:"available_slots"` + TeamId null.Int `db:"team_id"` + RaidLevel null.Int `db:"raid_level"` + Enabled null.Int `db:"enabled"` + ExRaidEligible null.Int `db:"ex_raid_eligible"` + InBattle null.Int `db:"in_battle"` + RaidPokemonMove1 null.Int `db:"raid_pokemon_move_1"` + RaidPokemonMove2 null.Int `db:"raid_pokemon_move_2"` + RaidPokemonForm null.Int `db:"raid_pokemon_form"` + RaidPokemonAlignment null.Int `db:"raid_pokemon_alignment"` + RaidPokemonCp null.Int `db:"raid_pokemon_cp"` + RaidIsExclusive null.Int `db:"raid_is_exclusive"` + CellId null.Int `db:"cell_id"` + Deleted bool `db:"deleted"` + TotalCp null.Int `db:"total_cp"` + FirstSeenTimestamp int64 `db:"first_seen_timestamp"` + RaidPokemonGender null.Int `db:"raid_pokemon_gender"` + SponsorId null.Int `db:"sponsor_id"` + PartnerId null.String `db:"partner_id"` + RaidPokemonCostume null.Int `db:"raid_pokemon_costume"` + RaidPokemonEvolution null.Int `db:"raid_pokemon_evolution"` + RaidSeed null.Int `db:"raid_seed"` + RaidPokemonStamina null.Int `db:"raid_pokemon_stamina"` + RaidPokemonCpMultiplier null.Float `db:"raid_pokemon_cp_multiplier"` + ArScanEligible null.Int `db:"ar_scan_eligible"` + PowerUpLevel null.Int `db:"power_up_level"` + PowerUpPoints null.Int `db:"power_up_points"` + PowerUpEndTimestamp null.Int `db:"power_up_end_timestamp"` + Description null.String `db:"description"` + Defenders null.String `db:"defenders"` + Rsvps null.String `db:"rsvps"` } // Gym struct. @@ -62,7 +65,6 @@ type Gym struct { GymData // Embedded data fields (all db columns) // Memory-only fields (not persisted to DB) - RaidSeed null.Int `db:"-"` // Raid seed (memory only, sent in webhook as string) RaidLobbyCount null.Int `db:"-"` // Raid lobby player count (memory only) RaidLobbyEndMs null.Int `db:"-"` // Raid lobby join-end timestamp ms (memory only) @@ -540,15 +542,33 @@ func (gym *Gym) SetRsvps(v null.String) { } } -// SetRaidSeed sets the raid seed (memory only, not saved to DB) func (gym *Gym) SetRaidSeed(v null.Int) { if gym.RaidSeed != v { if dbDebugEnabled { gym.changedFields = append(gym.changedFields, fmt.Sprintf("RaidSeed:%s->%s", FormatNull(gym.RaidSeed), FormatNull(v))) } gym.RaidSeed = v - // Do not set dirty, as this doesn't trigger a DB update - gym.internalDirty = true + gym.dirty = true + } +} + +func (gym *Gym) SetRaidPokemonStamina(v null.Int) { + if gym.RaidPokemonStamina != v { + if dbDebugEnabled { + gym.changedFields = append(gym.changedFields, fmt.Sprintf("RaidPokemonStamina:%s->%s", FormatNull(gym.RaidPokemonStamina), FormatNull(v))) + } + gym.RaidPokemonStamina = v + gym.dirty = true + } +} + +func (gym *Gym) SetRaidPokemonCpMultiplier(v null.Float) { + if !nullFloatAlmostEqual(gym.RaidPokemonCpMultiplier, v, floatTolerance) { + if dbDebugEnabled { + gym.changedFields = append(gym.changedFields, fmt.Sprintf("RaidPokemonCpMultiplier:%s->%s", FormatNull(gym.RaidPokemonCpMultiplier), FormatNull(v))) + } + gym.RaidPokemonCpMultiplier = v + gym.dirty = true } } diff --git a/decoder/gym_decode.go b/decoder/gym_decode.go index d12de45a..ddbcd039 100644 --- a/decoder/gym_decode.go +++ b/decoder/gym_decode.go @@ -140,6 +140,8 @@ func (gym *Gym) updateGymFromFort(fortData *pogo.PokemonFortProto, cellId uint64 gym.SetRaidPokemonForm(null.IntFrom(int64(fortData.RaidInfo.RaidPokemon.PokemonDisplay.Form))) gym.SetRaidPokemonAlignment(null.IntFrom(int64(fortData.RaidInfo.RaidPokemon.PokemonDisplay.Alignment))) gym.SetRaidPokemonCp(null.IntFrom(int64(fortData.RaidInfo.RaidPokemon.Cp))) + gym.SetRaidPokemonStamina(null.IntFrom(int64(fortData.RaidInfo.RaidPokemon.MaxStamina))) + gym.SetRaidPokemonCpMultiplier(null.FloatFrom(float64(fortData.RaidInfo.RaidPokemon.CpMultiplier))) gym.SetRaidPokemonGender(null.IntFrom(int64(fortData.RaidInfo.RaidPokemon.PokemonDisplay.Gender))) gym.SetRaidPokemonCostume(null.IntFrom(int64(fortData.RaidInfo.RaidPokemon.PokemonDisplay.Costume))) gym.SetRaidPokemonEvolution(null.IntFrom(int64(fortData.RaidInfo.RaidPokemon.PokemonDisplay.CurrentTempEvolution))) @@ -150,6 +152,8 @@ func (gym *Gym) updateGymFromFort(fortData *pogo.PokemonFortProto, cellId uint64 gym.SetRaidPokemonForm(null.IntFrom(0)) gym.SetRaidPokemonAlignment(null.IntFrom(0)) gym.SetRaidPokemonCp(null.IntFrom(0)) + gym.SetRaidPokemonStamina(null.NewInt(0, false)) + gym.SetRaidPokemonCpMultiplier(null.NewFloat(0, false)) gym.SetRaidPokemonGender(null.IntFrom(0)) gym.SetRaidPokemonCostume(null.IntFrom(0)) gym.SetRaidPokemonEvolution(null.IntFrom(0)) diff --git a/decoder/gym_state.go b/decoder/gym_state.go index 5ae4b2e8..f6108246 100644 --- a/decoder/gym_state.go +++ b/decoder/gym_state.go @@ -25,7 +25,8 @@ const gymSelectColumns = `id, lat, lon, name, url, last_modified_timestamp, raid available_slots, team_id, raid_level, enabled, ex_raid_eligible, in_battle, raid_pokemon_move_1, raid_pokemon_move_2, raid_pokemon_form, raid_pokemon_alignment, raid_pokemon_cp, raid_is_exclusive, cell_id, deleted, total_cp, first_seen_timestamp, raid_pokemon_gender, sponsor_id, partner_id, - raid_pokemon_costume, raid_pokemon_evolution, ar_scan_eligible, power_up_level, power_up_points, + raid_pokemon_costume, raid_pokemon_evolution, raid_seed, raid_pokemon_stamina, + raid_pokemon_cp_multiplier, ar_scan_eligible, power_up_level, power_up_points, power_up_end_timestamp, description, defenders, rsvps` func loadGymFromDatabase(ctx context.Context, db db.DbDetails, fortId string, gym *Gym) error { @@ -193,6 +194,8 @@ type RaidWebhook struct { ArScanEligible int64 `json:"ar_scan_eligible"` Rsvps json.RawMessage `json:"rsvps"` RaidSeed null.String `json:"raid_seed"` + Stamina *int64 `json:"stamina"` + CpMultiplier *float64 `json:"cp_multiplier"` } func createGymFortWebhooks(gym *Gym) { @@ -300,6 +303,8 @@ func createGymWebhooks(gym *Gym, areas []geo.AreaName) { } return null.String{} }(), + Stamina: gym.RaidPokemonStamina.Ptr(), + CpMultiplier: gym.RaidPokemonCpMultiplier.Ptr(), } webhooksSender.AddMessage(webhooks.Raid, raidHook, areas) @@ -369,8 +374,8 @@ func gymWriteDB(db db.DbDetails, gym *Gym, isNewRecord bool) error { ctx := context.Background() if isNewRecord { - res, err := db.GeneralDb.NamedExecContext(ctx, "INSERT INTO gym (id,lat,lon,name,url,last_modified_timestamp,raid_end_timestamp,raid_spawn_timestamp,raid_battle_timestamp,updated,raid_pokemon_id,guarding_pokemon_id,guarding_pokemon_display,available_slots,team_id,raid_level,enabled,ex_raid_eligible,in_battle,raid_pokemon_move_1,raid_pokemon_move_2,raid_pokemon_form,raid_pokemon_alignment,raid_pokemon_cp,raid_is_exclusive,cell_id,deleted,total_cp,first_seen_timestamp,raid_pokemon_gender,sponsor_id,partner_id,raid_pokemon_costume,raid_pokemon_evolution,ar_scan_eligible,power_up_level,power_up_points,power_up_end_timestamp,description, defenders, rsvps) "+ - "VALUES (:id,:lat,:lon,:name,:url,UNIX_TIMESTAMP(),:raid_end_timestamp,:raid_spawn_timestamp,:raid_battle_timestamp,:updated,:raid_pokemon_id,:guarding_pokemon_id,:guarding_pokemon_display,:available_slots,:team_id,:raid_level,:enabled,:ex_raid_eligible,:in_battle,:raid_pokemon_move_1,:raid_pokemon_move_2,:raid_pokemon_form,:raid_pokemon_alignment,:raid_pokemon_cp,:raid_is_exclusive,:cell_id,0,:total_cp,UNIX_TIMESTAMP(),:raid_pokemon_gender,:sponsor_id,:partner_id,:raid_pokemon_costume,:raid_pokemon_evolution,:ar_scan_eligible,:power_up_level,:power_up_points,:power_up_end_timestamp,:description, :defenders, :rsvps)", gym) + res, err := db.GeneralDb.NamedExecContext(ctx, "INSERT INTO gym (id,lat,lon,name,url,last_modified_timestamp,raid_end_timestamp,raid_spawn_timestamp,raid_battle_timestamp,updated,raid_pokemon_id,guarding_pokemon_id,guarding_pokemon_display,available_slots,team_id,raid_level,enabled,ex_raid_eligible,in_battle,raid_pokemon_move_1,raid_pokemon_move_2,raid_pokemon_form,raid_pokemon_alignment,raid_pokemon_cp,raid_is_exclusive,cell_id,deleted,total_cp,first_seen_timestamp,raid_pokemon_gender,sponsor_id,partner_id,raid_pokemon_costume,raid_pokemon_evolution,raid_seed,raid_pokemon_stamina,raid_pokemon_cp_multiplier,ar_scan_eligible,power_up_level,power_up_points,power_up_end_timestamp,description, defenders, rsvps) "+ + "VALUES (:id,:lat,:lon,:name,:url,UNIX_TIMESTAMP(),:raid_end_timestamp,:raid_spawn_timestamp,:raid_battle_timestamp,:updated,:raid_pokemon_id,:guarding_pokemon_id,:guarding_pokemon_display,:available_slots,:team_id,:raid_level,:enabled,:ex_raid_eligible,:in_battle,:raid_pokemon_move_1,:raid_pokemon_move_2,:raid_pokemon_form,:raid_pokemon_alignment,:raid_pokemon_cp,:raid_is_exclusive,:cell_id,0,:total_cp,UNIX_TIMESTAMP(),:raid_pokemon_gender,:sponsor_id,:partner_id,:raid_pokemon_costume,:raid_pokemon_evolution,:raid_seed,:raid_pokemon_stamina,:raid_pokemon_cp_multiplier,:ar_scan_eligible,:power_up_level,:power_up_points,:power_up_end_timestamp,:description, :defenders, :rsvps)", gym) statsCollector.IncDbQuery("insert gym", err) if err != nil { @@ -412,6 +417,9 @@ func gymWriteDB(db db.DbDetails, gym *Gym, isNewRecord bool) error { "partner_id = :partner_id, "+ "raid_pokemon_costume = :raid_pokemon_costume, "+ "raid_pokemon_evolution = :raid_pokemon_evolution, "+ + "raid_seed = :raid_seed, "+ + "raid_pokemon_stamina = :raid_pokemon_stamina, "+ + "raid_pokemon_cp_multiplier = :raid_pokemon_cp_multiplier, "+ "ar_scan_eligible = :ar_scan_eligible, "+ "power_up_level = :power_up_level, "+ "power_up_points = :power_up_points, "+ diff --git a/decoder/writebehind_batch.go b/decoder/writebehind_batch.go index 58503109..84962714 100644 --- a/decoder/writebehind_batch.go +++ b/decoder/writebehind_batch.go @@ -369,7 +369,8 @@ INSERT INTO gym ( raid_pokemon_move_2, raid_pokemon_form, raid_pokemon_alignment, raid_pokemon_cp, raid_is_exclusive, cell_id, deleted, total_cp, first_seen_timestamp, raid_pokemon_gender, sponsor_id, partner_id, raid_pokemon_costume, - raid_pokemon_evolution, ar_scan_eligible, power_up_level, power_up_points, + raid_pokemon_evolution, raid_seed, raid_pokemon_stamina, raid_pokemon_cp_multiplier, + ar_scan_eligible, power_up_level, power_up_points, power_up_end_timestamp, description, rsvps, defenders ) VALUES ( @@ -380,7 +381,8 @@ VALUES ( :raid_pokemon_move_2, :raid_pokemon_form, :raid_pokemon_alignment, :raid_pokemon_cp, :raid_is_exclusive, :cell_id, :deleted, :total_cp, UNIX_TIMESTAMP(), :raid_pokemon_gender, :sponsor_id, :partner_id, :raid_pokemon_costume, - :raid_pokemon_evolution, :ar_scan_eligible, :power_up_level, :power_up_points, + :raid_pokemon_evolution, :raid_seed, :raid_pokemon_stamina, :raid_pokemon_cp_multiplier, + :ar_scan_eligible, :power_up_level, :power_up_points, :power_up_end_timestamp, :description, :rsvps, :defenders ) ON DUPLICATE KEY UPDATE @@ -416,6 +418,9 @@ ON DUPLICATE KEY UPDATE partner_id = VALUES(partner_id), raid_pokemon_costume = VALUES(raid_pokemon_costume), raid_pokemon_evolution = VALUES(raid_pokemon_evolution), + raid_seed = VALUES(raid_seed), + raid_pokemon_stamina = VALUES(raid_pokemon_stamina), + raid_pokemon_cp_multiplier = VALUES(raid_pokemon_cp_multiplier), ar_scan_eligible = VALUES(ar_scan_eligible), power_up_level = VALUES(power_up_level), power_up_points = VALUES(power_up_points), diff --git a/sql/55_gym_raid_fields.up.sql b/sql/55_gym_raid_fields.up.sql new file mode 100644 index 00000000..5c70424c --- /dev/null +++ b/sql/55_gym_raid_fields.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE gym + ADD `raid_seed` BIGINT DEFAULT NULL AFTER `raid_battle_timestamp`, + ADD `raid_pokemon_stamina` INT DEFAULT NULL AFTER `raid_pokemon_cp`, + ADD `raid_pokemon_cp_multiplier` FLOAT DEFAULT NULL AFTER `raid_pokemon_stamina`; From 89df819b073643e0c7145774c6ea078257dc347f Mon Sep 17 00:00:00 2001 From: Malte <42342921+ccev@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:55:10 +0200 Subject: [PATCH 2/2] feat: alignment to raid_stats --- decoder/stats.go | 8 ++++++-- sql/55_gym_raid_fields.up.sql | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/decoder/stats.go b/decoder/stats.go index 5a318eb3..bd568eaf 100644 --- a/decoder/stats.go +++ b/decoder/stats.go @@ -65,6 +65,7 @@ type raidPokemonKey struct { pokemonId int16 formId int tempEvoId int + alignment int } type areaRaidCountDetail struct { @@ -483,6 +484,7 @@ func updateRaidStats(gym *Gym, areas []geo.AreaName) { pokemonId: int16(gym.RaidPokemonId.ValueOrZero()), formId: int(gym.RaidPokemonForm.ValueOrZero()), tempEvoId: int(gym.RaidPokemonEvolution.ValueOrZero()), + alignment: int(gym.RaidPokemonAlignment.ValueOrZero()), } countStats[raidLevel].count[rk]++ } @@ -866,6 +868,7 @@ type raidStatsDbRow struct { PokemonId int `db:"pokemon_id"` FormId int `db:"form_id"` TempEvoId int `db:"temp_evo_id"` + Alignment int `db:"alignment"` Count int `db:"count"` } @@ -893,6 +896,7 @@ func logRaidStats(statsDb *sqlx.DB) { PokemonId: int(rk.pokemonId), FormId: rk.formId, TempEvoId: rk.tempEvoId, + Alignment: rk.alignment, Count: count, }) } @@ -918,8 +922,8 @@ func logRaidStats(statsDb *sqlx.DB) { batchRows := rows[i:end] _, err := statsDb.NamedExec( "INSERT INTO raid_stats "+ - "(date, area, fence, level, pokemon_id, form_id, temp_evo_id, `count`)"+ - " VALUES (:date, :area, :fence, :level, :pokemon_id, :form_id, :temp_evo_id, :count)"+ + "(date, area, fence, level, pokemon_id, form_id, temp_evo_id, alignment, `count`)"+ + " VALUES (:date, :area, :fence, :level, :pokemon_id, :form_id, :temp_evo_id, :alignment, :count)"+ " ON DUPLICATE KEY UPDATE `count` = `count` + VALUES(`count`);", batchRows) if err != nil { log.Errorf("Error inserting raid_stats: %v", err) diff --git a/sql/55_gym_raid_fields.up.sql b/sql/55_gym_raid_fields.up.sql index 5c70424c..afdf0efe 100644 --- a/sql/55_gym_raid_fields.up.sql +++ b/sql/55_gym_raid_fields.up.sql @@ -2,3 +2,12 @@ ALTER TABLE gym ADD `raid_seed` BIGINT DEFAULT NULL AFTER `raid_battle_timestamp`, ADD `raid_pokemon_stamina` INT DEFAULT NULL AFTER `raid_pokemon_cp`, ADD `raid_pokemon_cp_multiplier` FLOAT DEFAULT NULL AFTER `raid_pokemon_stamina`; + +ALTER TABLE raid_stats + ADD COLUMN alignment SMALLINT UNSIGNED NOT NULL DEFAULT 0 AFTER temp_evo_id; + +ALTER TABLE raid_stats + DROP PRIMARY KEY; + +ALTER TABLE raid_stats + ADD PRIMARY KEY (date, area, fence, pokemon_id, form_id, temp_evo_id, alignment, level);