Skip to content

Commit

Permalink
Merge pull request #233 from UnownHash/dynamax
Browse files Browse the repository at this point in the history
Dynamax support
  • Loading branch information
Fabio1988 authored Sep 16, 2024
2 parents d4ba415 + a9e7ca6 commit f5b2010
Show file tree
Hide file tree
Showing 8 changed files with 219,851 additions and 178,905 deletions.
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type scanRule struct {
ProcessCells *bool `koanf:"cells"`
ProcessPokestops *bool `koanf:"pokestops"`
ProcessGyms *bool `koanf:"gyms"`
ProcessStations *bool `koanf:"stations"`
}

var Config configDefinition
32 changes: 32 additions & 0 deletions decoder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ type RawFortData struct {
Data *pogo.PokemonFortProto
}

type RawStationData struct {
Cell uint64
Data *pogo.StationProto
}

type RawWildPokemonData struct {
Cell uint64
Data *pogo.WildPokemonProto
Expand Down Expand Up @@ -56,6 +61,7 @@ var webhooksSender webhooksSenderInterface
var statsCollector stats_collector.StatsCollector
var pokestopCache *ttlcache.Cache[string, Pokestop]
var gymCache *ttlcache.Cache[string, Gym]
var stationCache *ttlcache.Cache[string, Station]
var weatherCache *ttlcache.Cache[int64, Weather]
var s2CellCache *ttlcache.Cache[uint64, S2Cell]
var spawnpointCache *ttlcache.Cache[int64, Spawnpoint]
Expand All @@ -68,6 +74,7 @@ var getMapFortsCache *ttlcache.Cache[string, *pogo.GetMapFortsOutProto_FortProto

var gymStripedMutex = stripedmutex.New(128)
var pokestopStripedMutex = stripedmutex.New(128)
var stationStripedMutex = stripedmutex.New(128)
var incidentStripedMutex = stripedmutex.New(128)
var pokemonStripedMutex = stripedmutex.New(1024)
var weatherStripedMutex = stripedmutex.New(128)
Expand Down Expand Up @@ -100,6 +107,11 @@ func initDataCache() {
)
go gymCache.Start()

stationCache = ttlcache.New[string, Station](
ttlcache.WithTTL[string, Station](60 * time.Minute),
)
go stationCache.Start()

weatherCache = ttlcache.New[int64, Weather](
ttlcache.WithTTL[int64, Weather](60 * time.Minute),
)
Expand Down Expand Up @@ -298,6 +310,26 @@ func UpdateFortBatch(ctx context.Context, db db.DbDetails, scanParameters ScanPa
}
}

func UpdateStationBatch(ctx context.Context, db db.DbDetails, scanParameters ScanParameters, p []RawStationData) {
for _, stationProto := range p {
stationId := stationProto.Data.Id
stationMutex, _ := stationStripedMutex.GetLock(stationId)
stationMutex.Lock()
station, err := getStationRecord(ctx, db, stationId)
if err != nil {
log.Errorf("getStationRecord: %s", err)
stationMutex.Unlock()
continue
}
if station == nil {
station = &Station{}
}
station.updateFromStationProto(stationProto.Data, stationProto.Cell)
saveStationRecord(ctx, db, station)
stationMutex.Unlock()
}
}

func UpdatePokemonBatch(ctx context.Context, db db.DbDetails, scanParameters ScanParameters, wildPokemonList []RawWildPokemonData, nearbyPokemonList []RawNearbyPokemonData, mapPokemonList []RawMapPokemonData, username string) {
for _, wild := range wildPokemonList {
encounterId := strconv.FormatUint(wild.Data.EncounterId, 10)
Expand Down
3 changes: 3 additions & 0 deletions decoder/scanarea.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ScanParameters struct {
ProcessWeather bool
ProcessPokestops bool
ProcessGyms bool
ProcessStations bool
ProcessCells bool
}

Expand Down Expand Up @@ -59,6 +60,7 @@ func FindScanConfiguration(scanContext string, lat, lon float64) ScanParameters
ProcessWeather: defaultTrue(rule.ProcessWeather),
ProcessPokestops: defaultTrue(rule.ProcessPokestops),
ProcessGyms: defaultTrue(rule.ProcessGyms),
ProcessStations: defaultTrue(rule.ProcessStations),
}
}

Expand All @@ -70,5 +72,6 @@ func FindScanConfiguration(scanContext string, lat, lon float64) ScanParameters
ProcessWeather: true,
ProcessGyms: true,
ProcessPokestops: true,
ProcessStations: true,
}
}
274 changes: 274 additions & 0 deletions decoder/station.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
package decoder

