Skip to content

feat(retroachievements): flag which ROM version RA actually supports - #4134

Open
sdornan wants to merge 6 commits into
rommapp:masterfrom
sdornan:claude/retro-achievements-green-badge-84d4be
Open

feat(retroachievements): flag which ROM version RA actually supports#4134
sdornan wants to merge 6 commits into
rommapp:masterfrom
sdornan:claude/retro-achievements-green-badge-84d4be

Conversation

@sdornan

@sdornan sdornan commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Sorry for opening so many PRs in such quick succession. Just wanting to fix and improve things as I import my library. I intend for this one to be the last one for the time being! Also, a future enhancement I am considering is showing the RA badge or a trophy on the game list. Let me know your thoughts on that!


Description
Explain the changes or enhancements you are proposing with this pull request.

RetroAchievements only unlocks achievements for the exact dump it has hashed, but nothing in RomM surfaced which of a game's versions that was. Picking between Star Fox 64 (USA) and Star Fox 64 (USA) (Rev 1) meant guessing, or opening each one in turn.

ra_id can't answer this. On the Hasheous path it comes from the matched game's metadata list (hasheous_handler.py), so it identifies the game, and every sibling of a title resolves to the same id. A per-version marker driven by it would light up on every row.

So this records the per-file fact instead:

  • RAHandler.hash_is_known asks RetroAchievements' own hash list (the ra_hashes_v2.json index built from get_game_list(..., include_hashes=True)), never another provider.
  • scan_handler now asks it for every ROM on an RA platform. Previously a Hasheous-supplied ra_id short-circuited the RA lookup entirely, which is how the per-file answer got lost.
  • The answer persists as roms.ra_hash_match (migration 0108), nullable so "never checked" stays distinct from "checked, and RA doesn't have it".

Two surfaces read it:

  • Version switcher shows the RA logo on the versions RA knows, so the choice is visible where it's made.
  • Metadata tab / Verification and the header badge now read the same field, falling back to Hasheous' ra_match only when RA was never asked. A definite no from RA outranks a Hasheous hit, since deferring there would promise achievements for a file RA has never seen. _filter_by_verified encodes the same precedence so the library filter can't drift from the badges.

Also included: the platform hash list is now memoised per platform with a short TTL. It was previously re-read and re-parsed from disk once per ROM during a scan, and it runs to several megabytes on large systems.

Note that ra_hash_match starts as NULL, so existing libraries show nothing new until those ROMs are rescanned or metadata-refreshed.

AI assistance disclosure

This PR was written with AI assistance (Claude Code). The investigation, implementation, migration, and tests were produced in a session with an agent, and reviewed by me before opening. Per CONTRIBUTING.md, disclosing this in full: the agent wrote the code and tests; I directed the approach, caught that ra_id was game-level rather than per-file, and chose to persist the flag at scan time rather than compute it per request.

Checklist
Please check all that apply.

  • I've tested the changes locally
  • I've updated relevant comments
  • I've assigned reviewers for this PR
  • I've added unit tests that cover the changes

Verification run locally: vue-tsc clean, npm run build clean, 279 v2 vitest tests, 1902 backend tests across tests/handler and tests/endpoints, both i18n scripts, and ruff/black/prettier/eslint on the touched files. Not yet verified in a browser against a live library, so the screenshot below is a faithful mock rather than a capture (see below).

Screenshots (if applicable)

image-1785972754387

Composed mock, not a live capture: built from the real ra.png asset and the generated v2 theme tokens, with the CSS copied verbatim from RMenu, RMenuItem, and VersionSwitcher. The RA-marked row is the base USA dump, which is the version RetroAchievements actually hashed for Star Fox 64. The light pane also shows the mark alongside the existing default-version bookmark.

The mark is a bare 14px logo rather than the gallery's tiled provider chip: the menu panel is width: max-content with no cap, so anything wider grows the whole dropdown. At 14px it costs exactly what the bookmark already in that slot costs.

Copilot AI lite review requested due to automatic review settings August 5, 2026 23:30

Copilot AI left a comment

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.

🟡 Changes recommended

ra_hash_match=False is currently likely to be dropped during scan metadata application due to truthy-only copying, preventing the intended persisted "definite no" state from being stored.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Adds a per-ROM-version RetroAchievements support signal (ra_hash_match) so the UI and backend "verified" logic can accurately reflect whether RetroAchievements will unlock for a specific dump, not just for the game-level ra_id.

Changes:

  • Persist roms.ra_hash_match (nullable tri-state) and expose it through ROM and sibling ROM schemas.
  • Update backend verified filtering and frontend verification utilities to prefer RetroAchievements' own hash verdict over Hasheous when available.
  • Surface support in the v2 VersionSwitcher and add tests across frontend and backend.
File summaries
File Description
frontend/src/v2/utils/romVerification.ts Updates verification logic to incorporate ra_hash_match precedence over Hasheous ra_match.
frontend/src/v2/utils/romVerification.test.ts Adds coverage for RetroAchievements precedence and ra_hash_match behavior.
frontend/src/v2/components/GameDetails/VersionSwitcher.vue Shows a RetroAchievements mark per sibling version based on ra_hash_match.
frontend/src/v2/components/GameDetails/VersionSwitcher.test.ts New tests validating per-version RetroAchievements marking and tooltip binding.
frontend/src/v2/components/GameDetails/RelatedGameCard.vue Extends synthetic SimpleRom stub with ra_hash_match.
frontend/src/v2/components/GameDetails/MetadataTab.vue Drives verification chips via db.matches (including RetroAchievements precedence).
frontend/src/locales/en_US/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/en_GB/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/bg_BG/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/cs_CZ/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/de_DE/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/es_ES/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/fr_FR/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/hu_HU/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/it_IT/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/ja_JP/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/ko_KR/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/pl_PL/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/pt_BR/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/ro_RO/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/ru_RU/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/tr_TR/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/zh_CN/rom.json Adds rom.retroachievements-supported string.
frontend/src/locales/zh_TW/rom.json Adds rom.retroachievements-supported string.
frontend/src/generated/models/SimpleRomSchema.ts Adds generated ra_hash_match field to SimpleRom schema.
frontend/src/generated/models/SiblingRomSchema.ts Adds generated ra_hash_match field to sibling schema.
frontend/src/generated/models/DetailedRomSchema.ts Adds generated ra_hash_match field to detailed schema.
backend/models/rom.py Adds Rom.ra_hash_match column mapping and documentation.
backend/alembic/versions/0108_roms_ra_hash_match.py Migration creating roms.ra_hash_match nullable boolean column.
backend/handler/metadata/ra_handler.py Adds hash index memoization and hash_is_known helper for per-file RA support.
backend/handler/scan_handler.py Populates ra_hash_match into RA handler result during scans.
backend/handler/database/roms_handler.py Ensures ra_hash_match is loaded for siblings and updates verified filter precedence.
backend/endpoints/responses/rom.py Exposes ra_hash_match on ROM and sibling ROM response schemas.
backend/tests/handler/metadata/test_ra_handler.py Adds unit tests for hash_is_known and hash index caching behavior.
backend/tests/handler/database/test_roms_verified_filter.py Adds tests to lock verified-filter precedence between RA and Hasheous, including Postgres SQL branch.
backend/tests/endpoints/roms/test_rom.py Ensures endpoints include ra_hash_match for sibling ROMs.
Review details

Files not reviewed (3)

  • frontend/src/generated/models/DetailedRomSchema.ts: Generated file
  • frontend/src/generated/models/SiblingRomSchema.ts: Generated file
  • frontend/src/generated/models/SimpleRomSchema.ts: Generated file

Suppressed comments (1)

frontend/src/v2/utils/romVerification.ts:28

  • Repo guidance forbids em dashes in comments. Replace the em dash here with a comma or rewrite into two sentences.
  • Files reviewed: 33/36 changed files
  • Comments generated: 3
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread backend/handler/scan_handler.py
Comment thread frontend/src/v2/components/GameDetails/VersionSwitcher.vue Outdated
Comment thread frontend/src/v2/utils/romVerification.ts Outdated
@sdornan

sdornan commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Good catch, and it was worse than "likely dropped" - it made the feature's headline behaviour unreachable.

The metadata priority loop in scan_rom copies truthy values only:

for key, field_value in handler_data.items():
    if field_value:
        rom_attrs[key] = field_value

So ra_hash_match=True persisted, but False was dropped and the column stayed NULL. NULL means never-checked and falls back to Hasheous, which means the "RetroAchievements has never seen this dump" branch could never fire in production. The precedence logic in _filter_by_verified and romVerification.ts was correct; the data never arrived. My tests missed it because they set the column directly rather than going through the scan.

Fixed in bb77311 by applying the tri-state explicitly after that loop. It also can't be gated on ra_id the way the loop is (available_sources requires a truthy id field), since a ROM can carry the game's id from Hasheous while RA has never seen the file.

Added a scan_rom test parametrised over all three states. I confirmed it fails on the True and False cases with the fix disabled, so it's a real guard rather than decoration.

Also took the suppressed em-dash comment: fixed in the same commit. For what it's worth, that was the only em-dash in the added lines of this PR (checked with git diff | grep '^+' | grep '—') - the other hits in those files are pre-existing.

Copilot AI left a comment

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.

🟡 Changes recommended

A transient failure reading the RA hash index can currently cause scans to overwrite an existing ra_hash_match value with NULL, unintentionally erasing known support state.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Files not reviewed (3)

  • frontend/src/generated/models/DetailedRomSchema.ts: Generated file
  • frontend/src/generated/models/SiblingRomSchema.ts: Generated file
  • frontend/src/generated/models/SimpleRomSchema.ts: Generated file

