Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
53b9b4c
docs: design spec for migrating remaining APIs to Huma
jfberry May 30, 2026
8756f83
docs: implementation plan for migrating remaining APIs to Huma
jfberry May 30, 2026
35c64b5
refactor: unify fort DNF range types (hide int8/int16 from schema)
jfberry May 30, 2026
70bf6cb
fix: fort scan bounding box takes ApiLatLon (lat/lon), not geo.Location
jfberry May 30, 2026
9f221d3
feat: add draft-badge helper and bytes-based geofence parser
jfberry May 30, 2026
bc363ac
refactor: ApiGymResult pointer-based + doc tags (Huma-documentable)
jfberry May 30, 2026
aaf16ed
refactor: ApiPokestopResult pointer-based + doc tags
jfberry May 30, 2026
595c6f3
refactor: ApiStationResult pointer-based + doc tags
jfberry May 30, 2026
9e0d0e1
docs: doc tags + required/optional on fort scan request/result types
jfberry May 30, 2026
c51e3a6
feat: serve fort scan via Huma (draft), retire gin handlers
jfberry May 30, 2026
20db989
feat: serve pokemon search + by-id via Huma
jfberry May 30, 2026
8210313
refactor: ApiTappableResult pointer-based + doc tags
jfberry May 30, 2026
4753014
feat: serve tier-3 read endpoints via Huma
jfberry May 30, 2026
2ca1000
feat: serve quest-status + clear-quests via Huma (geofence body)
jfberry May 30, 2026
b597f1d
feat: serve tier-4 operational endpoints via Huma
jfberry May 30, 2026
d61e037
docs: record results of remaining-APIs Huma migration
jfberry May 30, 2026
861de0b
fix: correct stale lock-instrumentation caller label in search-gyms
jfberry May 30, 2026
dc487e6
feat: serve GET /api/pokemon/available via Huma
jfberry May 30, 2026
5120eeb
refactor: geofence endpoints take json.RawMessage, documented as JSON
jfberry May 31, 2026
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
114 changes: 55 additions & 59 deletions decoder/api_fort.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,92 +9,88 @@ import (

"golbat/config"
"golbat/db"
"golbat/geo"
)

type ApiFortScan struct {
Min geo.Location `json:"min"`
Max geo.Location `json:"max"`
Limit int `json:"limit"`
DnfFilters []ApiFortDnfFilter `json:"filters"`
Min ApiLatLon `json:"min" doc:"SW (minimum lat/lon) corner of the bounding box."`
Max ApiLatLon `json:"max" doc:"NE (maximum lat/lon) corner of the bounding box."`
Limit int `json:"limit" required:"false" doc:"Max results to return; 0 uses the server default."`
DnfFilters []ApiFortDnfFilter `json:"filters" required:"false" doc:"OR'd filter clauses; a fort matches if it satisfies any one clause."`
}

