Skip to content

Core: identify ROMs for Spruce's game menu in a Cheevos helper - #25

Merged
abalakh merged 1 commit into
spruceUI:masterfrom
abalakh:core_identify_game
Oct 18, 2026
Merged

abalakh merged 1 commit into
spruceUI:masterfrom
abalakh:core_identify_game

Conversation

@abalakh

@abalakh abalakh commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Spruce's View achievement progress (spruceUI/spruceOS#1885) identified unknown ROMs through an identify-rom command added to Spruce's bundled RAOfflineProxy. That ownership broke twice:

  • A Spruce OTA copied nightly-46 proxy files over a nightly-52 install. rom_browser.py imported a helper the newer network.py no longer had, so the proxy couldn't start or stop, and RetroArch stayed pointed at 127.0.0.1:8099.
  • Proxy updates replace its directory. The nightly-54 refresh removed the command, hiding the action.

Cheevos now owns identification, and the installed proxy is never modified.

What changed

Helper. App/Cheevos/identify-rom.sh --path ROM --work-dir DIR --json sources Spruce's appEnv.sh and runs python -m cheevos.platform.identify_rom. It's headless: no PyUI, SDL, screens, sync or proxy service code. It prints one JSON object:

{"status":"matched","game_id":1479}
{"status":"no_match"}
{"status":"error","reason":"offline"}

Error reasons: auth, offline, network, rate_limited, response, file, hash, dependency, unavailable. The exit status is 0 for a match or miss, 1 for an error. config.json advertises the helper with "gameProgress": 2.

Lookup order

  1. Saved paths. Saves/cheevos/cache/rom-identifications.json (versions 1 and 2, 256 entries and 256 KB at most, atomic writes, misses expire after 7 days). Answered without loading hashing, SQLite or network code.
  2. Hashing. Only the installed proxy's rom_hashing wrapper is used, through platform/proxy_hasher.py. A missing or broken proxy is a dependency error. The proxy's ZIP and 7z rules are repeated in core/identification/candidates.py. Single-ROM ZIPs stream to SD in 64 KB chunks and are removed afterwards, including on SIGTERM.
  3. Hash to game, candidate by candidate in the hasher's order:
    • The proxy's cache, read-only (mode=ro).
    • Then RA's r=gameid. RA answers it without credentials (RAWeb's GetGameIdFromHashAction isn't authenticated, and rcheevos sends none), so no token, password or Web API key is read.
    • Only {"Success":true,"GameID":0} is a miss; errors are never saved.
    • RA's virtual IDs for untested, incompatible or patch-required hashes decode to the game's own ID.

The helper writes nothing to the proxy: no database writes, and no __pycache__ in its package.

Compatibility check. scripts/check_proxy_compat.py (make proxy, make proxy-check [PROXY=…]) checks a Spruce bundle or an upstream checkout using the proxy's real modules:

  • the adapter's API and the proxy modules it imports;
  • the same candidates as the proxy's own rom_browser.hash_candidates_for_manual_cache, for 14 fixtures on a simulated native hasher;
  • the read-only reader against what the proxy's Storage writes;
  • the ELF exports of each bundled libraproxy_rchash.so, which is never loaded off the device.

It records the proxy's version and source commit. CI checks the pinned bundle (proxy-tested-commit, nightly 54). The weekly drift job checks Spruce's latest Development.

Also changed

  • default_user_agent moved to the transport, so the helper doesn't load the RA client.
  • Docs

Testing

make check passes (1,055 tests). The unit tests cover:

  • the saved-path file's formats and bounds;
  • the archive rules and streaming extraction;
  • RA response parsing, including malformed and refused answers;
  • import failures of the proxy's code, including the OTA ImportError;
  • the helper in a real subprocess: the modules it loads, untouched proxy files, and cleanup on SIGTERM.

The device test ran on a Miyoo Mini+ with SpruceOS 4.5.5 and an unmodified RAOfflineProxy 2.2.0-alpha1 nightly 54, with the companion Spruce change installed:

Case Result Time
Saved path (an existing version-2 file) matched 0.6–0.7 s (1.3 s on the first run)
New ROM from Spruce's menu (The Mask, SNES ZIP) matched by RA; Cheevos opens on it; B returns to the same game in Spruce 3.1 s
GBA ZIP / PS1 CHD / PS1 CUE (native ARM hashing) matched by RA / the proxy's cache / the proxy's cache 6.7 / 1.6 / 1.4 s
Unknown hash / PS1 .iso the proxy can't hash no_match / Spruce's "hash" message, no launch 2.7 / 1.3 s
No network (separate network namespace) unknown hash → offline, not saved; a hash in the proxy's cache still matches
B while checking; SIGTERM during ZIP extraction process stopped, scratch removed, nothing saved
Proxy reinstalled while stopped, then restarted helper intact; proxy starts; login2 through it succeeds
Broken proxy copy (the OTA ImportError) dependency; the real proxy keeps running

The helper's RSS stays at 15 MB or less. MemAvailable stayed at 13 MB or more with PyUI, the proxy and the helper resident.

Not covered on the device: Spruce's own path mapping. The proxy there had no games cached from its menu, so the mapping was empty. That path was checked on a desktop against this helper.

Notes

  • Companion SpruceOS PR: . Either merge order is safe. An older Spruce never calls the helper. With an older Cheevos (gameProgress 1), the new Spruce code hides the action.

Spruce's "View achievement progress" identified unknown ROMs through an
identify-rom command added to its bundled RAOfflineProxy. A Spruce OTA
then mixed proxy versions and the proxy failed to import, and proxy
updates replace its directory, erasing the command. Cheevos now owns the
helper and leaves the installed proxy unmodified.

- Ship identify-rom.sh: it sources Spruce's appEnv.sh and runs the
  headless cheevos.platform.identify_rom (no PyUI, SDL, screens, sync or
  proxy service), which prints one JSON result: matched, no_match or an
  error with a stable reason. config.json advertises it with
  "gameProgress": 2.
- Answer saved path associations (rom-identifications.json, versions 1
  and 2) without loading hashing, SQLite or network code.
- Hash with the installed proxy's rom_hashing wrapper only, through an
  adapter that turns a missing or incompatible proxy into a "dependency"
  error and writes no bytecode into the proxy's package. Repeat the
  proxy's ZIP and 7z rules; stream single-ROM ZIPs to SD in 64 KB chunks
  and remove them on SIGTERM.
- Look hashes up in the proxy's cache read-only, then with RA's
  credential-free r=gameid. Only an explicit zero is a miss; errors are
  never saved. Decode RA's virtual IDs for untested or incompatible
  hashes to the game's own ID.
- Add scripts/check_proxy_compat.py (make proxy, make proxy-check): it
  checks a proxy bundle or checkout with the proxy's real modules (the
  adapter's API and imports, parity with rom_browser's archive rules on
  a simulated hasher, the cache schema) and the bundled libraries' ELF
  exports. CI checks the pinned bundle (proxy-tested-commit); the weekly
  drift job checks Spruce's latest.
- Move default_user_agent to the transport, so the helper doesn't load
  the RA client.
- Document the helper, its storage, device timings and the proxy-bump
  check.

Tested on Miyoo Mini+ (SpruceOS 4.5.5, RAOfflineProxy 2.2.0-alpha1
nightly 54, unmodified).
@abalakh
abalakh merged commit c39ae60 into spruceUI:master Oct 18, 2026
4 checks passed
@abalakh
abalakh deleted the core_identify_game branch October 18, 2026 12:08
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