Skip to content

Add persistent covers and backgrounds for PS5 and Switch - #60

Merged
Portablelle merged 7 commits into
mainfrom
codex/persistent-game-artwork
Oct 21, 2026
Merged

Portablelle merged 7 commits into
mainfrom
codex/persistent-game-artwork

Conversation

@Portablelle

@Portablelle Portablelle commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Returning to a ranking or the Emulation page discarded decoded artwork and briefly displayed placeholders even when an image was already cached. PS5 and Switch now share consistently sized portraits and selected-game backgrounds, with decoded RAM retention and checksummed persistent caches.

  • Prefer SteamGridDB; use official Steam library assets and verified PlayStation Store products when artwork is missing. Preserve full release identity, numbered sequels and remake disambiguation.
  • Render 160×240 portraits and 640×360 backgrounds, preserving wide-art composition with a blurred-cover fallback. Retain 96 portraits / 12 backgrounds in RAM and up to 512 / 128 on disk, with separate identities for marked PS5 releases, Switch/emulation and unmarked search results.
  • Add regressions for immediate page returns without the worker, offline restoration, corrupt/interrupted caches, LRU eviction, renderer cropping, provider failures and release-name variants. Publish shared provider metadata atomically, refresh removed Steam asset URLs and reject parenthesized fan-game qualifiers. Handle leading [PS5] tags without cache collisions and resolve Switch artwork using receipt/base-game display names. Include a sanitized Prowlarr audit tool that checks fallback providers on cold caches, plus default identity mappings.

Validation: final local Cubic review passed with no findings after corrections. 41 resolver tests and all 80 repository contracts passed. Native tests, packaging tests, service search/emulation integration and PS5 builds passed on the dedicated VPS. Live auditing covered 571 release titles, including successful portrait responses for all 189 PS5 releases; NHL 27 and Dragon’s Dogma 2: Dark Arisen portraits/backgrounds also passed authenticated HTTPS checks. Those figures cover the sampled release corpus, not every future title.

Deployment: matching native/service executables and corresponding sources are refreshed in packages/; deploy the artwork resolver with the identity mappings and a server-only SteamGridDB key at /opt/botty-artwork/steamgriddb-api-key. Existing console caches and operator overrides are preserved. Earlier development builds and the resolver were installed in the tested setup; the final Cubic fixes have additional host/build validation and still require rollout. This PR does not deploy services or create a release tag. No credentials, console backups or diagnostic logs are included.

Host-rendered preview with simulated catalog data (not a PS5 capture):

Portraits and selected-game background

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T13:35:54.572349Z 8d6b0f8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

};
bool ppsa=word.size()>4&&lower(word[0])=='p'&&lower(word[1])=='p'&&lower(word[2])=='s'&&lower(word[3])=='a';
for(unsigned i=4;ppsa&&i<word.size();++i)ppsa=word[i]>='0'&&word[i]<='9';
if(equals("ps5")){if(!emulation)platform=1;break;}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 R1: a leading PS5 tag makes every title share one cache key

key() stops at the first ps5 word, so a title whose first word is the platform tag ends up with an empty normalized title. For example, [PS5] Astro Bot and [PS5] Gran Turismo 7 both skip [ and hit ps5, then break with length==0. Both produce the same key: an empty prefix, the FNV offset hash, out[104]=2 and out[105]=1. This contradicts the guarantee stated above that a missing title must never share an entry.

The resolver handles this title shape: clean_title() removes [...] and resolves Astro Bot. So the service returns the correct art for the first game, and the native app stores it under the shared key. After that, retainedCovers_.load() in Artwork::request() and cache.load() in the worker return that same image for every other [PS5] … release. This affects rankings, search, Activity, Library and the hero background. The image persists in /download0 across restarts, and the app never asks the service again.

On a single page with two such titles, both slots download, but the second store() overwrites the first. When you come back to the page, both slots show the same picture.

Suggested fix: when the ps5 marker is reached with length==0, skip that word and keep normalizing instead of breaking. Alternatively, fall back to the ID-based key whenever the normalized length is 0. Add an artwork_cache_test assertion that [PS5] Game A and [PS5] Game B produce different keys.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed in 16f2383.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Claude review: Code review: no findings

Reviewed commits 8d6b0f8..1c0940d · checks: Code review complete; host checks pending · host validation source: pending isolated Claude host checks run on 1c0940d

ID Severity Location Finding Status
R1 🟡 homebrew/botty-native/src/artwork_cache.hpp:39 A leading [PS5] tag normalized every title to an empty key, so those games shared one cached portrait/hero fixed in 16f2383
Files reviewed
  • Incremental: homebrew/botty-native/src/artwork_cache.hpp, tests/artwork_cache_test.cpp; homebrew/botty/src/emulation.hpp, tests/emulation-test.cpp, tests/search_integration.py; deployment/botty-artwork/audit.py, test_server.py
  • Checked the textual package records: packages/botty-native/manifest.json, packages/botty-native/source/manifest.json, packages/botty/manifest.json and packages/botty-release.json. Their hashes and sizes agree with each other. Binary payloads (eboot.bin, botty-manager.elf, source archives and parts) were not opened; the isolated package checks validate them.
  • The rest of the PR was reviewed in full at 8d6b0f8.

Bottom line: Wait for the combined CI verdict

To fix before merge: nothing.

Notes (no action needed):

  • ⚪ R1 fix: a leading ps5 word is now skipped, and an empty normalized title falls back to the ID key. New tests cover [PS5] A vs [PS5] B, a bare [PS5] and !!!.
  • ⚪ artworkTitle() now returns the projected display name. When an entry has no NACP and no receipt, that name is the file stem, so the resolver is now queried with file stems where it used to get a 404. Art therefore follows the name the UI shows. The DLC→base-game case from the Codex thread is now covered by search_integration.py.
  • ⚪ In audit.py, an exception from steam_candidates (for example a network error) marks the row error without trying the PlayStation fallback. This only affects the operator audit report.

Not verified: PS5 runtime acceptance, including /download0 cache writes and hero rendering cost. Live SteamGridDB, Steam and PlayStation Store behavior was not checked either. Host checks were still pending when this review was written.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d6b0f82f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread homebrew/botty/src/emulation.hpp Outdated
const auto found=std::find_if(display_.begin(),display_.end(),[&](const auto& entry){return entry.second.id==id||entry.second.artworkId==id;});
if(found==display_.end())return {};
try{containedExisting(root(),found->first);if(sourceIdentity(found->first)!=found->second.identity)return {};}catch(...){return {};}
return found->second.data.value("name",std::string{});

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 Resolve artwork using the projected emulation title

When an emulation entry has no readable Control NACP, this returns an empty title even when the inventory displays a valid game name. For example, project() assigns installed DLC its base game's name, and entry() can obtain a game name from a verified receipt, but neither name is copied into Cached::data. Consequently, /api/artwork returns 404 without contacting the resolver, leaving these entries without portraits or backgrounds on a cold native cache. Preserve the projected display name for artwork resolution rather than relying exclusively on the raw NACP metadata.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Ready to merge. Claude's review of 1c0940d found nothing to fix.

@Portablelle
Portablelle merged commit 713db46 into main Oct 21, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant