Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 13 additions & 1 deletion config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,18 @@
"items": {
"__name": "DEFAULT_FILTERS_TAPPABLES_ITEMS",
"__format": "boolean"
},
"candy": {
"__name": "DEFAULT_FILTERS_TAPPABLES_CANDY",
"__format": "boolean"
},
"xlCandy": {
"__name": "DEFAULT_FILTERS_TAPPABLES_XL_CANDY",
"__format": "boolean"
},
"megaEnergy": {
"__name": "DEFAULT_FILTERS_TAPPABLES_MEGA_ENERGY",
"__format": "boolean"
}
},
"pokemon": {
Expand Down Expand Up @@ -2467,4 +2479,4 @@
}
}
}
}
}
5 changes: 4 additions & 1 deletion config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,10 @@
},
"tappables": {
"enabled": false,
"items": true
"items": true,
"candy": true,
"xlCandy": true,
"megaEnergy": true
},
"pokemon": {
"enabled": false,
Expand Down
4 changes: 3 additions & 1 deletion packages/types/lib/scanner.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,9 @@ export interface Tappable {
lon: number
type: string
fort_id: string | null
item_id: number
reward_type?: string | null
item_id: number | null
reward_pokemon_id?: number | null
count: number | null
expire_timestamp: number | null
expire_timestamp_verified: boolean
Expand Down
1 change: 1 addition & 0 deletions packages/types/lib/server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface DbContext {
hasStationedGmax: boolean
hasBattlePokemonStats: boolean
hasPokemonBackground: boolean
hasTappableRewards: boolean
hasPokemonShinyStats?: boolean
connection?: number
httpAuth?: { username: string; password: string } | null
Expand Down
15 changes: 13 additions & 2 deletions server/src/filters/builder/tappable.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,19 @@ function buildTappables(perms, defaults) {
})

state.event.getAvailable('tappables').forEach((key) => {
if (!filters[key]) {
filters[key] = new BaseFilter(defaults.items)
if (filters[key]) return
switch (key.charAt(0)) {
case 'c':
filters[key] = new BaseFilter(defaults.candy)
break
case 'x':
filters[key] = new BaseFilter(defaults.xlCandy)
break
case 'm':
filters[key] = new BaseFilter(defaults.megaEnergy)
break
default:
filters[key] = new BaseFilter(defaults.items)
}
})

Expand Down
2 changes: 2 additions & 0 deletions server/src/graphql/typeDefs/scanner.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ type Tappable {
lon: Float
type: String
fort_id: String
reward_type: String
item_id: Int
reward_pokemon_id: Int
count: Int
expire_timestamp: Int
expire_timestamp_verified: Boolean
Expand Down
146 changes: 90 additions & 56 deletions server/src/models/Tappable.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,37 @@ const { getAreaSql } = require('../utils/getAreaSql')
const { applyManualIdFilter } = require('../utils/manualFilter')
const { getEpoch } = require('../utils/getClientTime')

// Golbat stores the first loot item's LootItemProto field name as reward_type
const REWARD_FILTERS = [
{ prefix: 'q', rewardTypes: ['item'], column: 'item_id' },
{ prefix: 'c', rewardTypes: ['pokemon_candy'], column: 'reward_pokemon_id' },
{ prefix: 'x', rewardTypes: ['xl_candy'], column: 'reward_pokemon_id' },
{
prefix: 'm',
rewardTypes: ['mega_energy_pokemon_id', 'temp_evo_pokemon_branch'],
column: 'reward_pokemon_id',
},
]

const SELECT_COLUMNS = [
'id',
'lat',
'lon',
'type',
'fort_id',
'item_id',
'count',
'expire_timestamp',
'expire_timestamp_verified',
'updated',
]

/** @param {boolean} [hasTappableRewards] */
const getSelectColumns = (hasTappableRewards) =>
hasTappableRewards
? [...SELECT_COLUMNS, 'reward_type', 'reward_pokemon_id']
: SELECT_COLUMNS

class Tappable extends Model {
static get tableName() {
return 'tappable'
Expand All @@ -20,28 +51,18 @@ class Tappable extends Model {
* minLon: number,
* maxLon: number,
* }} args
* @param {import('@rm/types').DbContext} ctx
* @returns {Promise<import('@rm/types').FullTappable[]>}
*/
static async getAll(perms, args) {
static async getAll(perms, args, { hasTappableRewards }) {
if (!perms?.tappables) return []

const { filters: filterArgs = {}, minLat, maxLat, minLon, maxLon } = args

const { queryLimits = {} } = config.getSafe('api')
const timestamp = getEpoch()

const query = this.query().select([
'id',
'lat',
'lon',
'type',
'fort_id',
'item_id',
'count',
'expire_timestamp',
'expire_timestamp_verified',
'updated',
])
const query = this.query().select(getSelectColumns(hasTappableRewards))

applyManualIdFilter(query, {
manualId: filterArgs.onlyManualId,
Expand All @@ -56,28 +77,38 @@ class Tappable extends Model {
return []
}

query.whereNull('pokemon_id').whereNotNull('item_id')
query.whereNull('pokemon_id')

query.andWhere('expire_timestamp', '>', timestamp)

const itemIds = []
/** @type {Record<string, number[]>} */
const ids = Object.fromEntries(
REWARD_FILTERS.map(({ prefix }) => [prefix, []]),
)
Object.keys(filterArgs).forEach((key) => {
if (!key || key.startsWith('only')) return
switch (key.charAt(0)) {
case 'q': {
const itemId = Number.parseInt(key.slice(1), 10)
if (!Number.isNaN(itemId) && itemId !== 0) {
itemIds.push(itemId)
}
break
}
default:
break
if (!key || key.startsWith('only') || !ids[key.charAt(0)]) return
const id = Number.parseInt(key.slice(1), 10)
if (!Number.isNaN(id) && id !== 0) {
ids[key.charAt(0)].push(id)
}
})

if (itemIds.length) {
query.whereIn('item_id', itemIds)
if (hasTappableRewards) {
const hasIdFilter = Object.values(ids).some((list) => list.length)
query.andWhere((rewards) => {
REWARD_FILTERS.forEach(({ prefix, rewardTypes, column }) => {
if (hasIdFilter && !ids[prefix].length) return
rewards.orWhere((reward) => {
reward.whereIn('reward_type', rewardTypes).whereNotNull(column)
if (hasIdFilter) reward.whereIn(column, ids[prefix])
})
})
})
} else {
query.whereNotNull('item_id')
if (ids.q.length) {
query.whereIn('item_id', ids.q)
Comment on lines +110 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Suppress legacy items for reward-only selections

In the supported multi-schema DbManager path, one source can have migration 55 while another remains legacy, and the merged availability list can therefore expose (for example) c25. When the client selects only that candy filter, the legacy source reaches this branch with ids.c populated and ids.q empty, so it applies only item_id is not null and returns every item tappable even though no item filter is enabled. Distinguish an entirely unfiltered request from a request containing only reward kinds unsupported by this source, and return no rows for the latter.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 545b5a8: a source without the reward columns now returns no rows when the selection contains only candy / XL candy / mega energy keys (no query is issued). An unfiltered request still returns all item tappables. Covered by a new test in server/test/tappableRewards.test.js.

}
}

query.orderBy('updated', 'desc')
Expand All @@ -95,25 +126,15 @@ class Tappable extends Model {
* Retrieve tappable metadata for a specific tappable id.
* @param {import('@rm/types').Permissions} perms
* @param {string | number} tappableId
* @param {import('@rm/types').DbContext} ctx
* @returns {Promise<import('@rm/types').Tappable[]>}
*/
static async getById(perms, tappableId) {
static async getById(perms, tappableId, { hasTappableRewards }) {
if (!perms?.tappables || !tappableId) {
return []
}

const query = this.query().select([
'id',
'lat',
'lon',
'type',
'fort_id',
'item_id',
'count',
'expire_timestamp',
'expire_timestamp_verified',
'updated',
])
const query = this.query().select(getSelectColumns(hasTappableRewards))

if (!getAreaSql(query, perms.areaRestrictions, [])) {
return []
Expand All @@ -131,26 +152,39 @@ class Tappable extends Model {

/**
* Returns filter keys available for tappables
* @param {import('@rm/types').DbContext} ctx
* @returns {Promise<{ available: string[] }>}
*/
static async getAvailable() {
static async getAvailable({ hasTappableRewards }) {
if (!hasTappableRewards) {
const rows = await this.query()
.select('item_id')
.whereNull('pokemon_id')
.whereNotNull('item_id')
.groupBy('item_id')

return { available: rows.map((row) => `q${row.item_id}`) }
}

const rows = await this.query()
.select('item_id')
.count('id as total')
.select('reward_type', 'item_id', 'reward_pokemon_id')
.whereNull('pokemon_id')
.whereNotNull('item_id')
.groupBy('item_id')

const available = Array.from(
new Set(
rows
.map((row) => row.item_id)
.filter((itemId) => itemId !== null)
.map((itemId) => `q${itemId}`),
),
)
.whereIn(
'reward_type',
REWARD_FILTERS.flatMap(({ rewardTypes }) => rewardTypes),
)
.groupBy('reward_type', 'item_id', 'reward_pokemon_id')

const available = new Set()
rows.forEach((row) => {
const filter = REWARD_FILTERS.find(({ rewardTypes }) =>
rewardTypes.includes(row.reward_type),
)
const id = filter && row[filter.column]
if (id) available.add(`${filter.prefix}${id}`)
})

return { available }
return { available: [...available] }
}
}

Expand Down
5 changes: 5 additions & 0 deletions server/src/services/DbManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ class DbManager extends Logger {
const [hasShortcode] = await schema('route')
.columnInfo()
.then((columns) => ['shortcode' in columns])
const hasTappableRewards = await schema('tappable')
.columnInfo()
.then((columns) => 'reward_type' in columns)
.catch(() => false)

let hasPokemonShinyStats
try {
Expand Down Expand Up @@ -284,6 +288,7 @@ class DbManager extends Logger {
hasShortcode,
hasPokemonShinyStats,
hasPokemonBackground,
hasTappableRewards,
}
}

Expand Down
3 changes: 3 additions & 0 deletions server/test/clientFilterSelection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ const drawerCases = [
['nests', undefined, '19-0', '25-0'],
['stations', undefined, '19-0', '25-0'],
['tappables', undefined, 'q1', 'q2'],
['tappables', undefined, 'q1', 'c25'],
['tappables', undefined, 'c25', 'x25'],
['tappables', undefined, 'x25', 'm6'],
['gyms', undefined, 't1-0', 't2-0'],
['gyms', 'raids', 'e1', 'e2'],
['gyms', 'pokemon', '19-0', '25-0'],
Expand Down
Loading
Loading