type ApiFortDnfFilter struct {
PowerUpLevel *ApiFortDnfMinMax8 `json:"power_up_level"`
IsArScanEligible *bool `json:"is_ar_scan_eligible"`
PowerUpLevel *ApiFortDnfMinMax `json:"power_up_level" required:"false" doc:"Inclusive power-up level range; null means no power-up level constraint."`
IsArScanEligible *bool `json:"is_ar_scan_eligible" required:"false" doc:"When true, only match forts that are AR scan eligible; null means no AR eligibility constraint."`

// Gym
AvailableSlots *ApiFortDnfMinMax8 `json:"available_slots"`
TeamId []int8 `json:"team_id"`
RaidLevel []int8 `json:"raid_level"`
RaidPokemon []ApiDnfId `json:"raid_pokemon_id"`
AvailableSlots *ApiFortDnfMinMax `json:"available_slots" required:"false" doc:"Gym only: inclusive range of open defender slots; null means no slot constraint."`
TeamId []int8 `json:"team_id" required:"false" doc:"Gym only: allowed controlling team ids; empty means no team constraint."`
RaidLevel []int8 `json:"raid_level" required:"false" doc:"Gym only: allowed active raid levels; empty means no raid level constraint. Only matches gyms with an active raid."`
RaidPokemon []ApiDnfId `json:"raid_pokemon_id" required:"false" doc:"Gym only: allowed active raid boss pokemon/form pairs; empty means no raid pokemon constraint. Only matches gyms with an active raid."`

// Pokestop - unified quest (matches AR or no-AR)
LureId []int16 `json:"lure_id"`
QuestRewardType []int16 `json:"quest_reward_type"`
QuestRewardAmount *ApiFortDnfMinMax16 `json:"quest_reward_amount"`
QuestRewardItemId []int16 `json:"quest_reward_item_id"`
QuestRewardPokemon []ApiDnfId `json:"quest_reward_pokemon"`
LureId []int16 `json:"lure_id" required:"false" doc:"Pokestop only: allowed active lure module ids; empty means no lure constraint."`
QuestRewardType []int16 `json:"quest_reward_type" required:"false" doc:"Pokestop only: allowed quest reward types; matched against either the AR or no-AR quest. Empty means no reward type constraint."`
QuestRewardAmount *ApiFortDnfMinMax `json:"quest_reward_amount" required:"false" doc:"Pokestop only: inclusive quest reward amount range; matched against either the AR or no-AR quest. Null means no reward amount constraint."`
QuestRewardItemId []int16 `json:"quest_reward_item_id" required:"false" doc:"Pokestop only: allowed quest reward item ids; matched against either the AR or no-AR quest. Empty means no reward item constraint."`
QuestRewardPokemon []ApiDnfId `json:"quest_reward_pokemon" required:"false" doc:"Pokestop only: allowed quest reward pokemon/form pairs; matched against either the AR or no-AR quest. Empty means no reward pokemon constraint."`

// Pokestop - incident
IncidentDisplayType []int8 `json:"incident_display_type"`
IncidentStyle []int8 `json:"incident_style"`
IncidentCharacter []int16 `json:"incident_character"`
IncidentPokemon []ApiDnfId `json:"incident_pokemon"`
IncidentDisplayType []int8 `json:"incident_display_type" required:"false" doc:"Pokestop only: allowed incident display types; empty means no incident display type constraint."`
IncidentStyle []int8 `json:"incident_style" required:"false" doc:"Pokestop only: allowed incident styles; empty means no incident style constraint."`
IncidentCharacter []int16 `json:"incident_character" required:"false" doc:"Pokestop only: allowed incident character ids; empty means no incident character constraint."`
IncidentPokemon []ApiDnfId `json:"incident_pokemon" required:"false" doc:"Pokestop only: allowed incident pokemon/form pairs; empty means no incident pokemon constraint."`

// Pokestop - contest
ContestPokemon []ApiDnfId `json:"contest_pokemon"`
ContestPokemonType []int8 `json:"contest_pokemon_type"`
ContestTotalEntries *ApiFortDnfMinMax16 `json:"contest_total_entries"`
ContestPokemon []ApiDnfId `json:"contest_pokemon" required:"false" doc:"Pokestop only: allowed contest focus pokemon/form pairs; empty means no contest pokemon constraint."`
ContestPokemonType []int8 `json:"contest_pokemon_type" required:"false" doc:"Pokestop only: allowed contest pokemon types; empty means no contest type constraint."`
ContestTotalEntries *ApiFortDnfMinMax `json:"contest_total_entries" required:"false" doc:"Pokestop only: inclusive range for the contest's total number of entries; null means no contest entries constraint."`

// Station
BattleLevel []int8 `json:"battle_level"`
BattlePokemon []ApiDnfId `json:"battle_pokemon"`
BattleLevel []int8 `json:"battle_level" required:"false" doc:"Station only: allowed active max battle levels; empty means no battle level constraint. Only matches stations with an active battle."`
BattlePokemon []ApiDnfId `json:"battle_pokemon" required:"false" doc:"Station only: allowed active max battle pokemon/form pairs; empty means no battle pokemon constraint. Only matches stations with an active battle."`
}

type ApiDnfId struct {
Pokemon int16 `json:"pokemon_id"`
Form *int16 `json:"form"`
Pokemon int16 `json:"pokemon_id" doc:"Pokedex id to match. Required within an entry — a form without an id can never match."`
Form *int16 `json:"form" required:"false" doc:"Form id to match; null matches any form of the given id."`
}

type ApiFortDnfMinMax8 struct {
Min int8 `json:"min"`
Max int8 `json:"max"`
}

