-
released this
2026-07-31 20:30:34 -05:00 | 22 commits to main since this releaseAdded
-
The champ-select board says which mode it is showing. A badge beside "In champ select" names the map — muted for an ordinary game, accented for Classic Rift. Two boards for two modes were pixel-identical while being backed by entirely different champions, so the only way to tell a Classic Rift board from a Summoner'''s Rift one was to recognize the Jade roster by its champion art.
The mode comes from the LCU gameflow session'''s
map.id, read once on entering champ select. That is the only signal available at that point: the Live Client Data API the in-game paths use does not exist until the game process launches, and by then there is no pick left to advise on.Keyed on
map.id === 453and never on a mode-name substring. Queues 2450 and 3280 report aKIWI_JADEmode ("ARAM: Mayhem Classic-ish") on map 12, so anincludes('''JADE''')test would call those Classic Rift; a test pins that case. A session that cannot be read, or reports no map, is simply not badged — it is never guessed at. -
game-mode, a retained IPC message carrying the roster, map id and map name, re-delivered onmarkReady()like the other retained state so a reload mid-champ-select repaints the badge.
Notes
- This does not yet restore tier badges or automatic enemy lane placement in Classic Rift. Both read the op.gg tier list, which is still fetched as
type=rankedand keyed by live champion ids, so every60xxxlookup misses. A per-role tier list is the one op.gg request that cannot derive its queue from a champion id, which is exactly why the session-level mode above had to exist first. Seedocs/classic-opgg-plan.md.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-07-31 08:54:22 -05:00 | 23 commits to main since this releaseAdded
-
Classic Rift champ select reads the published counters artifact instead of
scraping op.gg. 3.11.0 made the live path work for the mode; this puts the
nightly file in front of it, the same short-circuit ranked has had — a map
lookup instead of one page fetch per enemy champion against a draft timer.It is a second file, not a second key in the existing one. op.gg indexes
classic pages by base champion id, so id 24 exists in both queues meaning two
different kits, and one map holding both would answer a classic Jax with
ranked Jax's matchups: correctly shaped, plausibly ranged, confidently wrong,
and undetectable by anything downstream.countersArtifactRouterpicks the
file from the id block and has its own tests for exactly that reason.A missing classic file is an expected state rather than an outage — the
producer's classic pass is opt-in and the mode is limited-time — so the router
reports nothing and the request falls through to op.gg, behaving exactly as it
did before the file existed. It never substitutes the other queue's
artifact. -
rosterForChampionId, for the places a champion arrives as a bare number with
no alias or map beside it.
Changed
CountersArtifactAdapternow takes the champion id when asking for its
artifact, since which file answers depends on which queue the champion is
from.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-07-30 23:18:34 -05:00 | 24 commits to main since this releaseAdded
-
Classic Rift champ select gets real advice. v3.10.0 fixed the in-game
overlay; the board was still blank, because every op.gg request went out as
type=rankedwith ajade_*slug that 307-redirects. op.gg does publish the
mode — counters, runes and item builds — undertype=classic, so the adapters
now ask for it.The champion id decides, not a flag. A
60xxxsubject can only have come
from a Classic Rift champ select and a live id can only have come from
anything else, soopggSubjectderives the queue and the slug id from the id
itself. Nothing is threaded through the pipeline, and there is no state that
can disagree with the id it describes.Only the CHAMPION slug needs translating (
60028→evelynn): op.gg keys
classic items and runes in the77xxxx/775xxxblocks, which is what the
live client reports, so those ids pass straight through — including into the
item-set import, which needed no change at all.
Fixed
-
Every classic counter would have silently failed to apply. op.gg names
opponents by base display name, so "Jax" resolves to live id 24 — while a
Classic Rift board is holding 60024. The matchup and the board would never
have matched, with no error raised anywhere: the advice simply never fires.
Matchups are now re-keyed into the subject's namespace, and the fix is
idempotent so an already-classic id is never shifted twice.Caught end-to-end against a captured live page rather than by reading, which
is the only way this one surfaces — every layer looks correct in isolation.
Notes
Champ-select roster detection (
rosterForMapId) keys on the gameflow map id
453, never on the mode string. Queues 2450 and 3280 reportgameMode
KIWI_JADE— "ARAM: Mayhem Classic-ish" — on map 12, so any substring test
against the mode name routes those ARAM games to classic pages. The four real
Classic Rift queues are all on map 453, which also survives Riot adding a fifth.
The Live Client Data API cannot help here: port 2999 is not listening until the
game process starts, and champ select has picks to advise on before that.op.gg does not segment this mode by rank — the
tierparameter is accepted and
discarded, with every band and a deliberately invalid value returning identical
rows. Classic figures therefore describe all ranks, and nothing should caption
them with a tier.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-07-30 20:28:26 -05:00 | 27 commits to main since this releaseEverything here came out of one real Classic Rift game, watched live through the
Live Client Data API. The mode was already detected correctly (v3.9.2); what
this release fixes is everything downstream that quietly assumed live-roster
data — most of which reported a confident wrong number rather than staying quiet.Fixed
-
Every lane's gold difference read
+0for an entire game, and the team-gold
bar with it. Classic Rift namespaces items exactly the way it namespaces
champions:770000 + base id, so Doran's Ring is 771056. The committed cost
table was generated from patch 16.12.1 — three patches before the mode shipped
— so not one of the 69 items seen across a 34-minute game had a known cost.
Every inventory summed to 0, so every difference was0 − 0, and the overlay
stated "dead even" on all five lanes while the game was decidedly not.The root cause was not the stale table, which is merely how it surfaced.
getItemCostreturned0for both "this item is free" and "no idea what this
item is", anditemGoldsummed straight through that. Absence now means
unknown: the generator keeps zero-cost entries (>= 0, not> 0) so a
trinket is a real 0, anditemGoldreturns null if any held item cannot be
priced. A partial sum was the worst available answer — it reads as "this player
is behind" rather than "we don't know" — so it is all-or-nothing, and null
propagates to—on the lane rows, the gold/min metric, and the team-gold bar,
which now hides rather than drawing a 50/50 split beside a meaningless+0.Regenerating against 16.15.1 also picked up three live items whose prices had
drifted since 16.12.1 (Bastionbreaker 3200 → 3000, Protoplasm Harness
2500 → 2600, Yun Tal Wildarrows 3100 → 3000), which had been skewing normal
games too. -
The benchmark harvester ground a full game at 0/150 against a classic
champion id. Match-V5 reports live ids, so a60xxxsubject matches nothing
it will ever return.HarvestQueue.enqueuenow refuses those ids outright and
logsharvest.skipped_classic; the Riot key is shared with the nightly crawl,
so the wasted budget was not free.
Changed
-
Roster detection gained two stronger signals, and the reasoning behind
ignoringgameModewas wrong. The previous comment dismissed the field
because ordinary Summoner's Rift reports"CLASSIC"— true, but it does not
follow that the field is useless. Classic Rift reports"JADE", matching
theJade_champion aliases, which makes it a clean mode-level discriminator.
Detection now triesrawChampionName→gameMode→mapNumber→mapName,
strongest first, andmapNumber: 453is preferred over"Map453"because it
is the id rather than a rendering of it. All four were observed together in the
real game; the redundancy is for the payload shapes Riot ships next.CLASSIC
is still explicitly rejected, with a test pinning it.The
Map453spelling that v3.9.2 guessed at is now confirmed rather than
assumed.
Added
- The in-game tracker sits 120px higher in Classic Rift. Its HUD is the
taller 2009-era layout, so a bottom-anchored panel at the normal 12px margin
overlapped the scoreboard and champion frame. The detected roster now travels
on the live-metrics IPC message, and only the bottom edge moves — side margins,
top-anchored corners, and live games are all untouched.
Known gaps
- Champ select still resolves nothing in Classic — counters, tier badges,
defensive bans, runes and item builds all fall through to op.gg, which is
queried withtype=rankedand thejade_*slug. op.gg does publish the mode
undertype=classicwith the base slug, so this is fixable and is not a
limitation of the source; it is simply not in this release.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-07-29 18:17:03 -05:00 | 28 commits to main since this releaseFixed
-
Champion display names stopped being unique, and it silently cost 35% of the
roster its stats. Patch 16.15 shipped the Classic Rift mode (map 453, queue
4300) and with it 60 alternate champion entries in the client's
champion-summary.json— aliasJade_<Base>, id60000 + base id. Every one
carries its base champion's display name, so "Garen" now names both id 86 and
id 60086.ChampionNameResolverbuilt its name→id maps last-write-wins, and the asset is
listed id-ascending, so the classic id always landed last and won. All 60 shared
names resolved to a60xxxid — Ahri, Annie, Garen, Lux, Teemo, Wukong, Master
Yi, Miss Fortune, Nunu & Willump and 51 more. Nothing in the published artifacts
is keyed by those ids, so the in-game overlay, the scouting report, and both
op.gg adapters quietly lost benchmarks, damage splits, and counters for a third
of the roster, with no error surfaced.Champ select was never affected: it passes numeric
championIdstraight
through, and ids stayed unique throughout. Only name→id resolution was
ambiguous.
Changed
-
Name→id resolution now takes the roster it means, because breaking the tie
the obvious way would have been worse. The Live Client Data API identifies
participants by display name, so simply preferring the lowest id would have
fixed normal games and started answering classic ones with live-roster stats.
Jade Annie has 460 HP and 550 attack range against live Annie's 560 and 625, on
a reworked kit — that is a confident wrong number where an unresolved id
honestly renders "—", and showing nothing is the recoverable failure.So the lookups are split per roster and the in-game poller identifies the game
before asking.rawChampionNameis the primary signal, being per-participant
and exact;mapNameis the fallback.gameModeis deliberately ignored —
ordinary Summoner's Rift reports the literal string"CLASSIC", so it
identifies the wrong thing. Anything unidentified stayslive, which is both
the safe default and the overwhelmingly common case, and the verdict is logged
once per game (live.roster_detected) with the raw strings beside it so a real
classic game confirms themapNamespelling.op.gg adapters stay pinned to the live roster: op.gg publishes no classic pages,
so a classic subject 404s to "no data" until it does.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v3.9.1 — scouting report: team damage rollup only
StableAll checks were successfulCI / build-and-test (push) Successful in 2m18sreleased this
2026-07-28 22:39:20 -05:00 | 30 commits to main since this releaseChanged
-
The scouting report shows the team damage rollup only — the per-champion bar
is gone. 3.9.1 reverses 3.9.0 on this point. That release dropped the bars
from the champ-select board but deliberately kept them on the report, on the
reasoning that "one player is the point" there. In use that turned out to be
wrong: a champion's damage split is a property of its kit, so the bar reads the
same on the card as it does folded into the column rollup above it, and the
report is the surface where that costs most — it is read mid-game under time
pressure, and ten repetitions of a fixed number were pushing apart the rows
that genuinely differ per player (rank, mastery, on-champ form).Both surfaces now follow one rule: damage is a team question, answered once per
column.ChampionDamageBarandsplitTooltipare deleted rather than left unused —
neither surface renders per-champion splits now, and the coverage they had
(thin sample, previous-patch fallback, off-patch aggregate) is asserted on
profileTooltip, which is what the rollups actually use.No mid-game refinement is possible here for now: Riot's Live Client Data API
exposes no damage figures for any player (scorescarries only kills, deaths,
assists, creep score and ward score), so the rollup stays kit-derived until the
post-game match data lands.
Fixed
- A test that passed on the day it was written and failed every day after.
stats-provider-artifact.test.tshardcoded a fixturefetchedAtwhile leaving
the provider on the real clock. The provider persists the payload's own
fetchedAtrather than a write timestamp, so once that literal aged past
cacheTtlHoursthe cached entry stopped counting as fresh and a test asserting
a cache hit began asserting against a genuine cache miss. The clock is now
pinned to the fixture. Test-only — the provider's lookup order was never wrong.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-07-27 20:51:18 -05:00 | 31 commits to main since this releaseChanged
-
Rune pages now come from the nightly crawler instead of being scraped during
champ select. Recommended runes were the last thing this app fetched from
op.gg at the moment it could least afford to: lock-in gives you about thirty
seconds, the request queued behind stats and tier-list fetches on the shared
rate limiter, and the result was cached in memory only — so every restart
re-scraped every champion you had looked at.lol-companion-data now crawls those same pages nightly, off the same
tier-list index it already fetches for counters, and publishes
data/champion-runes.json. Lock-in reads it out of memory.op.gg stays wired as the fallback and nothing about the feature changes when
it is used. A miss for any reason — the artifact not fetched yet, a patch
mismatch, a champion op.gg does not list at that role — falls straight through
to the scraper, exactly as before. The artifact is refused off-patch
(servesOffPatch: false), because a recommended build is a claim about
this patch's meta and the crawler is a night behind on patch-drop morning.The published file stores board rows as bare perk ids against a shared perk
dictionary rather than repeating names and icon URLs on every option: measured
over the same data, 212 KB against 1.9 MB. Which rune each row picked is
derived positionally fromselectedPerkIds— deliberately not by testing
membership, because stat shard ids repeat across rows (5008 in rows 1 and 2,
5001 in rows 2 and 3) and a membership test would highlight two shards where
the page picked one.Perk hover descriptions are unaffected. They come from the running client's
perk catalog over the LCU, which a headless crawler has no access to, so they
are still attached on this side for both paths. -
The champ-select board no longer tells you what to build. Each team's
damage rollup used to end in an instruction — "Mostly AP → build MR". The same
rollup renders for both columns, and that advice is only ever right about one
of them: under YOUR team it read as "your team is AP-heavy, so build MR",
which is backwards. The headline now names the composition and stops there
("Mostly AP"), which is true of whichever column you are reading. The scouting
report's rollups carried the same line and lost it for the same reason. -
Enemy lane dropdowns state the lane outright. They used to be prefixed
with their provenance — "Guess · Jungle", "Auto · Support" — which spent the
widest label in the row on a qualifier, clipped under the dropdown arrow at
the longest lanes, and told the reader something the row's position already
told them. The dropdown now reads "Jungle". The lane it defaults to is also
dropped from the list below it, so the open menu no longer offers the same
lane twice. -
"Support" is now "Utility" on the board, matching the role name the ally
column has always shown. The off-role tier tag follows (SUP→UTIL). Other
surfaces are untouched: the settings panel and the rune/item import headings
still say Support, and imported item-set names (LoLC: Lux Support) keep
theirs so already-imported sets are not orphaned in the client.
Removed
- Per-champion damage bars on the champ-select board. Ten of them, one per
cell, restated in ten thin bars what the two team rollups above them already
say in words — and the team number is the one a player actually itemises
against. The rollups stay. The per-player bar is unchanged on the scouting
report, where a single player's profile is the point of the surface.
Fixed
- The two team columns line up row for row again. The ally column renders
its role as plain text and the enemy column as a dropdown, which is several
pixels taller, so the two sides drifted further apart with every row — by the
middle lane they were visibly offset, in a layout whose whole purpose is
reading TOP against TOP. Both sides now reserve the same height for that line,
and a cell with no tier badge (an unfilled lane, or a champion op.gg has no
data for) holds its badge row open instead of collapsing it, so the columns
stay aligned mid-draft as well.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
released this
2026-07-27 16:47:36 -05:00 | 33 commits to main since this releaseChanged
The overlay now compares against the nearest measured tier instead of nothing.
Every source of benchmark numbers is banded well inside the ladder: the nightly crawler publishes GOLD/PLATINUM/EMERALD, and the seed shipped in the app is GOLD-only. An exact-tier lookup therefore answered nothing at all for an Iron, Bronze, Silver, Diamond, Master, Grandmaster or Challenger player — the CS/min and gold/min comparison lines simply stayed blank until their own harvest slowly filled a bucket in, which is exactly the Riot budget this app exists to stop spending.
A miss now walks outward along the ladder and takes the closest tier any layer does have: Bronze and below compare against GOLD, Diamond and above against EMERALD. Every seeded champion has a comparison at every rank from the first launch.
The substitution is reported, not hidden — the overlay captions itself with the tier that actually answered ("vs GOLD avg"), never with the tier it asked for. Showing Gold numbers under a Bronze label would misreport what you are being measured against, which is worse than showing nothing.
Exact hits are untouched: your own tier is tried across all three layers before any neighbour is considered, so a real Bronze bucket from your harvest still beats the artifact's Gold one. Equidistant ties resolve downward. Nothing hardcodes the band, so if the crawler's tier range is ever widened, real buckets start winning on their own.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)
-
v3.7.0 — counters, tier lists and benchmarks from the nightly crawler
StableAll checks were successfulCI / build-and-test (push) Successful in 1m47sreleased this
2026-07-27 13:52:01 -05:00 | 34 commits to main since this releaseCounters, tier lists and the overlay's benchmark targets now come from the nightly crawler in lol-companion-data instead of being re-fetched from op.gg while you are in draft.
Counters and tier lists
The crawler already fetches every op.gg counters page and all five tier-list pages each night — it has to, since op.gg has no endpoint that enumerates which champions are played at which role. It now publishes them, and the app reads that one file.
A champ select used to cost one op.gg page fetch per enemy champion, queued through the shared rate limiter, plus five more per patch for the tier badges. Those are now map lookups against a file fetched once on connect.
The scraper is still there and still correct. Anything the artifact does not cover — a champion op.gg does not list at that role, a patch the crawler has not caught up to, an unreachable Forgejo — falls straight through to it, with no retry delay in between. On patch-drop morning the crawler is a night behind, and an off-patch tier list is a claim about the wrong meta rather than a slightly old one; the app recognises that and scrapes for the day instead.
Both sources score a matchup through the same module, so a defensive-ban suggestion cannot depend on which one answered. The artifact publishes only what op.gg reported — games, wins, win rate — and never a counter rating: that number is tuned against this app's threat threshold and belongs on this side.
Overlay benchmarks
The in-game CS/min and gold comparisons had two sources: your own harvest, which spends your Riot key competing with the scouting report for the same budget, and a
benchmarks.jsoncommitted to the repo that only moves when a release ships one. The published artifact is a third, covering 173 champions across Gold/Platinum/Emerald and refreshing nightly for free.Lookups now resolve per (champion, role, tier) rather than per file. A harvested
benchmarks.jsonused to replace the bundled seed outright, which was backwards — the harvest fills in one champion at a time as you play, so for weeks a returning player had fewer comparisons than a fresh install.
Install:
LoL-Companion-3.7.0-setup.exe(installer) orLoL-Companion-3.7.0-portable.exe(no install).Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
Source code (ZIP)
-
released this
2026-07-26 22:41:22 -05:00 | 35 commits to main since this releaseAdded
Champion damage-type splits on the board and the scouting report. Every drafted champion now carries a physical / magic / true bar, and each team carries a weighted rollup that names the call — "Mostly AD → build armor".
The numbers come from the nightly crawler in lol-companion-data, which has been publishing them for a while with nothing on this side reading them. The app now fetches the published aggregate at runtime, strict-parses it against a mirrored copy of the producer schema, and caches the last good copy under
userDataso it survives a failed fetch, an offline session, or a cold start. No Riot key and no League client are needed — the splits are ready before the client is even up.The rollup weights each champion by its role share of team damage, using the weights the artifact ships. A flat mean would count a support magic split as heavily as the ADC, and the resulting armor-vs-MR call would be wrong for exactly the players relying on it. Both teams are rolled up separately on both surfaces, and a rollup covering only part of a team says so rather than implying full coverage.
Per-champion splits are read at the lane the champion is shown in, so re-assigning an enemy lane on the board re-reads their split for that lane. A role bucket is only preferred over the champion overall average once it has enough games behind it, since the producer deliberately publishes buckets as thin as a single game; a thin split is marked with
*rather than hidden, and a champion the crawler has never seen shows no bar at all rather than a zeroed one.A patch mismatch is treated as expected, not as an error: the crawler needs a night of games on a new patch before it can publish for it, so on patch-drop day the previous patch aggregate is shown and labelled rather than blanked.
Notes
- Read-only against the League Client (LCU) — never writes to the client.
- The in-game overlays on-screen render has not yet been verified in a live match.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads