Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ bearer_token = "secret"
[cleanup]
pokemon = true # Keep pokemon table is kept nice and short
incidents = true # Remove incidents after expiry
station_battles = true # Remove station battles after expiry
quests = true # Remove quests after expiry
tappables = true # Remove tappables after expiry
stats = true # Enable/Disable stats history
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type cleanup struct {
Pokemon bool `koanf:"pokemon"`
Quests bool `koanf:"quests"`
Incidents bool `koanf:"incidents"`
StationBattles bool `koanf:"station_battles"`
Tappables bool `koanf:"tappables"`
Stats bool `koanf:"stats"`
StatsDays int `koanf:"stats_days"`
Expand Down
13 changes: 7 additions & 6 deletions config/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ func ReadConfig() (configDefinition, error) {
Compress: true,
},
Cleanup: cleanup{
Pokemon: true,
Quests: true,
Incidents: true,
Tappables: true,
StatsDays: 7,
DeviceHours: 24,
Pokemon: true,
Quests: true,
Incidents: true,
StationBattles: true,
Tappables: true,
StatsDays: 7,
DeviceHours: 24,
},
Database: database{
MaxPool: 100,
Expand Down
32 changes: 26 additions & 6 deletions decoder/api_fort.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,34 @@ func isFortDnfMatch(fortType FortType, fortLookup *FortLookup, filter *ApiFortDn
}
} else if fortLookup.FortType == STATION {
if filter.BattleLevel != nil || filter.BattlePokemon != nil {
// Check if battle has expired
if fortLookup.BattleEndTimestamp <= now {
return false
if len(fortLookup.StationBattles) == 0 {
if fortLookup.BattleEndTimestamp <= now {
return false
}
if filter.BattleLevel != nil && !slices.Contains(filter.BattleLevel, fortLookup.BattleLevel) {
return false
}
if filter.BattlePokemon != nil && !matchDnfIdPair(filter.BattlePokemon, fortLookup.BattlePokemonId, fortLookup.BattlePokemonForm) {
return false
}
return true
}
if filter.BattleLevel != nil && !slices.Contains(filter.BattleLevel, fortLookup.BattleLevel) {
return false

matchedBattle := false
for _, battle := range fortLookup.StationBattles {
if battle.BattleEndTimestamp <= now {
continue
}
if filter.BattleLevel != nil && !slices.Contains(filter.BattleLevel, battle.BattleLevel) {
continue
}
if filter.BattlePokemon != nil && !matchDnfIdPair(filter.BattlePokemon, battle.BattlePokemonId, battle.BattlePokemonForm) {
continue
}
matchedBattle = true
break
}
if filter.BattlePokemon != nil && !matchDnfIdPair(filter.BattlePokemon, fortLookup.BattlePokemonId, fortLookup.BattlePokemonForm) {
if !matchedBattle {
return false
}
}
Expand Down
117 changes: 71 additions & 46 deletions decoder/api_station.go
Original file line number Diff line number Diff line change
@@ -1,55 +1,80 @@
package decoder

import "github.com/guregu/null/v6"
import (
"time"

"github.com/guregu/null/v6"
)

type ApiStationResult struct {
Id string `json:"id"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Name string `json:"name"`
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
IsBattleAvailable bool `json:"is_battle_available"`
Updated int64 `json:"updated"`
BattleLevel null.Int `json:"battle_level"`
BattleStart null.Int `json:"battle_start"`
BattleEnd null.Int `json:"battle_end"`
BattlePokemonId null.Int `json:"battle_pokemon_id"`
BattlePokemonForm null.Int `json:"battle_pokemon_form"`
BattlePokemonCostume null.Int `json:"battle_pokemon_costume"`
BattlePokemonGender null.Int `json:"battle_pokemon_gender"`
BattlePokemonAlignment null.Int `json:"battle_pokemon_alignment"`
BattlePokemonBreadMode null.Int `json:"battle_pokemon_bread_mode"`
BattlePokemonMove1 null.Int `json:"battle_pokemon_move_1"`
BattlePokemonMove2 null.Int `json:"battle_pokemon_move_2"`
TotalStationedPokemon null.Int `json:"total_stationed_pokemon"`
TotalStationedGmax null.Int `json:"total_stationed_gmax"`
StationedPokemon null.String `json:"stationed_pokemon"`
Id string `json:"id"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Name string `json:"name"`
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
IsBattleAvailable bool `json:"is_battle_available"`
Updated int64 `json:"updated"`
BattleLevel null.Int `json:"battle_level"`
BattleStart null.Int `json:"battle_start"`
BattleEnd null.Int `json:"battle_end"`
BattlePokemonId null.Int `json:"battle_pokemon_id"`
BattlePokemonForm null.Int `json:"battle_pokemon_form"`
BattlePokemonCostume null.Int `json:"battle_pokemon_costume"`
BattlePokemonGender null.Int `json:"battle_pokemon_gender"`
BattlePokemonAlignment null.Int `json:"battle_pokemon_alignment"`
BattlePokemonBreadMode null.Int `json:"battle_pokemon_bread_mode"`
BattlePokemonMove1 null.Int `json:"battle_pokemon_move_1"`
BattlePokemonMove2 null.Int `json:"battle_pokemon_move_2"`
TotalStationedPokemon null.Int `json:"total_stationed_pokemon"`
TotalStationedGmax null.Int `json:"total_stationed_gmax"`
StationedPokemon null.String `json:"stationed_pokemon"`
Battles []ApiStationBattle `json:"battles,omitempty"`
}

func BuildStationResult(station *Station) ApiStationResult {
return ApiStationResult{
Id: station.Id,
Lat: station.Lat,
Lon: station.Lon,
Name: station.Name,
StartTime: station.StartTime,
EndTime: station.EndTime,
IsBattleAvailable: station.IsBattleAvailable,
Updated: station.Updated,
BattleLevel: station.BattleLevel,
BattleStart: station.BattleStart,
BattleEnd: station.BattleEnd,
BattlePokemonId: station.BattlePokemonId,
BattlePokemonForm: station.BattlePokemonForm,
BattlePokemonCostume: station.BattlePokemonCostume,
BattlePokemonGender: station.BattlePokemonGender,
BattlePokemonAlignment: station.BattlePokemonAlignment,
BattlePokemonBreadMode: station.BattlePokemonBreadMode,
BattlePokemonMove1: station.BattlePokemonMove1,
BattlePokemonMove2: station.BattlePokemonMove2,
TotalStationedPokemon: station.TotalStationedPokemon,
TotalStationedGmax: station.TotalStationedGmax,
StationedPokemon: station.StationedPokemon,
now := time.Now().Unix()
snapshot := collectStationBattleSnapshot(station, now)
hasBattleState := hasHydratedStationBattles(station.Id)

result := ApiStationResult{
Id: station.Id,
Lat: station.Lat,
Lon: station.Lon,
Name: station.Name,
StartTime: station.StartTime,
EndTime: station.EndTime,
IsBattleAvailable: station.IsBattleAvailable,
Updated: station.Updated,
TotalStationedPokemon: station.TotalStationedPokemon,
TotalStationedGmax: station.TotalStationedGmax,
StationedPokemon: station.StationedPokemon,
Battles: buildApiStationBattlesFromSlice(snapshot.Battles),
}
if snapshot.Canonical != nil {
result.BattleLevel = null.IntFrom(int64(snapshot.Canonical.BattleLevel))
result.BattleStart = null.IntFrom(snapshot.Canonical.BattleStart)
result.BattleEnd = null.IntFrom(snapshot.Canonical.BattleEnd)
result.BattlePokemonId = snapshot.Canonical.BattlePokemonId
result.BattlePokemonForm = snapshot.Canonical.BattlePokemonForm
result.BattlePokemonCostume = snapshot.Canonical.BattlePokemonCostume
result.BattlePokemonGender = snapshot.Canonical.BattlePokemonGender
result.BattlePokemonAlignment = snapshot.Canonical.BattlePokemonAlignment
result.BattlePokemonBreadMode = snapshot.Canonical.BattlePokemonBreadMode
result.BattlePokemonMove1 = snapshot.Canonical.BattlePokemonMove1
result.BattlePokemonMove2 = snapshot.Canonical.BattlePokemonMove2
} else if !hasBattleState {
result.BattleLevel = station.BattleLevel
result.BattleStart = station.BattleStart
result.BattleEnd = station.BattleEnd
result.BattlePokemonId = station.BattlePokemonId
result.BattlePokemonForm = station.BattlePokemonForm
result.BattlePokemonCostume = station.BattlePokemonCostume
result.BattlePokemonGender = station.BattlePokemonGender
result.BattlePokemonAlignment = station.BattlePokemonAlignment
result.BattlePokemonBreadMode = station.BattlePokemonBreadMode
result.BattlePokemonMove1 = station.BattlePokemonMove1
result.BattlePokemonMove2 = station.BattlePokemonMove2
}
return result
}
27 changes: 23 additions & 4 deletions decoder/fortRtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package decoder
import (
"encoding/json"
"sync"
"time"

"github.com/guregu/null/v6"
"github.com/puzpuzpuz/xsync/v3"
Expand Down Expand Up @@ -57,6 +58,7 @@ type FortLookup struct {
BattleLevel int8
BattlePokemonId int16
BattlePokemonForm int16
StationBattles []FortLookupStationBattle
}

var fortLookupCache *xsync.MapOf[string, FortLookup]
Expand Down Expand Up @@ -202,14 +204,31 @@ func updateGymLookup(gym *Gym) {
}

func updateStationLookup(station *Station) {
updateStationLookupFromSnapshot(station, collectStationBattleSnapshot(station, time.Now().Unix()))
}

func updateStationLookupFromSnapshot(station *Station, snapshot stationBattleSnapshot) {
battles := buildFortLookupStationBattlesFromSlice(snapshot.Battles)
canonical := snapshot.Canonical
battleEndTimestamp := int64(0)
battleLevel := int8(0)
battlePokemonId := int16(0)
battlePokemonForm := int16(0)
if canonical != nil {
battleEndTimestamp = canonical.BattleEnd
battleLevel = int8(canonical.BattleLevel)
battlePokemonId = int16(canonical.BattlePokemonId.ValueOrZero())
battlePokemonForm = int16(canonical.BattlePokemonForm.ValueOrZero())
}
fortLookupCache.Store(station.Id, FortLookup{
FortType: STATION,
Lat: station.Lat,
Lon: station.Lon,
BattleEndTimestamp: station.BattleEnd.ValueOrZero(),
BattleLevel: int8(station.BattleLevel.ValueOrZero()),
BattlePokemonId: int16(station.BattlePokemonId.ValueOrZero()),
BattlePokemonForm: int16(station.BattlePokemonForm.ValueOrZero()),
BattleEndTimestamp: battleEndTimestamp,
BattleLevel: battleLevel,
BattlePokemonId: battlePokemonId,
BattlePokemonForm: battlePokemonForm,
StationBattles: battles,
})
}

Expand Down
1 change: 1 addition & 0 deletions decoder/gmo_decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func UpdateStationBatch(ctx context.Context, db db.DbDetails, scanParameters Sca
continue
}
station.updateFromStationProto(stationProto.Data, stationProto.Cell)
syncStationBattlesFromProto(station, stationProto.Data.BattleDetails)
saveStationRecord(ctx, db, station)
unlock()
}
Expand Down
10 changes: 6 additions & 4 deletions decoder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,13 @@ func initDataCache() {
TTL: fortCacheTTL,
KeyToShard: StringKeyToShard,
})
if config.Config.FortInMemory {
stationCache.OnEviction(func(ctx context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, *Station]) {
stationCache.OnEviction(func(ctx context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, *Station]) {
clearStationBattleCaches(item.Key())
if config.Config.FortInMemory {
s := item.Value()
evictFortFromTree(s.Id, s.Lat, s.Lon)
})
}
}
})

tappableCache = ttlcache.New[uint64, *Tappable](
ttlcache.WithTTL[uint64, *Tappable](60 * time.Minute),
Expand Down Expand Up @@ -170,6 +171,7 @@ func initDataCache() {
})
initPokemonRtree()
initFortRtree()
initStationBattleCache()

incidentCache = ttlcache.New[string, *Incident](
ttlcache.WithTTL[string, *Incident](60 * time.Minute),
Expand Down
21 changes: 15 additions & 6 deletions decoder/preload.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import (
"golbat/db"
)

// Preload loads forts, stations, active incidents, and recent spawnpoints from DB into cache.
// Preload loads forts, stations, active station battles, active incidents, and recent spawnpoints from DB into cache.
// If populateRtree is true, also builds the rtree index for forts.
func Preload(dbDetails db.DbDetails, populateRtree bool) {
startTime := time.Now()

var wg sync.WaitGroup
var pokestopCount, gymCount, stationCount, incidentCount, spawnpointCount int32
var pokestopCount, gymCount, stationCount, stationBattleCount, incidentCount, spawnpointCount int32

// Phase 1: Load forts and spawnpoints in parallel.
// Forts must be loaded before incidents so that the fort lookup cache
Expand All @@ -41,11 +41,20 @@ func Preload(dbDetails db.DbDetails, populateRtree bool) {
}()
wg.Wait()

// Phase 2: Load incidents (needs pokestop lookup entries to exist)
incidentCount = preloadIncidents(dbDetails, populateRtree)
// Phase 2: Load child state that depends on forts/stations already being present.
wg.Add(2)
go func() {
defer wg.Done()
stationBattleCount = preloadStationBattles(dbDetails, populateRtree)
}()
go func() {
defer wg.Done()
incidentCount = preloadIncidents(dbDetails, populateRtree)
}()
wg.Wait()

log.Infof("Preload: loaded %d pokestops, %d gyms, %d stations, %d incidents, %d spawnpoints in %v (rtree=%v)",
pokestopCount, gymCount, stationCount, incidentCount, spawnpointCount, time.Since(startTime), populateRtree)
log.Infof("Preload: loaded %d pokestops, %d gyms, %d stations, %d station battles, %d incidents, %d spawnpoints in %v (rtree=%v)",
pokestopCount, gymCount, stationCount, stationBattleCount, incidentCount, spawnpointCount, time.Since(startTime), populateRtree)
}

// PreloadForts loads all forts from DB into cache.
Expand Down
13 changes: 13 additions & 0 deletions decoder/station.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package decoder

import (
"fmt"
"time"

"github.com/guregu/null/v6"
)
Expand Down Expand Up @@ -57,12 +58,17 @@ type Station struct {
// StationOldValues holds old field values for webhook comparison
type StationOldValues struct {
EndTime int64
IsBattleAvailable bool
HasCanonicalBattle bool
CanonicalBattleSeed int64
BattleProjection *StationBattleData
BattleEnd null.Int
BattlePokemonId null.Int
BattlePokemonForm null.Int
BattlePokemonCostume null.Int
BattlePokemonGender null.Int
BattlePokemonBreadMode null.Int
BattleListSignature string
}

// IsDirty returns true if any field has been modified
Expand Down Expand Up @@ -93,14 +99,21 @@ func (station *Station) Unlock() {
// snapshotOldValues saves current values for webhook comparison
// Call this after loading from cache/DB but before modifications
func (station *Station) snapshotOldValues() {
now := time.Now().Unix()
snapshot := collectStationBattleSnapshot(station, now)
station.oldValues = StationOldValues{
IsBattleAvailable: station.IsBattleAvailable,
HasCanonicalBattle: snapshot.Canonical != nil,
CanonicalBattleSeed: canonicalBattleSeed(snapshot.Canonical),
BattleProjection: stationBattleFromStationProjection(station),
EndTime: station.EndTime,
BattleEnd: station.BattleEnd,
BattlePokemonId: station.BattlePokemonId,
BattlePokemonForm: station.BattlePokemonForm,
BattlePokemonCostume: station.BattlePokemonCostume,
BattlePokemonGender: station.BattlePokemonGender,
BattlePokemonBreadMode: station.BattlePokemonBreadMode,
BattleListSignature: snapshot.Signature,
}
}

Expand Down
Loading