Skip to content
Closed
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
10 changes: 4 additions & 6 deletions decoder/api_pokemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,8 @@ func SearchPokemon(request ApiPokemonSearch) ([]*Pokemon, error) {
found := slices.Contains(request.SearchIds, pokemonLookupItem.PokemonLookup.PokemonId)

if found {
if pokemonCacheEntry := getPokemonFromCache(pokemonId); pokemonCacheEntry != nil {
pokemon := pokemonCacheEntry.Value()
results = append(results, &pokemon)
if pokemon := peekPokemonFromCache(pokemonId); pokemon != nil {
results = append(results, pokemon)
pokemonMatched++

if pokemonMatched > maxPokemon {
Expand All @@ -151,9 +150,8 @@ func SearchPokemon(request ApiPokemonSearch) ([]*Pokemon, error) {
// Get one result

func GetOnePokemon(pokemonId uint64) *ApiPokemonResult {
if item := getPokemonFromCache(pokemonId); item != nil {
pokemon := item.Value()
apiPokemon := buildApiPokemonResult(&pokemon)
if pokemon := peekPokemonFromCache(pokemonId); pokemon != nil {
apiPokemon := buildApiPokemonResult(pokemon)
return &apiPokemon
}
return nil
Expand Down
6 changes: 2 additions & 4 deletions decoder/api_pokemon_scan_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,8 @@ func GetPokemonInArea(retrieveParameters ApiPokemonScan) []*ApiPokemonResult {
results := make([]*ApiPokemonResult, 0, len(returnKeys))

for _, key := range returnKeys {
if pokemonCacheEntry := getPokemonFromCache(key); pokemonCacheEntry != nil {
pokemon := pokemonCacheEntry.Value()

apiPokemon := buildApiPokemonResult(&pokemon)
if pokemon := peekPokemonFromCache(key); pokemon != nil {
apiPokemon := buildApiPokemonResult(pokemon)
results = append(results, &apiPokemon)
}
}
Expand Down
11 changes: 3 additions & 8 deletions decoder/api_pokemon_scan_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,12 @@ func GetPokemonInArea2(retrieveParameters ApiPokemonScan2) []*ApiPokemonResult {
startUnix := start.Unix()

for _, key := range returnKeys {
if pokemonCacheEntry := getPokemonFromCache(key); pokemonCacheEntry != nil {
pokemon := pokemonCacheEntry.Value()

if pokemon := peekPokemonFromCache(key); pokemon != nil {
if pokemon.ExpireTimestamp.ValueOrZero() < startUnix {
continue
}

apiPokemon := buildApiPokemonResult(&pokemon)

apiPokemon := buildApiPokemonResult(pokemon)
results = append(results, &apiPokemon)
}
}
Expand Down Expand Up @@ -185,9 +182,7 @@ func GrpcGetPokemonInArea2(retrieveParameters *pb.PokemonScanRequest) []*pb.Poke
startUnix := start.Unix()

for _, key := range returnKeys {
if pokemonCacheEntry := getPokemonFromCache(key); pokemonCacheEntry != nil {
pokemon := pokemonCacheEntry.Value()

if pokemon := peekPokemonFromCache(key); pokemon != nil {
if pokemon.ExpireTimestamp.ValueOrZero() < startUnix {
continue
}
Expand Down
11 changes: 3 additions & 8 deletions decoder/api_pokemon_scan_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,13 @@ func GetPokemonInArea3(retrieveParameters ApiPokemonScan3) *PokemonScan3Result {
startUnix := start.Unix()

for _, key := range returnKeys {
if pokemonCacheEntry := getPokemonFromCache(key); pokemonCacheEntry != nil {
pokemon := pokemonCacheEntry.Value()

if pokemon := peekPokemonFromCache(key); pokemon != nil {
if pokemon.ExpireTimestamp.ValueOrZero() < startUnix {
examined--
continue
}

apiPokemon := buildApiPokemonResult(&pokemon)

apiPokemon := buildApiPokemonResult(pokemon)
results = append(results, &apiPokemon)
}
}
Expand Down Expand Up @@ -204,9 +201,7 @@ func GrpcGetPokemonInArea3(retrieveParameters *pb.PokemonScanRequestV3) ([]*pb.P
startUnix := start.Unix()

for _, key := range returnKeys {
if pokemonCacheEntry := getPokemonFromCache(key); pokemonCacheEntry != nil {
pokemon := pokemonCacheEntry.Value()

if pokemon := peekPokemonFromCache(key); pokemon != nil {
if pokemon.ExpireTimestamp.ValueOrZero() < startUnix {
continue
}
Expand Down
36 changes: 27 additions & 9 deletions decoder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ var weatherCache *ttlcache.Cache[int64, Weather]
var weatherConsensusCache *ttlcache.Cache[int64, *WeatherConsensusState]
var s2CellCache *ttlcache.Cache[uint64, S2Cell]
var spawnpointCache *ttlcache.Cache[int64, Spawnpoint]
var pokemonCache []*ttlcache.Cache[uint64, Pokemon]
var pokemonCache []*ttlcache.Cache[uint64, *Pokemon]
var incidentCache *ttlcache.Cache[string, Incident]
var playerCache *ttlcache.Cache[string, Player]
var routeCache *ttlcache.Cache[string, Route]
Expand Down Expand Up @@ -101,16 +101,34 @@ func (cl *gohbemLogger) Print(message string) {
log.Info("Gohbem - ", message)
}

func getPokemonCache(key uint64) *ttlcache.Cache[uint64, Pokemon] {
func getPokemonCache(key uint64) *ttlcache.Cache[uint64, *Pokemon] {
return pokemonCache[key%uint64(len(pokemonCache))]
}

func setPokemonCache(key uint64, value Pokemon, ttl time.Duration) {
func setPokemonCache(key uint64, value *Pokemon, ttl time.Duration) {
getPokemonCache(key).Set(key, value, ttl)
}

func getPokemonFromCache(key uint64) *ttlcache.Item[uint64, Pokemon] {
return getPokemonCache(key).Get(key)
// getPokemonFromCache returns a COPY of the cached Pokemon to preserve
// change detection semantics. Returns nil if not found.
// Use this when you need to modify the pokemon and compare changes.
func getPokemonFromCache(key uint64) *Pokemon {
item := getPokemonCache(key).Get(key)
if item == nil {
return nil
}
pokemonCopy := *item.Value()
return &pokemonCopy
}

// peekPokemonFromCache returns the cached Pokemon pointer directly without copying.
// Use this for read-only access where no modifications will be made.
func peekPokemonFromCache(key uint64) *Pokemon {
item := getPokemonCache(key).Get(key)
if item == nil {
return nil
}
return item.Value()
}

func deletePokemonFromCache(key uint64) {
Expand Down Expand Up @@ -160,11 +178,11 @@ func initDataCache() {

// pokemon is the most active table. Use an array of caches to increase concurrency for querying ttlcache, which places a global lock for each Get/Set operation
// Initialize pokemon cache array: by picking it to be nproc, we should expect ~nproc*(1-1/e) ~ 63% concurrency
pokemonCache = make([]*ttlcache.Cache[uint64, Pokemon], runtime.NumCPU())
pokemonCache = make([]*ttlcache.Cache[uint64, *Pokemon], runtime.NumCPU())
for i := 0; i < len(pokemonCache); i++ {
pokemonCache[i] = ttlcache.New[uint64, Pokemon](
ttlcache.WithTTL[uint64, Pokemon](60*time.Minute),
ttlcache.WithDisableTouchOnHit[uint64, Pokemon](), // Pokemon will last 60 mins from when we first see them not last see them
pokemonCache[i] = ttlcache.New[uint64, *Pokemon](
ttlcache.WithTTL[uint64, *Pokemon](60*time.Minute),
ttlcache.WithDisableTouchOnHit[uint64, *Pokemon](), // Pokemon will last 60 mins from when we first see them not last see them
)
go pokemonCache[i].Start()
}
Expand Down
55 changes: 29 additions & 26 deletions decoder/pokemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ type Pokemon struct {
Pvp null.String `db:"pvp" json:"pvp"`
IsEvent int8 `db:"is_event" json:"is_event"`

internal grpc.PokemonInternal
internal PokemonInternalNative
}

//
Expand Down Expand Up @@ -133,10 +133,8 @@ type Pokemon struct {

func getPokemonRecord(ctx context.Context, db db.DbDetails, encounterId uint64) (*Pokemon, error) {
if db.UsePokemonCache {
inMemoryPokemon := getPokemonFromCache(encounterId)
if inMemoryPokemon != nil {
pokemon := inMemoryPokemon.Value()
return &pokemon, nil
if pokemon := getPokemonFromCache(encounterId); pokemon != nil {
return pokemon, nil
}
}
if config.Config.PokemonMemoryOnly {
Expand All @@ -161,7 +159,7 @@ func getPokemonRecord(ctx context.Context, db db.DbDetails, encounterId uint64)
}

if db.UsePokemonCache {
setPokemonCache(encounterId, pokemon, ttlcache.DefaultTTL)
setPokemonCache(encounterId, &pokemon, ttlcache.DefaultTTL)
}
pokemonRtreeUpdatePokemonOnGet(&pokemon)
return &pokemon, nil
Expand All @@ -174,7 +172,7 @@ func getOrCreatePokemonRecord(ctx context.Context, db db.DbDetails, encounterId
}
pokemon = &Pokemon{Id: encounterId}
if db.UsePokemonCache {
setPokemonCache(encounterId, *pokemon, ttlcache.DefaultTTL)
setPokemonCache(encounterId, pokemon, ttlcache.DefaultTTL)
}
return pokemon, nil
}
Expand Down Expand Up @@ -222,7 +220,8 @@ func hasChangesPokemon(old *Pokemon, new *Pokemon) bool {
}

func savePokemonRecordAsAtTime(ctx context.Context, db db.DbDetails, pokemon *Pokemon, isEncounter, writeDB, webhook bool, now int64) {
oldPokemon, _ := getPokemonRecord(ctx, db, pokemon.Id)
// Use peek for oldPokemon - we only need it for read-only comparison, no copy needed
oldPokemon := peekPokemonFromCache(pokemon.Id)

if oldPokemon != nil && !hasChangesPokemon(oldPokemon, pokemon) {
return
Expand Down Expand Up @@ -299,7 +298,8 @@ func savePokemonRecordAsAtTime(ctx context.Context, db db.DbDetails, pokemon *Po
if strong != nil {
strong.RemoveDittoAuxInfo()
}
marshaled, err := proto.Marshal(&pokemon.internal)
pb := pokemon.internal.ToProto()
marshaled, err := proto.Marshal(pb)
if err == nil {
pokemon.GolbatInternal = marshaled
} else {
Expand Down Expand Up @@ -408,7 +408,7 @@ func savePokemonRecordAsAtTime(ctx context.Context, db db.DbDetails, pokemon *Po
pokemon.Pvp = null.NewString("", false) // Reset PVP field to avoid keeping it in memory cache

if db.UsePokemonCache {
setPokemonCache(pokemon.Id, *pokemon, pokemon.remainingDuration(now))
setPokemonCache(pokemon.Id, pokemon, pokemon.remainingDuration(now))
}
}

Expand Down Expand Up @@ -501,16 +501,19 @@ func (pokemon *Pokemon) populateInternal() {
if len(pokemon.GolbatInternal) == 0 || len(pokemon.internal.ScanHistory) != 0 {
return
}
err := proto.Unmarshal(pokemon.GolbatInternal, &pokemon.internal)
var pb grpc.PokemonInternal
err := proto.Unmarshal(pokemon.GolbatInternal, &pb)
if err != nil {
log.Warnf("Failed to parse internal data for %d: %s", pokemon.Id, err)
pokemon.internal.Reset()
pokemon.internal = PokemonInternalNative{}
return
}
pokemon.internal = PokemonInternalFromProto(&pb)
}

func (pokemon *Pokemon) locateScan(isStrong bool, isBoosted bool) (*grpc.PokemonScan, bool) {
func (pokemon *Pokemon) locateScan(isStrong bool, isBoosted bool) (*PokemonScanNative, bool) {
pokemon.populateInternal()
var bestMatching *grpc.PokemonScan
var bestMatching *PokemonScanNative
for _, entry := range pokemon.internal.ScanHistory {
if entry.Strong != isStrong {
continue
Expand All @@ -524,7 +527,7 @@ func (pokemon *Pokemon) locateScan(isStrong bool, isBoosted bool) (*grpc.Pokemon
return bestMatching, false
}

func (pokemon *Pokemon) locateAllScans() (unboosted, boosted, strong *grpc.PokemonScan) {
func (pokemon *Pokemon) locateAllScans() (unboosted, boosted, strong *PokemonScanNative) {
pokemon.populateInternal()
for _, entry := range pokemon.internal.ScanHistory {
if entry.Strong {
Expand Down Expand Up @@ -697,7 +700,7 @@ func (pokemon *Pokemon) updateFromNearby(ctx context.Context, db db.DbDetails, n
if overrideLatLon {
pokemon.Lat, pokemon.Lon = lat, lon
} else {
midpoint := s2.LatLngFromPoint(s2.Point{s2.PointFromLatLng(s2.LatLngFromDegrees(pokemon.Lat, pokemon.Lon)).
midpoint := s2.LatLngFromPoint(s2.Point{Vector: s2.PointFromLatLng(s2.LatLngFromDegrees(pokemon.Lat, pokemon.Lon)).
Add(s2.PointFromLatLng(s2.LatLngFromDegrees(lat, lon)).Vector)})
pokemon.Lat = midpoint.Lat.Degrees()
pokemon.Lon = midpoint.Lng.Degrees()
Expand Down Expand Up @@ -762,14 +765,14 @@ func (pokemon *Pokemon) setUnknownTimestamp(now int64) {
}
}

func checkScans(old *grpc.PokemonScan, new *grpc.PokemonScan) error {
func checkScans(old *PokemonScanNative, new *PokemonScanNative) error {
if old == nil || old.CompressedIv() == new.CompressedIv() {
return nil
}
return errors.New(fmt.Sprintf("Unexpected IV mismatch %s != %s", old, new))
}

func (pokemon *Pokemon) setDittoAttributes(mode string, isDitto bool, old, new *grpc.PokemonScan) {
func (pokemon *Pokemon) setDittoAttributes(mode string, isDitto bool, old, new *PokemonScanNative) {
if isDitto {
log.Debugf("[POKEMON] %d: %s Ditto found %s -> %s", pokemon.Id, mode, old, new)
pokemon.IsDitto = true
Expand All @@ -779,7 +782,7 @@ func (pokemon *Pokemon) setDittoAttributes(mode string, isDitto bool, old, new *
log.Debugf("[POKEMON] %d: %s not Ditto found %s -> %s", pokemon.Id, mode, old, new)
}
}
func (pokemon *Pokemon) resetDittoAttributes(mode string, old, aux, new *grpc.PokemonScan) (*grpc.PokemonScan, error) {
func (pokemon *Pokemon) resetDittoAttributes(mode string, old, aux, new *PokemonScanNative) (*PokemonScanNative, error) {
log.Debugf("[POKEMON] %d: %s Ditto was reset %s (%s) -> %s", pokemon.Id, mode, old, aux, new)
pokemon.IsDitto = false
pokemon.DisplayPokemonId = null.NewInt(0, false)
Expand All @@ -790,7 +793,7 @@ func (pokemon *Pokemon) resetDittoAttributes(mode string, old, aux, new *grpc.Po
// As far as I'm concerned, wild Ditto only depends on species but not costume/gender/form
var dittoDisguises sync.Map

func confirmDitto(scan *grpc.PokemonScan) {
func confirmDitto(scan *PokemonScanNative) {
now := time.Now()
lastSeen, exists := dittoDisguises.Swap(scan.Pokemon, now)
if exists {
Expand All @@ -813,7 +816,7 @@ func confirmDitto(scan *grpc.PokemonScan) {

// detectDitto returns the IV/level set that should be used for persisting to db/seen if caught.
// error is set if something unexpected happened and the scan history should be cleared.
func (pokemon *Pokemon) detectDitto(scan *grpc.PokemonScan) (*grpc.PokemonScan, error) {
func (pokemon *Pokemon) detectDitto(scan *PokemonScanNative) (*PokemonScanNative, error) {
unboostedScan, boostedScan, strongScan := pokemon.locateAllScans()
if scan.Strong {
if strongScan != nil {
Expand Down Expand Up @@ -886,7 +889,7 @@ func (pokemon *Pokemon) detectDitto(scan *grpc.PokemonScan) (*grpc.PokemonScan,
}

isBoosted := scan.Weather != int32(pogo.GameplayWeatherProto_NONE)
var matchingScan *grpc.PokemonScan
var matchingScan *PokemonScanNative
if unboostedScan != nil || boostedScan != nil {
if unboostedScan != nil && boostedScan != nil { // if we have both IVs then they must be correct
if unboostedScan.Level == scan.Level {
Expand Down Expand Up @@ -1112,7 +1115,7 @@ func (pokemon *Pokemon) addEncounterPokemon(ctx context.Context, db db.DbDetails
pokemon.Size = null.IntFrom(int64(proto.Size))
pokemon.Weight = null.FloatFrom(float64(proto.WeightKg))

scan := grpc.PokemonScan{
scan := PokemonScanNative{
Weather: int32(pokemon.Weather.Int64),
Strong: pokemon.IsStrong.Bool,
Attack: proto.IndividualAttack,
Expand Down Expand Up @@ -1153,7 +1156,7 @@ func (pokemon *Pokemon) addEncounterPokemon(ctx context.Context, db db.DbDetails
pokemon.calculateIv(int64(caughtIv.Attack), int64(caughtIv.Defense), int64(caughtIv.Stamina))
}
if err == nil {
newScans := make([]*grpc.PokemonScan, len(pokemon.internal.ScanHistory)+1)
newScans := make([]*PokemonScanNative, len(pokemon.internal.ScanHistory)+1)
entriesCount := 0
for _, oldEntry := range pokemon.internal.ScanHistory {
if oldEntry.Strong != scan.Strong || !oldEntry.Strong &&
Expand All @@ -1169,7 +1172,7 @@ func (pokemon *Pokemon) addEncounterPokemon(ctx context.Context, db db.DbDetails
// undo possible changes
scan.Confirmed = false
scan.Weather = int32(pokemon.Weather.Int64)
pokemon.internal.ScanHistory = make([]*grpc.PokemonScan, 1)
pokemon.internal.ScanHistory = make([]*PokemonScanNative, 1)
pokemon.internal.ScanHistory[0] = &scan
}
}
Expand Down Expand Up @@ -1342,7 +1345,7 @@ func (pokemon *Pokemon) recomputeCpIfNeeded(ctx context.Context, db db.DbDetails
}
var displayPokemon int
shouldOverrideIv := false
var overrideIv *grpc.PokemonScan
var overrideIv *PokemonScanNative
if pokemon.IsDitto {
displayPokemon = int(pokemon.DisplayPokemonId.Int64)
if pokemon.Weather.Int64 == int64(pogo.GameplayWeatherProto_NONE) {
Expand Down
4 changes: 2 additions & 2 deletions decoder/pokemonRtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ func initPokemonRtree() {

// Set up OnEviction callbacks for each cache in the array
for i := range pokemonCache {
pokemonCache[i].OnEviction(func(ctx context.Context, ev ttlcache.EvictionReason, v *ttlcache.Item[uint64, Pokemon]) {
pokemonCache[i].OnEviction(func(ctx context.Context, ev ttlcache.EvictionReason, v *ttlcache.Item[uint64, *Pokemon]) {
r := v.Value()
removePokemonFromTree(&r)
removePokemonFromTree(r)
// Rely on the pokemon pvp lookup caches to remove themselves rather than trying to synchronise
})
}
Expand Down
Loading