import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"golbat/db"
"golbat/pogo"
"time"

"github.com/jellydator/ttlcache/v3"
log "github.com/sirupsen/logrus"
"gopkg.in/guregu/null.v4"
)

type Station struct {
Id string `db:"id"`
Lat float64 `db:"lat"`
Lon float64 `db:"lon"`
Name string `db:"name"`
CellId int64 `db:"cell_id"`
StartTime int64 `db:"start_time"`
EndTime int64 `db:"end_time"`
CooldownComplete int64 `db:"cooldown_complete"`
IsBattleAvailable bool `db:"is_battle_available"`
IsInactive bool `db:"is_inactive"`
Updated int64 `db:"updated"`

BattleLevel null.Int `db:"battle_level"`
BattleStart null.Int `db:"battle_start"`
BattleEnd null.Int `db:"battle_end"`
BattlePokemonId null.Int `db:"battle_pokemon_id"`
BattlePokemonForm null.Int `db:"battle_pokemon_form"`
BattlePokemonCostume null.Int `db:"battle_pokemon_costume"`
BattlePokemonGender null.Int `db:"battle_pokemon_gender"`
BattlePokemonAlignment null.Int `db:"battle_pokemon_alignment"`
BattlePokemonBreadMode null.Int `db:"battle_pokemon_bread_mode"`
BattlePokemonMove1 null.Int `db:"battle_pokemon_move_1"`
BattlePokemonMove2 null.Int `db:"battle_pokemon_move_2"`

TotalStationedPokemon null.Int `db:"total_stationed_pokemon"`
StationedPokemon null.String `db:"stationed_pokemon"`
}

func getStationRecord(ctx context.Context, db db.DbDetails, stationId string) (*Station, error) {
inMemoryStation := stationCache.Get(stationId)
if inMemoryStation != nil {
station := inMemoryStation.Value()
return &station, nil
}
station := Station{}
err := db.GeneralDb.GetContext(ctx, &station,
`
SELECT id, lat, lon, name, cell_id, start_time, end_time, cooldown_complete, is_battle_available, is_inactive, updated, battle_level, battle_start, battle_end, battle_pokemon_id, battle_pokemon_form, battle_pokemon_costume, battle_pokemon_gender, battle_pokemon_alignment, battle_pokemon_bread_mode, battle_pokemon_move_1, battle_pokemon_move_2, total_stationed_pokemon, stationed_pokemon
FROM station WHERE id = ?
`, stationId)
statsCollector.IncDbQuery("select station", err)

if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}

if err != nil {
return nil, err
}
return &station, nil
}

