Skip to content

feat(games): every game pill shows the game's app icon, or a monogram - #404

Merged
sivert-io merged 1 commit into
mainfrom
feat/game-pill-icons-linked-and-steam
Sep 25, 2026
Merged

sivert-io merged 1 commit into
mainfrom
feat/game-pill-icons-linked-and-steam

Conversation

@sivert-io

Copy link
Copy Markdown
Collaborator

What was wrong

On the test box, "Your games" drew IGDB box art for Rocket League, Valorant, League of Legends, Dota 2 and Trackmania.

  • Its packs were installed from the catalog at 1.0.1, before they had app icons. Only a pack with source bundled fell back to the icon the image ships, so these packs showed no icon, and the popular-list fallback never ran because the installed pack had already claimed the slug.
  • A game saved from IGDB was matched to a module or pack only by exact slug.
  • The pill used the IGDB cover whenever it had no icon.

What changes

Linking (gameCatalogService.linkBuiltin). A stored game is matched to its module or pack in this order:

  1. IGDB id
  2. slug
  3. a slug alias
  4. normalised name

Packs and module catalogue entries can now carry igdbId. The validator accepts it. The bundled index carries it too, so a pack installed without it still links. CS2 is 242408.

An alias or name match is refused when both sides know their IGDB id and the ids differ. For example, "DeadLock" (2021) is a different game from Valve's Deadlock.

Any pack falls back to the snapshot's icon, whoever installed it. This applies to the pill, /api/packs/:slug/app-icon and the catalog app-icon route. This alone fixes the test box, with no pack update needed.

Steam client icons for any game on Steam, with no key:

  • Steam app id: from IGDB's Steam external game (search now asks for external_games), or from Wikidata P1733 when IGDB isn't set up.
  • Icon hash: steamcmd.net common.clienticon.
  • Download: shared.fastly.steamstatic.com/community_assets/.... The old Cloudflare path is the fallback; it 404s for new games such as Battlefield 6.
  • Processing: the largest frame of the .ico is scaled down to 128 px and stored as PNG under DATA_DIR/game-icons/<sha256-16>.png. It's served from GET /api/games/icons/:file with max-age=31536000, immutable. Frames under 48 px are not used.
  • PNG, not WebP: the release image has no node_modules, so sharp can't come along. pngjs (pure JS, already in the lockfile) is bundled instead, and there is no pure-JS WebP encoder worth shipping.
  • Database: new games columns steam_app_id, icon_url, icon_source, icon_checked_at, added by the schema's automatic ADD COLUMN.
  • Scheduling: runs in the background after a search, after a player saves their games, at startup, and after IGDB credentials are saved. One pass at a time, 1 s between requests, 6 s timeouts, and a failed row is retried after 14 days. No request ever waits on it. It is off in CI (GAMES_ENRICH=off); the specs force it against a fake Steam.

The pill never draws a cover. With no icon, it shows the game's first letter on a neutral tile (GameMark neutral). Other GameMark uses (teams, tournaments) are unchanged.

Bundled packs synced from Auto-Tournament/packs#8: igdbId on 32 packs, and app icons for Battlefield 6, League of Legends, StarCraft II and Chess. Every popular title now has an icon with no keys set.

No SteamGridDB and no new setting: everything above works with zero keys.

Merge order

  1. This PR first, then deploy. Instances on older 3.0 betas refuse an unknown pack field, so they would refuse igdbId.
  2. Then feat(packs): IGDB ids on every pack, and app icons for the last four packs#8.

The CS2 igdbId reaches the installed CS2 module with its next release. Until then, CS2 still links by slug.

Checks

  • eslint .: 0 errors
  • Typecheck ratchet: api 21, client 52, unchanged
  • API docs regenerated (/api/games/icons/:file)
  • New tests/api/game-icons.spec.ts (16 tests):
    • link order and the IGDB-id veto
    • igdbId validation, and that the bundled index matches the pack files
    • Steam app id from IGDB and Wikidata, and the client icon hash
    • .ico decoding for PNG and BMP frames, never scaled up, small frames rejected
    • over HTTP: an IGDB result linked by id (CS2, and Dota 2 through the bundled index); an installed pack without an icon falling back; a Steam icon fetched, with the old-path fallback and content-addressed dedupe; no keys at all through Wikidata; every popular game with an icon; the icon route refusing anything else
  • app-icons.spec.ts updated: League of Legends now has an icon.
  • Locally against the bundled build (own port and Postgres), these all pass: game-icons, app-icons, games-catalog, game-catalog, game-packs, bundled-packs, pack-index, catalog-feed-fetch, catalog-update-all, manual-report-module, game-integrations.
  • Checked by hand with IGDB credentials removed. The "Your games" dialog shows:
    • pack icons for CS2, RL, Valorant, LoL, Dota 2, Trackmania, Chess, SC2 and BF6
    • a Steam icon fetched via Wikidata for Deep Rock Galactic: Survivor
    • a clean "D" monogram for Deep Rock Galactic, whose Steam icon is only 32 px

🤖 Generated with Claude Code

"Your games" drew IGDB box art for Rocket League, Valorant, League of
Legends, Dota 2 and Trackmania. Two causes: a pack installed from the
catalog before its game had an icon hid the icon the image ships for the
same game (only bundled installs fell back to the snapshot), and a game
saved from IGDB was only ever matched to a module or pack by exact slug.

Linking. A stored game is matched to the module or pack for the same game
by IGDB id, then slug, then a slug alias, then its normalised name. Packs
and module catalogue entries may name `igdbId`; the bundled index carries
it too, so a pack installed before its file had one still links. An alias
or name match is refused when both sides know different IGDB ids
("DeadLock" 2021 is not Valve's Deadlock). CS2 names 242408.

Any pack now falls back to the snapshot's icon for its game, whoever
installed it, on the pill and on /api/packs/:slug/app-icon and the catalog
route.

Steam client icons for any game on Steam, with no key: the Steam app id
comes from IGDB's Steam external game (search now asks for it) or from
Wikidata P1733; the icon from steamcmd.net's `clienticon` and
shared.fastly.steamstatic.com (the old Cloudflare path is the fallback; it
404s for new games such as Battlefield 6). The largest frame of the .ico
is scaled to 128 px in pure JS (pngjs, bundled; the release image has no
node_modules, so no sharp) and stored as PNG under DATA_DIR/game-icons,
named by its hash, served from /api/games/icons/:file with a year-long
immutable cache and recorded on the games row (steam_app_id, icon_url,
icon_source, icon_checked_at). It runs in the background after searches,
saves and startup: one pass at a time, 1 s between requests, 6 s timeouts,
failures retried after 14 days. A frame under 48 px is not used.

The pill never draws a cover: no icon means the game's first letter on a
neutral tile (`GameMark neutral`).

Bundled packs synced from Auto-Tournament/packs: igdbId on 32 packs, new
app icons for Battlefield 6, League of Legends, StarCraft II and Chess.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sivert-io
sivert-io merged commit 0768684 into main Sep 25, 2026
11 checks passed
@sivert-io
sivert-io deleted the feat/game-pill-icons-linked-and-steam branch September 25, 2026 10:31
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant