feat: add local achievement tracking and toasts #2307

Open
Varonal wants to merge 4 commits from Varonal/codex/oot-achievements into main
Varonal commented 2026-11-11 14:11:12 +00:00 (Migrated from github.com)

Why

Hookshot needs a local progress page and visible feedback for gameplay achievements, with progress stored separately from game saves.

What this PR contains

  • A local tracker with 98 stable progress slots. Existing IDs remain stable for local progress files.
  • A scrollable menu page and a queue that shows a four-second toast for each reported unlock.
  • Original badge-like icons drawn from UI primitives: grayscale while locked, color when unlocked.
  • No downloaded badge images or third-party achievement names and descriptions. The rows use neutral placeholders for now.
  • Automatic unlock checks are disabled until each game event is verified, so loading an existing save awards nothing.

Remaining work

  • Write original achievement names and descriptions, or get maintainer confirmation before reusing community-authored text.
  • Verify each unlock signal in game, then implement item, boss, minigame, damage, timing, session, and full-run triggers with save-slot isolation.
  • Capture screenshots of the page and toast after in-game validation.

Validation

  • Windows Vulkan build succeeded with the Hookshot Builder toolchain: python -I nx2/build.py --standalone --jobs 2 (RX_VK_ONLY=1).
  • Updated tests/test_fps_overlay_order.py to cover a toast as the only visible overlay; tests and in-game validation have not been run in this update.
## Why Hookshot needs a local progress page and visible feedback for gameplay achievements, with progress stored separately from game saves. ## What this PR contains - A local tracker with 98 stable progress slots. Existing IDs remain stable for local progress files. - A scrollable menu page and a queue that shows a four-second toast for each reported unlock. - Original badge-like icons drawn from UI primitives: grayscale while locked, color when unlocked. - No downloaded badge images or third-party achievement names and descriptions. The rows use neutral placeholders for now. - Automatic unlock checks are disabled until each game event is verified, so loading an existing save awards nothing. ## Remaining work - Write original achievement names and descriptions, or get maintainer confirmation before reusing community-authored text. - Verify each unlock signal in game, then implement item, boss, minigame, damage, timing, session, and full-run triggers with save-slot isolation. - Capture screenshots of the page and toast after in-game validation. ## Validation - Windows Vulkan build succeeded with the Hookshot Builder toolchain: `python -I nx2/build.py --standalone --jobs 2` (`RX_VK_ONLY=1`). - Updated `tests/test_fps_overlay_order.py` to cover a toast as the only visible overlay; tests and in-game validation have not been run in this update.
Joey-Magnus (Migrated from github.com) requested changes 2026-11-11 20:08:02 +00:00
Joey-Magnus (Migrated from github.com) left a comment

Thanks for the work on this. The menu page and the toast queue are tidy. But several things have to change before it can merge. I covered the licensing, the build, the tests and the unlock triggers, and only skimmed the drawing code.

  • blocking assets/oot-achievement-badges/*.png (~100 files) and assets/oot-achievement-badges.json are RetroAchievements badge images, downloaded from media.retroachievements.org (nx2/achievement_icons.py download()). The ones I opened are renders of Nintendo's characters and items (Zelda, the Iron Boots). They come with no license, so the project can't put them under the GPL. CONTRIBUTING says nothing derived from the game goes into the repository, and that third-party parts need a compatible license and a PROVENANCE.md note. These have neither. The titles and descriptions are also RetroAchievements' set, written by their community, so that needs the same question answered. Please drop the badge PNGs and the download script. Original icons, or generic ones built from what the repo already carries, would avoid the issue. Then check with the maintainers whether the RA titles and texts can be used at all.

  • blocking Player builds would stop. nx2/build.py:904 runs achievement_icons.write() whenever it compiles the Vulkan back end. header() opens assets/oot-achievement-badges.json (nx2/achievement_icons.py:87) and raises FileNotFoundError for any missing PNG (line 81). The Windows builder's package copies only the tracked gpu, host, nx2, scripts and third_party folders, plus assets/pipeline_states/ (scripts/release_parts.py:197, state_list). So a player's build there has neither the JSON nor the PNGs and stops. logo_rgba handles the same situation by writing an empty header when its PNG is missing. Whatever replaces the badges needs either that fallback or to ship in the package.

  • blocking tests/test_fps_overlay_order.py fails at this head. It cuts menu_record out of vk_menu.cpp and compiles it with stubs, and the new achievement_toast_visible/draw_achievement_toast aren't declared there (vk_menu.cpp:3797). It passes at this PR's base. Per AGENTS.md the check should be updated in this PR so it covers the toast as another thing that shows (one ImGui frame when only the toast shows). Don't weaken it. CI hasn't run here yet (only the title check), so it hasn't caught this.

  • blocking Merge conflict with main in gpu/vk/vk_api.h. Main is at MKWVK_API_VERSION 17. This PR only adds an enum (HOS_OPT_ACHIEVEMENT_BASE) and doesn't change MkwVkHost or MkwVkRenderer, so the bump isn't needed: drop it when you rebase.

  • should fix Several automatic unlocks don't match their descriptions:

    • "Rise and Shine!!" (oot_achievements.h:21) unlocks on any Item_Bottle. Bottles come from several places.
    • "A Royal Autograph" (line 22) unlocks on Item_Chick, which you have before meeting Zelda.
    • "Hey, Listen me!" (id 5) is marked OOT_ACH_MANUAL, but host/hos_main.c:600 unlocks it whenever the current map is KokiriForest_Child. That includes loading any child save there, as the comment admits.
    • "Hero of Time" (line 585) unlocks from the adult flag, so it also fires when loading any adult save. That may be intended, but it's worth saying so.

    More generally, on first start with an existing save every item-based entry unlocks at once, and each toast lasts 4 s. Consider no toasts for the first pass after a save loads. Better still, unlock only the entries you've verified in the game: the description says in-game validation is still to be done, and the pouch names (Armor_Adult_HeavyBoot, Item_RoyalLetter, ...) are unconfirmed.

  • minor achievement_toast_id (vk_menu.cpp:1060) is a plain int. menu_tick writes it (line 3302) while the render thread reads it in draw_achievement_toast, which is a data race. Make it std::atomic<int>, or read it under achievement_toast_mu.

  • minor Screenshots of the page and the toast are needed (CONTRIBUTING, Commits and pull requests 5). The description should also open with why the PR is needed.

Thanks for the work on this. The menu page and the toast queue are tidy. But several things have to change before it can merge. I covered the licensing, the build, the tests and the unlock triggers, and only skimmed the drawing code. - **blocking** `assets/oot-achievement-badges/*.png` (~100 files) and `assets/oot-achievement-badges.json` are RetroAchievements badge images, downloaded from `media.retroachievements.org` (`nx2/achievement_icons.py` `download()`). The ones I opened are renders of Nintendo's characters and items (Zelda, the Iron Boots). They come with no license, so the project can't put them under the GPL. CONTRIBUTING says nothing derived from the game goes into the repository, and that third-party parts need a compatible license and a `PROVENANCE.md` note. These have neither. The titles and descriptions are also RetroAchievements' set, written by their community, so that needs the same question answered. Please drop the badge PNGs and the download script. Original icons, or generic ones built from what the repo already carries, would avoid the issue. Then check with the maintainers whether the RA titles and texts can be used at all. - **blocking** Player builds would stop. `nx2/build.py:904` runs `achievement_icons.write()` whenever it compiles the Vulkan back end. `header()` opens `assets/oot-achievement-badges.json` (`nx2/achievement_icons.py:87`) and raises `FileNotFoundError` for any missing PNG (line 81). The Windows builder's package copies only the tracked `gpu`, `host`, `nx2`, `scripts` and `third_party` folders, plus `assets/pipeline_states/` (`scripts/release_parts.py:197`, `state_list`). So a player's build there has neither the JSON nor the PNGs and stops. `logo_rgba` handles the same situation by writing an empty header when its PNG is missing. Whatever replaces the badges needs either that fallback or to ship in the package. - **blocking** `tests/test_fps_overlay_order.py` fails at this head. It cuts `menu_record` out of `vk_menu.cpp` and compiles it with stubs, and the new `achievement_toast_visible`/`draw_achievement_toast` aren't declared there (`vk_menu.cpp:3797`). It passes at this PR's base. Per AGENTS.md the check should be updated in this PR so it covers the toast as another thing that shows (one ImGui frame when only the toast shows). Don't weaken it. CI hasn't run here yet (only the title check), so it hasn't caught this. - **blocking** Merge conflict with main in `gpu/vk/vk_api.h`. Main is at `MKWVK_API_VERSION 17`. This PR only adds an enum (`HOS_OPT_ACHIEVEMENT_BASE`) and doesn't change `MkwVkHost` or `MkwVkRenderer`, so the bump isn't needed: drop it when you rebase. - **should fix** Several automatic unlocks don't match their descriptions: - "Rise and Shine!!" (`oot_achievements.h:21`) unlocks on any `Item_Bottle`. Bottles come from several places. - "A Royal Autograph" (line 22) unlocks on `Item_Chick`, which you have before meeting Zelda. - "Hey, Listen me!" (id 5) is marked `OOT_ACH_MANUAL`, but `host/hos_main.c:600` unlocks it whenever the current map is `KokiriForest_Child`. That includes loading any child save there, as the comment admits. - "Hero of Time" (line 585) unlocks from the adult flag, so it also fires when loading any adult save. That may be intended, but it's worth saying so. More generally, on first start with an existing save every item-based entry unlocks at once, and each toast lasts 4 s. Consider no toasts for the first pass after a save loads. Better still, unlock only the entries you've verified in the game: the description says in-game validation is still to be done, and the pouch names (`Armor_Adult_HeavyBoot`, `Item_RoyalLetter`, ...) are unconfirmed. - **minor** `achievement_toast_id` (`vk_menu.cpp:1060`) is a plain `int`. `menu_tick` writes it (line 3302) while the render thread reads it in `draw_achievement_toast`, which is a data race. Make it `std::atomic<int>`, or read it under `achievement_toast_mu`. - **minor** Screenshots of the page and the toast are needed (CONTRIBUTING, *Commits and pull requests* 5). The description should also open with why the PR is needed.
Joey-Magnus (Migrated from github.com) reviewed 2026-11-11 21:26:14 +00:00
Joey-Magnus (Migrated from github.com) left a comment

The earlier blockers are gone (no RetroAchievements assets or download, no API bump, atomic toast id), and test_fps_overlay_order covers the toast. What's left is 98 rows named "Local Achievement NN" with nothing that can unlock them, plus leftovers: four unused probe names in oot_cheat_items.h and an unused declaration at host/hos_main.c:593. Whether a page with no working achievements should ship ahead of the triggers is a maintainer call. Screenshots would help either way.

The earlier blockers are gone (no RetroAchievements assets or download, no API bump, atomic toast id), and `test_fps_overlay_order` covers the toast. What's left is 98 rows named "Local Achievement NN" with nothing that can unlock them, plus leftovers: four unused probe names in `oot_cheat_items.h` and an unused declaration at `host/hos_main.c:593`. Whether a page with no working achievements should ship ahead of the triggers is a maintainer call. Screenshots would help either way.
hckrman101 commented 2026-11-11 22:17:25 +00:00 (Migrated from github.com)

This now conflicts with main in gpu/vk/vk_menu.cpp, host/hos_main.c. Could you merge main into the branch (or rebase) when you get a chance? I'll take another look once it's up to date.

This now conflicts with main in `gpu/vk/vk_menu.cpp`, `host/hos_main.c`. Could you merge main into the branch (or rebase) when you get a chance? I'll take another look once it's up to date.
Owner

@Varonal commented on GitHub · 2026-11-12 07:00 UTC · original

Thanks both for the careful follow-up and for taking another look. I’ll continue the achievement work and game testing locally on my fork, remove catalog entries without verified triggers, and resolve the branch conflicts as part of that work. I’ll push a commit and update the PR once I have a working, tested result, then ask for another review. I appreciate the thorough feedback.

> [@Varonal](https://github.com/Varonal) commented on GitHub · 2026-11-12 07:00 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2307#issuecomment-6817067640) Thanks both for the careful follow-up and for taking another look. I’ll continue the achievement work and game testing locally on my fork, remove catalog entries without verified triggers, and resolve the branch conflicts as part of that work. I’ll push a commit and update the PR once I have a working, tested result, then ask for another review. I appreciate the thorough feedback. <!-- github-sync:c:6817067640 -->
Commenting is not possible because the repository is archived.
No description provided.