type ApiFortDnfMinMax16 struct {
Min int16 `json:"min"`
Max int16 `json:"max"`
// ApiFortDnfMinMax is an inclusive integer range used by the fort filter clauses
// (int16 internally — wide enough for all fort range fields).
type ApiFortDnfMinMax struct {
Min int16 `json:"min" doc:"Minimum value (inclusive)."`
Max int16 `json:"max" doc:"Maximum value (inclusive)."`
}

type ApiGymScanResult struct {
Gyms []*ApiGymResult `json:"gyms"`
Examined int `json:"examined"`
Skipped int `json:"skipped"`
Total int `json:"total"`
Gyms []*ApiGymResult `json:"gyms" doc:"Matching gyms within the bounding box."`
Examined int `json:"examined" doc:"Number of forts examined during the spatial scan."`
Skipped int `json:"skipped" doc:"Number of forts skipped because they were not found in the lookup cache."`
Total int `json:"total" doc:"Total number of forts in the spatial index at scan time."`
}

type ApiPokestopScanResult struct {
Pokestops []*ApiPokestopResult `json:"pokestops"`
Examined int `json:"examined"`
Skipped int `json:"skipped"`
Total int `json:"total"`
Pokestops []*ApiPokestopResult `json:"pokestops" doc:"Matching pokestops within the bounding box."`
Examined int `json:"examined" doc:"Number of forts examined during the spatial scan."`
Skipped int `json:"skipped" doc:"Number of forts skipped because they were not found in the lookup cache."`
Total int `json:"total" doc:"Total number of forts in the spatial index at scan time."`
}

type ApiStationScanResult struct {
Stations []*ApiStationResult `json:"stations"`
Examined int `json:"examined"`
Skipped int `json:"skipped"`
Total int `json:"total"`
Stations []*ApiStationResult `json:"stations" doc:"Matching stations within the bounding box."`
Examined int `json:"examined" doc:"Number of forts examined during the spatial scan."`
Skipped int `json:"skipped" doc:"Number of forts skipped because they were not found in the lookup cache."`
Total int `json:"total" doc:"Total number of forts in the spatial index at scan time."`
}

type ApiFortCombinedScanResult struct {
Gyms []*ApiGymResult `json:"gyms"`
Pokestops []*ApiPokestopResult `json:"pokestops"`
Stations []*ApiStationResult `json:"stations"`
Examined int `json:"examined"`
Skipped int `json:"skipped"`
Total int `json:"total"`
Gyms []*ApiGymResult `json:"gyms" doc:"Matching gyms within the bounding box."`
Pokestops []*ApiPokestopResult `json:"pokestops" doc:"Matching pokestops within the bounding box."`
Stations []*ApiStationResult `json:"stations" doc:"Matching stations within the bounding box."`
Examined int `json:"examined" doc:"Number of forts examined during the spatial scan."`
Skipped int `json:"skipped" doc:"Number of forts skipped because they were not found in the lookup cache."`
Total int `json:"total" doc:"Total number of forts in the spatial index at scan time."`
}

// matchDnfIdPair checks if any ApiDnfId in the filter matches the given pokemon/form pair
Expand All @@ -112,7 +108,7 @@ func isFortDnfMatch(fortType FortType, fortLookup *FortLookup, filter *ApiFortDn
if fortType != 0 && fortType != fortLookup.FortType {
return false
}
if filter.PowerUpLevel != nil && (fortLookup.PowerUpLevel < filter.PowerUpLevel.Min || fortLookup.PowerUpLevel > filter.PowerUpLevel.Max) {
if filter.PowerUpLevel != nil && (int16(fortLookup.PowerUpLevel) < filter.PowerUpLevel.Min || int16(fortLookup.PowerUpLevel) > filter.PowerUpLevel.Max) {
return false
}
if filter.IsArScanEligible != nil && !fortLookup.IsArScanEligible {
Expand All @@ -121,7 +117,7 @@ func isFortDnfMatch(fortType FortType, fortLookup *FortLookup, filter *ApiFortDn

switch fortLookup.FortType {
case GYM:
if filter.AvailableSlots != nil && (fortLookup.AvailableSlots < filter.AvailableSlots.Min || fortLookup.AvailableSlots > filter.AvailableSlots.Max) {
if filter.AvailableSlots != nil && (int16(fortLookup.AvailableSlots) < filter.AvailableSlots.Min || int16(fortLookup.AvailableSlots) > filter.AvailableSlots.Max) {
return false
}
if filter.TeamId != nil && !slices.Contains(filter.TeamId, fortLookup.TeamId) {
Expand Down Expand Up @@ -217,8 +213,8 @@ func isFortDnfMatch(fortType FortType, fortLookup *FortLookup, filter *ApiFortDn
func internalGetForts(fortType FortType, retrieveParameters ApiFortScan) ([]string, int, int, int) {
start := time.Now()

minLocation := retrieveParameters.Min
maxLocation := retrieveParameters.Max
minLocation := retrieveParameters.Min.Location()
maxLocation := retrieveParameters.Max.Location()

maxForts := config.Config.Tuning.MaxPokemonResults
if retrieveParameters.Limit > 0 && retrieveParameters.Limit < maxForts {
Expand Down Expand Up @@ -407,8 +403,8 @@ func FortCombinedScanEndpoint(retrieveParameters ApiFortScan, dbDetails db.DbDet
func internalGetFortsCombined(retrieveParameters ApiFortScan) (gymKeys, pokestopKeys, stationKeys []string, examined, skipped, total int) {
start := time.Now()

minLocation := retrieveParameters.Min
maxLocation := retrieveParameters.Max
minLocation := retrieveParameters.Min.Location()
maxLocation := retrieveParameters.Max.Location()

maxForts := config.Config.Tuning.MaxPokemonResults
if retrieveParameters.Limit > 0 && retrieveParameters.Limit < maxForts {
Expand Down
Loading
Loading