func saveStationRecord(ctx context.Context, db db.DbDetails, station *Station) {
oldStation, _ := getStationRecord(ctx, db, station.Id)
now := time.Now().Unix()
if oldStation != nil && !hasChangesStation(oldStation, station) {
if oldStation.Updated > now-900 {
// if a gym is unchanged, but we did see it again after 15 minutes, then save again
return
}
}

station.Updated = now

//log.Traceln(cmp.Diff(oldStation, station))
if oldStation == nil {
res, err := db.GeneralDb.NamedExecContext(ctx,
`
INSERT INTO station (id, lat, lon, name, cell_id, start_time, end_time, cooldown_complete, is_battle_available, is_inactive, updated, battle_level, battle_start, battle_end, battle_pokemon_id, battle_pokemon_form, battle_pokemon_costume, battle_pokemon_gender, battle_pokemon_alignment, battle_pokemon_bread_mode, battle_pokemon_move_1, battle_pokemon_move_2, total_stationed_pokemon, stationed_pokemon)
VALUES (:id,:lat,:lon,:name,:cell_id,:start_time,:end_time,:cooldown_complete,:is_battle_available,:is_inactive,:updated,:battle_level,:battle_start,:battle_end,:battle_pokemon_id,:battle_pokemon_form,:battle_pokemon_costume,:battle_pokemon_gender,:battle_pokemon_alignment,:battle_pokemon_bread_mode,:battle_pokemon_move_1,:battle_pokemon_move_2,:total_stationed_pokemon,:stationed_pokemon)
`, station)

statsCollector.IncDbQuery("insert station", err)
if err != nil {
log.Errorf("insert station: %s", err)
return
}
_, _ = res, err
} else {
res, err := db.GeneralDb.NamedExecContext(ctx, `
UPDATE station
SET
lat = :lat,
lon = :lon,
name = :name,
cell_id = :cell_id,
start_time = :start_time,
end_time = :end_time,
cooldown_complete = :cooldown_complete,
is_battle_available = :is_battle_available,
is_inactive = :is_inactive,
updated = :updated,
battle_level = :battle_level,
battle_start = :battle_start,
battle_end = :battle_end,
battle_pokemon_id = :battle_pokemon_id,
battle_pokemon_form = :battle_pokemon_form,
battle_pokemon_costume = :battle_pokemon_costume,
battle_pokemon_gender = :battle_pokemon_gender,
battle_pokemon_alignment = :battle_pokemon_alignment,
battle_pokemon_bread_mode = :battle_pokemon_bread_mode,
battle_pokemon_move_1 = :battle_pokemon_move_1,
battle_pokemon_move_2 = :battle_pokemon_move_2,
total_stationed_pokemon = :total_stationed_pokemon,
stationed_pokemon = :stationed_pokemon
WHERE id = :id
`, station,
)
statsCollector.IncDbQuery("update station", err)
if err != nil {
log.Errorf("Update station %s", err)
}
_, _ = res, err
}

stationCache.Set(station.Id, *station, ttlcache.DefaultTTL)
createStationWebhooks(oldStation, station)

}

// hasChangesStation compares two Station structs
// Float tolerance: Lat, Lon
func hasChangesStation(old *Station, new *Station) bool {
return old.Id != new.Id ||
old.Name != new.Name ||
old.StartTime != new.StartTime ||
old.EndTime != new.EndTime ||
old.StationedPokemon != new.StationedPokemon ||
old.CooldownComplete != new.CooldownComplete ||
old.IsBattleAvailable != new.IsBattleAvailable ||
old.BattleLevel != new.BattleLevel ||
old.BattleStart != new.BattleStart ||
old.BattleEnd != new.BattleEnd ||
old.BattlePokemonId != new.BattlePokemonId ||
old.BattlePokemonForm != new.BattlePokemonForm ||
old.BattlePokemonCostume != new.BattlePokemonCostume ||
old.BattlePokemonGender != new.BattlePokemonGender ||
old.BattlePokemonAlignment != new.BattlePokemonAlignment ||
old.BattlePokemonBreadMode != new.BattlePokemonBreadMode ||
old.BattlePokemonMove1 != new.BattlePokemonMove1 ||
old.BattlePokemonMove2 != new.BattlePokemonMove2 ||
!floatAlmostEqual(old.Lat, new.Lat, floatTolerance) ||
!floatAlmostEqual(old.Lon, new.Lon, floatTolerance)
}

func (station *Station) updateFromStationProto(stationProto *pogo.StationProto, cellId uint64) *Station {
station.Id = stationProto.Id
station.Name = stationProto.Name
station.Lat = stationProto.Lat
station.Lon = stationProto.Lng
station.StartTime = stationProto.StartTimeMs / 1000
station.EndTime = stationProto.EndTimeMs / 1000
station.CooldownComplete = stationProto.CooldownCompleteMs
station.IsBattleAvailable = stationProto.IsBreadBattleAvailable
if battleDetails := stationProto.BattleDetails; battleDetails != nil {
station.BattleLevel = null.IntFrom(int64(battleDetails.BattleLevel))
station.BattleStart = null.IntFrom(battleDetails.BattleWindowStartMs / 1000)
station.BattleEnd = null.IntFrom(battleDetails.BattleWindowEndMs / 1000)
if pokemon := battleDetails.BattlePokemon; pokemon != nil {
station.BattlePokemonId = null.IntFrom(int64(pokemon.PokemonId))
station.BattlePokemonMove1 = null.IntFrom(int64(pokemon.Move1))
station.BattlePokemonMove2 = null.IntFrom(int64(pokemon.Move2))
station.BattlePokemonForm = null.IntFrom(int64(pokemon.PokemonDisplay.Form))
station.BattlePokemonCostume = null.IntFrom(int64(pokemon.PokemonDisplay.Costume))
station.BattlePokemonGender = null.IntFrom(int64(pokemon.PokemonDisplay.Gender))
station.BattlePokemonAlignment = null.IntFrom(int64(pokemon.PokemonDisplay.Alignment))
station.BattlePokemonBreadMode = null.IntFrom(int64(pokemon.PokemonDisplay.BreadModeEnum))
if rewardPokemon := battleDetails.RewardPokemon; rewardPokemon != nil && pokemon.PokemonId != rewardPokemon.PokemonId {
log.Infof("[DYNAMAX] Pokemon reward differs from battle: Battle %v - Reward %v", pokemon, rewardPokemon)
}
}
}
station.CellId = int64(cellId)
return station
}