Suppressed comments (1)

backend/handler/metadata/ra_handler.py:262

  • When the hash list can't be read, hash_is_known returns None, and the scan code persists that, which can unintentionally clear a previously known ra_hash_match value (turning a known True or False into NULL) due to a transient IO or parse error. Consider keeping the existing DB value in this error path so scans do not erase known RA support state on temporary failures.
                "Couldn't read the RetroAchievements hash list, "
                "leaving hash support unknown: %s",
                exc,
            )
            return None
  • Files reviewed: 35/38 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI left a comment

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.

🟢 Ready to approve

The tri-state RA support signal is implemented end-to-end (scan persistence, API exposure, filtering, and v2 UI) with targeted tests covering precedence and regression cases.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Files not reviewed (3)

  • frontend/src/generated/models/DetailedRomSchema.ts: Generated file
  • frontend/src/generated/models/SiblingRomSchema.ts: Generated file
  • frontend/src/generated/models/SimpleRomSchema.ts: Generated file
  • Files reviewed: 35/38 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

sdornan and others added 6 commits August 6, 2026 12:49
Achievements only unlock for the exact dump RetroAchievements hashed, but
nothing in RomM said which of a game's versions that was. `ra_id` cannot
answer it: on the Hasheous path it comes from the matched game's metadata
list, so every sibling of a title resolves to the same id.

Record the per-file fact instead. `hash_is_known` asks RetroAchievements'
own hash list, and the scan now asks it for every ROM on an RA platform
rather than skipping the lookup whenever Hasheous already supplied an id.
The result persists as `roms.ra_hash_match`, nullable so "never checked"
stays distinct from "checked, absent".

The version switcher marks the versions RA knows, and the Verification
section's RetroAchievements tag reads the same field, falling back to
Hasheous' `ra_match` only when RA was never asked. A definite no from RA
outranks a Hasheous hit, since deferring there would promise achievements
for a file RA has never seen.

Also memoise the hash list per platform. It was re-read and re-parsed
once per ROM, and it is multiple megabytes on large systems.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The metadata priority loop in `scan_rom` copies truthy values only, so
`ra_hash_match=False` was dropped and the column stayed NULL. NULL means
never-checked and falls back to Hasheous, which made the "RetroAchievements
has never seen this dump" branch unreachable in practice: the precedence
logic was right, but the data never arrived.

Apply the tri-state explicitly, after that loop. It also can't be gated on
`ra_id` the way the loop is, since a ROM can carry the game's id from
Hasheous while RA has never seen the file.

Covered by a scan_rom test over all three states, which fails on the
True and False cases without this fix.

Also drop an em-dash from a comment, per the repo's style rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The parent-attach pattern renders a `display: none` anchor span, not a
comment node. RTooltip's own comment said comment node, which is what the
VersionSwitcher comment repeated. The layout conclusion is unchanged;
only the mechanism was described wrong.

Also drop the em-dash from romVerification's opening comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same fact, that `ra_id` is the game and every version shares it, was
spelled out at six sites. It now lives once on the column it describes,
with a line at each use site.

Also fixes a stale "comment node" reference in the VersionSwitcher test
that the previous commit missed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… check

`hash_is_known` returns None when there is nothing to check against: no RA
hash computed for this pass (a scan that doesn't rehash, or
SKIP_HASH_CALCULATION), or a hash list that couldn't be read. Persisting
that turned a known True or False back into NULL, and NULL falls back to
Hasheous, so a single unlucky rescan discarded the answer.

Only write an answer that was actually reached. Leaving the key out of
`rom_attrs` is what preserves the stored value: `add_rom` merges a detached
`Rom(**rom_attrs)`, and merge skips attributes the instance never set.

The test asserts through that merge rather than on `scan_rom`'s return
value, which can't distinguish an absent key from an explicit None.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… require achievements

Two ways the mark could lie.

RetroAchievements adds hashes continuously, but the local hash list is only
refetched every REFRESH_RETROACHIEVEMENTS_CACHE_DAYS, which defaults to 30.
A miss against a month-old list was being recorded as a definite no, and
that no outranks Hasheous. Refetch before reporting one: at most a single
request per platform per scan, since the fresh list is memoised and written
to disk. A failed refetch leaves the answer unknown rather than negative.

The list also covers games RetroAchievements has no achievements for, so a
hash match alone promised unlocks that could never happen. Gate the mark on
the game having achievements, read off the metadata the details page already
loads. A per-sibling count would mean hydrating `ra_metadata` for every
sibling, which the sibling load_only lists exist to avoid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sdornan
sdornan force-pushed the claude/retro-achievements-green-badge-84d4be branch from 388ce16 to fa3935c Compare August 6, 2026 17:49
@gantoine gantoine added the on-hold Pending further research or blocked by another issue label Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

on-hold Pending further research or blocked by another issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants