feat: virtual Amiibo scanning and the game's Amiibo rewards #1521

Open
nicholasgower wants to merge 7 commits from nicholasgower/feat/amiibo into main
nicholasgower commented 2026-11-10 06:06:18 +00:00 (Migrated from github.com)

The game's amiibo function asks Horizon for an NFC tag, but a PC has no reader, so every scan currently falls through to the generic stubs. This adds a virtual tag, so the game sees a real Amiibo ID and runs its own reward code.

What changed

  • host/nn_nfp.c answers nn::nfp and nn::nfp::sdk at the SDK boundary: device listing, detection, mount/unmount, tag/model/common/register info, application areas, events and amiibo settings.
  • host/nfp_amiibo.h holds the SDK layouts and the selectable figures: Link (Ocarina of Time, the default), Zelda, Young Link, Young Zelda, the two 2026 Ocarina of Time figures (Mask 101/102), and Mario for the unsupported path.
  • The Insert menu's Gameplay page has an Amiibo row, whose last entry, None, presents no virtual tag (so the game waits for a real reader, as it does today). HOS_AMIIBO=<profile or 0x...>, HOS_AMIIBO_ID and HOS_AMIIBO_RANDOM_UID=1 override it; amiibo= is the saved key.
  • The tag arrives a moment after StartDetection rather than inside it, as a figure being tapped does. Presenting it inside the call made the whole mount-and-read finish before the game had drawn its "touch the amiibo" prompt.
  • StopDetection puts the reader back to Initialized, so the game's next scan starts detection again instead of waiting on a state it never leaves.
  • GetApplicationArea/SetApplicationArea are answered under both SDK spellings. The game imports the unsigned long (Pm/Em) form, and the mangled name differs from unsigned long long (Py/Ey), so those calls were reaching the stub and no size was written.
  • tests/test_amiibo.py checks the SDK offsets, the ids, the numbering endianness, the UIDs and the effects.

Testing

  • python -I -B tests/test_amiibo.py passes, and python -I -B tests/run_all.py passes apart from two unrelated environment-only checks (Windows symlink privileges in nx2/test_nup_paths.py, and windows.h not on the test's include path in tests/test_precompile_compute_guard.py).
  • The Amiibo effect has not been confirmed by a human. It is tested only in this checkout, with scripted input against a local copy of the game.
  • What the game does now: pressing the amiibo button opens its "Use amiibo — Touch the amiibo to the NFC touchpoint" prompt, and the game asks for the reader, starts detection, sees the tag appear, mounts it, reads GetTagInfo (UID, NFC-A/TYPE2) and GetModelInfo (e.g. character 010000, series 09, numbering 034b for the Ocarina of Time Link figure), then stops detection. So the game receives the right tag and the right figure.
  • What it does not do yet: the prompt stays open and the game keeps re-scanning (detect, mount, read the model, stop, start again) instead of giving the reward. Pressing A or B closes the prompt and play continues normally, with no item, mask or "this amiibo does not work" message seen. The remaining gap is in the game's own amiibo screen, not in the tag data the emulator returns.
  • The 2026 figures' retail IDs are not public yet. Their profiles use the series' next numbering; a real figure's id can be presented exactly with HOS_AMIIBO=0x....

No game files or build output are included.

The game's amiibo function asks Horizon for an NFC tag, but a PC has no reader, so every scan currently falls through to the generic stubs. This adds a virtual tag, so the game sees a real Amiibo ID and runs its own reward code. ## What changed - `host/nn_nfp.c` answers `nn::nfp` and `nn::nfp::sdk` at the SDK boundary: device listing, detection, mount/unmount, tag/model/common/register info, application areas, events and amiibo settings. - `host/nfp_amiibo.h` holds the SDK layouts and the selectable figures: Link (Ocarina of Time, the default), Zelda, Young Link, Young Zelda, the two 2026 Ocarina of Time figures (Mask 101/102), and Mario for the unsupported path. - The Insert menu's Gameplay page has an **Amiibo** row, whose last entry, **None**, presents no virtual tag (so the game waits for a real reader, as it does today). `HOS_AMIIBO=<profile or 0x...>`, `HOS_AMIIBO_ID` and `HOS_AMIIBO_RANDOM_UID=1` override it; `amiibo=` is the saved key. - The tag arrives a moment after `StartDetection` rather than inside it, as a figure being tapped does. Presenting it inside the call made the whole mount-and-read finish before the game had drawn its "touch the amiibo" prompt. - `StopDetection` puts the reader back to `Initialized`, so the game's next scan starts detection again instead of waiting on a state it never leaves. - `GetApplicationArea`/`SetApplicationArea` are answered under both SDK spellings. The game imports the `unsigned long` (`Pm`/`Em`) form, and the mangled name differs from `unsigned long long` (`Py`/`Ey`), so those calls were reaching the stub and no size was written. - `tests/test_amiibo.py` checks the SDK offsets, the ids, the numbering endianness, the UIDs and the effects. ## Testing - `python -I -B tests/test_amiibo.py` passes, and `python -I -B tests/run_all.py` passes apart from two unrelated environment-only checks (Windows symlink privileges in `nx2/test_nup_paths.py`, and `windows.h` not on the test's include path in `tests/test_precompile_compute_guard.py`). - **The Amiibo effect has not been confirmed by a human.** It is tested only in this checkout, with scripted input against a local copy of the game. - What the game does now: pressing the amiibo button opens its "Use amiibo — Touch the amiibo to the NFC touchpoint" prompt, and the game asks for the reader, starts detection, sees the tag appear, mounts it, reads `GetTagInfo` (UID, NFC-A/TYPE2) and `GetModelInfo` (e.g. `character 010000, series 09, numbering 034b` for the Ocarina of Time Link figure), then stops detection. So the game receives the right tag and the right figure. - What it does not do yet: the prompt stays open and the game keeps re-scanning (detect, mount, read the model, stop, start again) instead of giving the reward. Pressing A or B closes the prompt and play continues normally, with no item, mask or "this amiibo does not work" message seen. The remaining gap is in the game's own amiibo screen, not in the tag data the emulator returns. - The 2026 figures' retail IDs are not public yet. Their profiles use the series' next numbering; a real figure's id can be presented exactly with `HOS_AMIIBO=0x...`. No game files or build output are included.
nicholasgower commented 2026-11-10 07:37:26 +00:00 (Migrated from github.com)