func (station *Station) updateFromGetStationedPokemonDetailsOutProto(stationProto *pogo.GetStationedPokemonDetailsOutProto) *Station {
type stationedPokemonDetail struct {
PokemonId int `json:"pokemon_id"`
Form int `json:"form"`
Costume int `json:"costume"`
Gender int `json:"gender"`
BreadMode int `json:"bread_mode"`
}

var stationedPokemon []stationedPokemonDetail
for _, stationedPokemonDetails := range stationProto.StationedPokemons {
pokemon := stationedPokemonDetails.Pokemon
stationedPokemon = append(stationedPokemon, stationedPokemonDetail{
PokemonId: int(pokemon.PokemonId),
Form: int(pokemon.PokemonDisplay.Form),
Costume: int(pokemon.PokemonDisplay.Costume),
Gender: int(pokemon.PokemonDisplay.Gender),
BreadMode: int(pokemon.PokemonDisplay.BreadModeEnum),
})
}
jsonString, _ := json.Marshal(stationedPokemon)
station.StationedPokemon = null.StringFrom(string(jsonString))
station.TotalStationedPokemon = null.IntFrom(int64(stationProto.TotalNumStationedPokemon))
return station
}

func (station *Station) resetStationedPokemonFromStationDetailsNotFound() *Station {
jsonString, _ := json.Marshal([]string{})
station.StationedPokemon = null.StringFrom(string(jsonString))
station.TotalStationedPokemon = null.IntFrom(0)
return station
}

func ResetStationedPokemonWithStationDetailsNotFound(ctx context.Context, db db.DbDetails, request *pogo.GetStationedPokemonDetailsProto) string {
stationId := request.StationId
stationMutex, _ := stationStripedMutex.GetLock(stationId)
stationMutex.Lock()
defer stationMutex.Unlock()

station, err := getStationRecord(ctx, db, stationId)
if err != nil {
log.Printf("Get station %s", err)
return "Error getting station"
}

if station == nil {
log.Infof("Stationed pokemon details for station %s not found", stationId)
return fmt.Sprintf("Stationed pokemon details for station %s not found", stationId)
}

station.resetStationedPokemonFromStationDetailsNotFound()
saveStationRecord(ctx, db, station)
return fmt.Sprintf("StationedPokemonDetails %s", stationId)
}

func UpdateStationWithStationDetails(ctx context.Context, db db.DbDetails, request *pogo.GetStationedPokemonDetailsProto, stationDetails *pogo.GetStationedPokemonDetailsOutProto) string {
stationId := request.StationId
stationMutex, _ := stationStripedMutex.GetLock(stationId)
stationMutex.Lock()
defer stationMutex.Unlock()

station, err := getStationRecord(ctx, db, stationId)
if err != nil {
log.Printf("Get station %s", err)
return "Error getting station"
}

if station == nil {
log.Infof("Stationed pokemon details for station %s not found", stationId)
return fmt.Sprintf("Stationed pokemon details for station %s not found", stationId)
}

station.updateFromGetStationedPokemonDetailsOutProto(stationDetails)
saveStationRecord(ctx, db, station)
return fmt.Sprintf("StationedPokemonDetails %s", stationId)
}

func createStationWebhooks(oldStation *Station, station *Station) {
//TODO we need to define webhooks, are they needed for stations, or only for battles?
}
Loading

0 comments on commit f5b2010

Please sign in to comment.