From abf625fc517393470d1690536ed60d9ac03f00e3 Mon Sep 17 00:00:00 2001 From: James Berry Date: Sun, 11 Jan 2026 16:48:50 +0000 Subject: [PATCH 1/2] Fix go vet warnings - webhooks_test.go: Use t.Errorf instead of t.Fatalf in goroutine - decoder/pokemon.go: Add keyed field for s2.Point struct literal - decoder: Change pokemonCache to store *Pokemon to avoid copying mutex embedded in protobuf MessageState - Add peekPokemonFromCache for read-only access (no copy) - getPokemonFromCache returns a copy for change detection semantics - Use peekPokemonFromCache in savePokemonRecordAsAtTime for oldPokemon comparison (optimization from pokemon_less_gc branch) Co-Authored-By: Claude Opus 4.5 --- decoder/api_pokemon.go | 10 ++++------ decoder/api_pokemon_scan_v1.go | 6 ++---- decoder/api_pokemon_scan_v2.go | 11 +++-------- decoder/api_pokemon_scan_v3.go | 11 +++-------- decoder/main.go | 36 +++++++++++++++++++++++++--------- decoder/pokemon.go | 17 ++++++++-------- decoder/pokemonRtree.go | 4 ++-- decoder/weather_iv.go | 9 ++++----- webhooks/webhooks_test.go | 2 +- 9 files changed, 54 insertions(+), 52 deletions(-) diff --git a/decoder/api_pokemon.go b/decoder/api_pokemon.go index 0126cad4..5d997132 100644 --- a/decoder/api_pokemon.go +++ b/decoder/api_pokemon.go @@ -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 { @@ -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 diff --git a/decoder/api_pokemon_scan_v1.go b/decoder/api_pokemon_scan_v1.go index 737393ad..b07ba10a 100644 --- a/decoder/api_pokemon_scan_v1.go +++ b/decoder/api_pokemon_scan_v1.go @@ -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) } } diff --git a/decoder/api_pokemon_scan_v2.go b/decoder/api_pokemon_scan_v2.go index 878f3ab5..6dd25bdd 100644 --- a/decoder/api_pokemon_scan_v2.go +++ b/decoder/api_pokemon_scan_v2.go @@ -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) } } @@ -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 } diff --git a/decoder/api_pokemon_scan_v3.go b/decoder/api_pokemon_scan_v3.go index 8c5ef4ad..1fd07940 100644 --- a/decoder/api_pokemon_scan_v3.go +++ b/decoder/api_pokemon_scan_v3.go @@ -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) } } @@ -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 } diff --git a/decoder/main.go b/decoder/main.go index 42ec4c1b..87c37606 100644 --- a/decoder/main.go +++ b/decoder/main.go @@ -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] @@ -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) { @@ -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() } diff --git a/decoder/pokemon.go b/decoder/pokemon.go index 9244cc2b..55e824bd 100644 --- a/decoder/pokemon.go +++ b/decoder/pokemon.go @@ -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 { @@ -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 @@ -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 } @@ -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 @@ -408,7 +407,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)) } } @@ -697,7 +696,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() diff --git a/decoder/pokemonRtree.go b/decoder/pokemonRtree.go index 89e1edde..139d7eb4 100644 --- a/decoder/pokemonRtree.go +++ b/decoder/pokemonRtree.go @@ -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 }) } diff --git a/decoder/weather_iv.go b/decoder/weather_iv.go index 582e1760..d6355f71 100644 --- a/decoder/weather_iv.go +++ b/decoder/weather_iv.go @@ -203,7 +203,7 @@ func ProactiveIVSwitch(ctx context.Context, db db.DbDetails, weatherUpdate Weath pokemonLocked := 0 pokemonUpdated := 0 pokemonCpUpdated := 0 - var pokemon Pokemon + var pokemon *Pokemon pokemonTree2.Search([2]float64{cellLo.Lng.Degrees(), cellLo.Lat.Degrees()}, [2]float64{cellHi.Lng.Degrees(), cellHi.Lat.Degrees()}, func(min, max [2]float64, pokemonId uint64) bool { if !weatherCell.ContainsPoint(s2.PointFromLatLng(s2.LatLngFromDegrees(min[1], min[0]))) { return true @@ -227,9 +227,8 @@ func ProactiveIVSwitch(ctx context.Context, db db.DbDetails, weatherUpdate Weath pokemonMutex, _ := pokemonStripedMutex.GetLock(pokemonId) pokemonMutex.Lock() pokemonLocked++ - pokemonEntry := getPokemonFromCache(pokemonId) - if pokemonEntry != nil { - pokemon = pokemonEntry.Value() + pokemon = getPokemonFromCache(pokemonId) + if pokemon != nil { if pokemonLookup.PokemonLookup.PokemonId == pokemon.PokemonId && (pokemon.IsDitto || int64(pokemonLookup.PokemonLookup.Form) == pokemon.Form.ValueOrZero()) && int64(newWeather) != pokemon.Weather.ValueOrZero() && pokemon.ExpireTimestamp.ValueOrZero() >= startUnix && pokemon.Updated.ValueOrZero() < timestamp { pokemon.repopulateIv(int64(newWeather), pokemon.IsStrong.ValueOrZero()) if !pokemon.Cp.Valid { @@ -237,7 +236,7 @@ func ProactiveIVSwitch(ctx context.Context, db db.DbDetails, weatherUpdate Weath pokemon.recomputeCpIfNeeded(ctx, db, map[int64]pogo.GameplayWeatherProto_WeatherCondition{ weatherUpdate.S2CellId: pogo.GameplayWeatherProto_WeatherCondition(newWeather), }) - savePokemonRecordAsAtTime(ctx, db, &pokemon, false, toDB && pokemon.Cp.Valid, pokemon.Cp.Valid, timestamp) + savePokemonRecordAsAtTime(ctx, db, pokemon, false, toDB && pokemon.Cp.Valid, pokemon.Cp.Valid, timestamp) pokemonUpdated++ if pokemon.Cp.Valid { pokemonCpUpdated++ diff --git a/webhooks/webhooks_test.go b/webhooks/webhooks_test.go index c56b35e9..130a807f 100644 --- a/webhooks/webhooks_test.go +++ b/webhooks/webhooks_test.go @@ -161,7 +161,7 @@ func TestWebhooksFull(t *testing.T) { defer wg.Done() err := sender.Run(ctx) if err != nil { - t.Fatalf("unexpected error starting webhooksSender: %s", err) + t.Errorf("unexpected error starting webhooksSender: %s", err) } }() From 508b1138402421d7ba9a4c6d9953d515f385093e Mon Sep 17 00:00:00 2001 From: James Berry Date: Mon, 12 Jan 2026 14:47:45 +0000 Subject: [PATCH 2/2] Convert Pokemon internal protobuf to native Go struct Replace grpc.PokemonInternal with a native PokemonInternalNative struct that can be safely copied without mutex issues. The protobuf types contain embedded sync.Mutex via protoimpl.MessageState which causes go vet warnings when the Pokemon struct is copied. - Add PokemonScanNative and PokemonInternalNative types in decoder package - Add ToProto/FromProto conversion functions for serialization - Update all internal usages to use native types - Protobuf is now only used at DB serialization boundaries Co-Authored-By: Claude Opus 4.5 --- decoder/pokemon.go | 38 +++++----- decoder/pokemon_internal.go | 137 ++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 decoder/pokemon_internal.go diff --git a/decoder/pokemon.go b/decoder/pokemon.go index 55e824bd..0911f36f 100644 --- a/decoder/pokemon.go +++ b/decoder/pokemon.go @@ -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 } // @@ -298,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 { @@ -500,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 @@ -523,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 { @@ -761,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 @@ -778,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) @@ -789,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 { @@ -812,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 { @@ -885,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 { @@ -1111,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, @@ -1152,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 && @@ -1168,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 } } @@ -1341,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) { diff --git a/decoder/pokemon_internal.go b/decoder/pokemon_internal.go new file mode 100644 index 00000000..0c451005 --- /dev/null +++ b/decoder/pokemon_internal.go @@ -0,0 +1,137 @@ +package decoder + +import "golbat/grpc" + +// PokemonScanNative is a native Go equivalent of grpc.PokemonScan +// that can be safely copied (no embedded mutex from protobuf MessageState) +type PokemonScanNative struct { + // from display + Weather int32 + Strong bool + // from encounter + Level int32 + Attack int32 + Defense int32 + Stamina int32 + // for Ditto detection + CellWeather int32 + Pokemon int32 + Costume int32 + Gender int32 + Form int32 + // this is set if there is only one non-strong IV set but we were able to confirm it for some reason + Confirmed bool +} + +// PokemonInternalNative is a native Go equivalent of grpc.PokemonInternal +// that can be safely copied (no embedded mutex from protobuf MessageState) +type PokemonInternalNative struct { + ScanHistory []*PokemonScanNative +} + +// CompressedIv returns a compressed representation of the IV values +func (s *PokemonScanNative) CompressedIv() int32 { + return s.Attack | s.Defense<<4 | s.Stamina<<8 +} + +// MustBeBoosted returns true if the level indicates the Pokemon must be weather boosted +func (s *PokemonScanNative) MustBeBoosted() bool { + return s.Level > 30 && s.Level <= 35 +} + +// MustBeUnboosted returns true if the level/IVs indicate the Pokemon must be unboosted +func (s *PokemonScanNative) MustBeUnboosted() bool { + return s.Level <= 5 || s.Attack < 4 || s.Defense < 4 || s.Stamina < 4 +} + +// MustHaveRerolled returns true if the Pokemon must have rerolled based on comparing with another scan +func (s *PokemonScanNative) MustHaveRerolled(other *PokemonScanNative) bool { + return s.Strong != other.Strong || s.Pokemon != other.Pokemon || s.Costume != other.Costume || + s.Gender != other.Gender || s.Form != other.Form +} + +// RemoveDittoAuxInfo clears auxiliary info for saving space when no longer needed +func (s *PokemonScanNative) RemoveDittoAuxInfo() { + s.CellWeather = 0 + s.Pokemon = 0 + s.Costume = 0 + s.Gender = 0 + s.Form = 0 + s.Confirmed = false +} + +// String returns a string representation of the scan +func (s *PokemonScanNative) String() string { + // Delegate to the protobuf String() for consistent formatting + return s.ToProto().String() +} + +// ToProto converts a PokemonScanNative to a grpc.PokemonScan protobuf +func (s *PokemonScanNative) ToProto() *grpc.PokemonScan { + if s == nil { + return nil + } + return &grpc.PokemonScan{ + Weather: s.Weather, + Strong: s.Strong, + Level: s.Level, + Attack: s.Attack, + Defense: s.Defense, + Stamina: s.Stamina, + CellWeather: s.CellWeather, + Pokemon: s.Pokemon, + Costume: s.Costume, + Gender: s.Gender, + Form: s.Form, + Confirmed: s.Confirmed, + } +} + +// PokemonScanFromProto converts a grpc.PokemonScan protobuf to a PokemonScanNative +func PokemonScanFromProto(pb *grpc.PokemonScan) *PokemonScanNative { + if pb == nil { + return nil + } + return &PokemonScanNative{ + Weather: pb.Weather, + Strong: pb.Strong, + Level: pb.Level, + Attack: pb.Attack, + Defense: pb.Defense, + Stamina: pb.Stamina, + CellWeather: pb.CellWeather, + Pokemon: pb.Pokemon, + Costume: pb.Costume, + Gender: pb.Gender, + Form: pb.Form, + Confirmed: pb.Confirmed, + } +} + +// ToProto converts a PokemonInternalNative to a grpc.PokemonInternal protobuf +func (p *PokemonInternalNative) ToProto() *grpc.PokemonInternal { + if p == nil { + return nil + } + pb := &grpc.PokemonInternal{ + ScanHistory: make([]*grpc.PokemonScan, len(p.ScanHistory)), + } + for i, scan := range p.ScanHistory { + pb.ScanHistory[i] = scan.ToProto() + } + return pb +} + +// PokemonInternalFromProto converts a grpc.PokemonInternal protobuf to a PokemonInternalNative +func PokemonInternalFromProto(pb *grpc.PokemonInternal) PokemonInternalNative { + if pb == nil { + return PokemonInternalNative{} + } + native := PokemonInternalNative{ + ScanHistory: make([]*PokemonScanNative, len(pb.ScanHistory)), + } + for i, scan := range pb.ScanHistory { + native.ScanHistory[i] = PokemonScanFromProto(scan) + } + return native +}