Debugging and testing is ongoing. Once I've confirmed this functionality works, I will rebase and declare it ready for review.

Debugging and testing is ongoing. Once I've confirmed this functionality works, I will rebase and declare it ready for review.
Joey-Magnus (Migrated from github.com) reviewed 2026-11-11 21:46:25 +00:00
Joey-Magnus (Migrated from github.com) left a comment

Merged onto current main (15646d1), this is clean, and tests/run_all.py passes there (377 passed, 0 failed). The CI failures come from the old base: nx2/test_draw_parameters.py's mesh_fallback fixture, and the Windows nx2/test_nup_out_of_space.py path check, which fail the same way on other PRs from that period. A few points for when it comes out of draft:

  • host/nn_nfp.c:150 reads HOS_AMIIBO_RANDOM_UID with atoi. Since #2042, switches use env_on from hookshot_env.h, so that =true and =on work.
  • The random UID comes from rand(), and nothing in host/ calls srand, so every launch produces the same sequence of UIDs. That works against the once-a-day rule this switch is for: the first scan after a restart repeats a UID the game has already seen. Seeding once from hos_now_ns() would fix it.
  • The default figure is Link (index 0), so every player gets a working amiibo with no reader as soon as this lands. That seems like a maintainer call; defaulting to None would keep today's behaviour.
Merged onto current main (15646d1), this is clean, and tests/run_all.py passes there (377 passed, 0 failed). The CI failures come from the old base: nx2/test_draw_parameters.py's mesh_fallback fixture, and the Windows nx2/test_nup_out_of_space.py path check, which fail the same way on other PRs from that period. A few points for when it comes out of draft: - host/nn_nfp.c:150 reads HOS_AMIIBO_RANDOM_UID with atoi. Since #2042, switches use env_on from hookshot_env.h, so that `=true` and `=on` work. - The random UID comes from rand(), and nothing in host/ calls srand, so every launch produces the same sequence of UIDs. That works against the once-a-day rule this switch is for: the first scan after a restart repeats a UID the game has already seen. Seeding once from hos_now_ns() would fix it. - The default figure is Link (index 0), so every player gets a working amiibo with no reader as soon as this lands. That seems like a maintainer call; defaulting to None would keep today's behaviour.
github-sync changed title from WIP: feat: virtual Amiibo scanning and the game's Amiibo rewards to feat: virtual Amiibo scanning and the game's Amiibo rewards 2026-11-12 19:01:14 +00:00
Commenting is not possible because the repository is archived.
No description provided.