diff --git a/svc/api/spec.ts b/svc/api/spec.ts index 4aa0c301a..eb5ad3c83 100644 --- a/svc/api/spec.ts +++ b/svc/api/spec.ts @@ -1430,6 +1430,17 @@ You can use the API without a key, but registering for a key allows increased ra type: "integer", }, }, + { + name: "role", + in: "query", + description: + "Benchmark against cores or supports only, estimated from farm within each team. Can be combined with bracket. Omit for all roles", + required: false, + schema: { + type: "string", + enum: ["core", "support"], + }, + }, ], responses: { 200: { @@ -1457,7 +1468,20 @@ You can use the API without a key, but registering for a key allows increased ra .json({ error: "bracket must be an integer between 1 and 8" }); } } - const result = await getHeroBenchmarks(req.query.hero_id, bracket); + let role: string | null = null; + if (req.query.role !== undefined) { + role = String(req.query.role); + if (role !== "core" && role !== "support") { + return res + .status(400) + .json({ error: "role must be core or support" }); + } + } + const result = await getHeroBenchmarks( + req.query.hero_id, + bracket, + role, + ); return res.json(result); }, }, diff --git a/svc/counts.ts b/svc/counts.ts index d6c6d6269..cbfc4ce44 100644 --- a/svc/counts.ts +++ b/svc/counts.ts @@ -213,6 +213,23 @@ runReliableQueue("counts", 2, async (job: CountsJob, metadata) => { const turbo = isTurbo(match); // 1 to 8 based on the average rank of the match, as in updateHeroCounts const rank = !turbo && avg ? Math.floor(avg / 10) : null; + // Rank each team by gold_per_min and call the top 3 cores, the rest + // supports. The exact 1-5 position needs parsed data, but this split is + // right for 99.4% of players using api data alone (see #2980). + const roles = new Map(); + if (!turbo) { + [true, false].forEach((radiant) => { + const team = match.players.filter((p) => isRadiant(p) === radiant); + if (team.length === 5) { + team + .slice() + .sort((a, b) => (b.gold_per_min ?? 0) - (a.gold_per_min ?? 0)) + .forEach((p, i) => + roles.set(p.player_slot, i < 3 ? "core" : "support"), + ); + } + }); + } if ( match.match_id % 100 < Number(config.BENCHMARKS_SAMPLE_PERCENT) && (isSignificant(match) || turbo) @@ -244,10 +261,23 @@ runReliableQueue("counts", 2, async (job: CountsJob, metadata) => { 2, ); redis.expireat(rkey, expiretime); + // Filtered sets keep both slots so every key has the same + // shape, with an empty segment for the filter not in use. + const role = roles.get(p.player_slot); + if (role) { + const roleKey = `${rkey}::${role}`; + redis.zadd(roleKey, metric, match.match_id); + redis.expireat(roleKey, expiretime); + } if (rank) { - const rankKey = `${rkey}:${rank}`; + const rankKey = `${rkey}:${rank}:`; redis.zadd(rankKey, metric, match.match_id); redis.expireat(rankKey, expiretime); + if (role) { + const rankRoleKey = `${rkey}:${rank}:${role}`; + redis.zadd(rankRoleKey, metric, match.match_id); + redis.expireat(rankRoleKey, expiretime); + } } } }); diff --git a/svc/util/queries.ts b/svc/util/queries.ts index 8da05a341..c337df06a 100644 --- a/svc/util/queries.ts +++ b/svc/util/queries.ts @@ -108,11 +108,22 @@ export async function getHeroItemPopularity(heroId: string) { late_game_items: lateGameItems, }; } -export async function getHeroBenchmarks(heroId: string, bracket?: number | null) { +export async function getHeroBenchmarks( + heroId: string, + bracket?: number | null, + role?: string | null, +) { const ret: AnyDict = {}; const arr = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99]; - // Bracketed sets add the 1-8 rank bracket after the global/turbo slot - const suffix = bracket ? `:${bracket}` : ""; + // Key slots are benchmarks:::::: + // Filtered sets always carry both the bracket and role slots, empty for the + // ones not in use, and are only written for non-turbo matches: + // ...:::5: ...::::core ...:::5:core + // The unfiltered key keeps no suffix at all so existing ones still resolve. + let suffix = ""; + if (bracket || role) { + suffix = `:${bracket ?? ""}:${role ?? ""}`; + } const items: [metric: string, percentile: number][] = []; Object.keys(benchmarks).forEach((metric) => { arr.forEach((percentile) => {