Debug tab: a Movement speed choice for Link (1x to 3x) #2020

Merged
ArmstrongThomas merged 1 commit from feat/debug-link-speed into main 2026-11-11 00:17:16 +00:00
ArmstrongThomas commented 2026-11-10 23:54:35 +00:00 (Migrated from github.com)

Requested by Tommy · project thread

Getting around while testing takes a long time at Link's own pace. This adds a Movement speed choice to the Debug tab's tools: 1x, 1.25x, 1.5x, 2x or 3x.

What changed

  • Debug tab (gpu/vk/vk_menu.cpp): a "Movement speed" row in the Player group (Left/Right choose, stops at 1x and 3x). Only the Debug tab changes; the two-column boxed layout stays as it was. With the debug tools off, Link is back to his own speed. The row's percent goes to the game side as option HOS_OPT_CHEAT_SPEED (36, gpu/vk/vk_api.h; an older renderer answers 0, which the host reads as 1x, so MKWVK_API_VERSION is unchanged).
  • The speed (nx2/oot_patches.py patch_cct_speed, host/hos_main.c hos_oot_cct_speed): the forward speed the game sets on a character controller goes through the host, which scales it for Link's controller only. It is hooked where the speed is set: f_809552f0, the move Link's walk, run and swim go through (at its two setter calls), plus the controller's setter f_80559df0 and f_80964bf0, which inlines it. The physics step then moves him by that speed, so walls and floors still stop him. Like the other debug-tool patches, it is applied only to the supported build's lift.
  • Limits: speeds above 15 m/s are left alone (a ledge climb sets about 55 m/s for a frame, and scaling it threw Link past the ledge at 3x). The speed is only scaled while the left stick is pushed (host/nn_hid.c hos_move_stick_held), so walks the game scripts (the walk into a map after a map change) keep their pace.
  • No-clip flight follows the row too: flight moves Link itself at 4 m/s, so that speed is now 4 m/s times the multiple.
  • I tried the controller's own speed-rate field (+476, OneShotPhysicsCctSetSpeedRate) first. The game sets it again before every step, so writing it had no effect.

Testing

In the game (Windows, RTX, headless scripted runs with a throwaway save; Link's position logged once a second while the stick was held forward):

Kokiri Forest, run 1x 2x 3x
speed set on Link's controller 4.5 m/s 9.0 m/s 13.5 m/s
distance covered per second 4.6 m 9.0 m 13.4 m
  • Flying (no-clip flight): 4.0 m/s at 1x, 12 m/s at 3x.
  • Arriving at Lake Hylia at 3x: the scripted arrival walk ended at the same spot as at 1x.
  • Hyrule Field at 3x: walking into the cliff no longer flings Link up and over it (it did before the 15 m/s limit was added).
  • python -I -B tests/run_all.py: the same 62 checks fail with and without this change (they need gcc/g++, which this PC doesn't have). The checks that skip for want of gcc/g++, run with clang instead: no new failures once tests/test_input_background.py was updated for the new stick flag in host/nn_hid.c. New: tests/test_movement_speed.py (the hooks, the host's limits, the menu's choices).

Screenshots

The Debug tab at 2560x1440, menu size Small, Medium and Large (at this resolution Medium and Large already used one column before this change):

Small Medium Large
Small Medium Large

🤖 Generated with Claude Code

<!-- claude-projects-attribution: {"github_login":"ArmstrongThomas"} --> _Requested by **Tommy** · [project thread](https://claude.ai/code/project/chan_015MzNddca7FpV1zgPobgL8v?thread=cmsg_015MzNddca7FpV1zgPobgL8vMNVMaVC6wu1uyLn9NWRKWo)_ Getting around while testing takes a long time at Link's own pace. This adds a **Movement speed** choice to the Debug tab's tools: 1x, 1.25x, 1.5x, 2x or 3x. ## What changed - **Debug tab** (`gpu/vk/vk_menu.cpp`): a "Movement speed" row in the Player group (Left/Right choose, stops at 1x and 3x). Only the Debug tab changes; the two-column boxed layout stays as it was. With the debug tools off, Link is back to his own speed. The row's percent goes to the game side as option `HOS_OPT_CHEAT_SPEED` (36, `gpu/vk/vk_api.h`; an older renderer answers 0, which the host reads as 1x, so `MKWVK_API_VERSION` is unchanged). - **The speed** (`nx2/oot_patches.py` `patch_cct_speed`, `host/hos_main.c` `hos_oot_cct_speed`): the forward speed the game sets on a character controller goes through the host, which scales it for Link's controller only. It is hooked where the speed is set: `f_809552f0`, the move Link's walk, run and swim go through (at its two setter calls), plus the controller's setter `f_80559df0` and `f_80964bf0`, which inlines it. The physics step then moves him by that speed, so walls and floors still stop him. Like the other debug-tool patches, it is applied only to the supported build's lift. - **Limits**: speeds above 15 m/s are left alone (a ledge climb sets about 55 m/s for a frame, and scaling it threw Link past the ledge at 3x). The speed is only scaled while the left stick is pushed (`host/nn_hid.c` `hos_move_stick_held`), so walks the game scripts (the walk into a map after a map change) keep their pace. - **No-clip flight** follows the row too: flight moves Link itself at 4 m/s, so that speed is now 4 m/s times the multiple. - I tried the controller's own speed-rate field (`+476`, `OneShotPhysicsCctSetSpeedRate`) first. The game sets it again before every step, so writing it had no effect. ## Testing In the game (Windows, RTX, headless scripted runs with a throwaway save; Link's position logged once a second while the stick was held forward): | Kokiri Forest, run | 1x | 2x | 3x | |---|---|---|---| | speed set on Link's controller | 4.5 m/s | 9.0 m/s | 13.5 m/s | | distance covered per second | 4.6 m | 9.0 m | 13.4 m | - Flying (no-clip flight): 4.0 m/s at 1x, 12 m/s at 3x. - Arriving at Lake Hylia at 3x: the scripted arrival walk ended at the same spot as at 1x. - Hyrule Field at 3x: walking into the cliff no longer flings Link up and over it (it did before the 15 m/s limit was added). - `python -I -B tests/run_all.py`: the same 62 checks fail with and without this change (they need gcc/g++, which this PC doesn't have). The checks that skip for want of gcc/g++, run with clang instead: no new failures once `tests/test_input_background.py` was updated for the new stick flag in `host/nn_hid.c`. New: `tests/test_movement_speed.py` (the hooks, the host's limits, the menu's choices). ## Screenshots The Debug tab at 2560x1440, menu size Small, Medium and Large (at this resolution Medium and Large already used one column before this change): | Small | Medium | Large | |---|---|---| | ![Small](https://raw.githubusercontent.com/ArmstrongThomas/Hookshot/pr-assets/debug-link-speed/pr-assets/movement-speed-small.jpg) | ![Medium](https://raw.githubusercontent.com/ArmstrongThomas/Hookshot/pr-assets/debug-link-speed/pr-assets/movement-speed-medium.jpg) | ![Large](https://raw.githubusercontent.com/ArmstrongThomas/Hookshot/pr-assets/debug-link-speed/pr-assets/movement-speed-large.jpg) | 🤖 Generated with [Claude Code](https://claude.com/claude-code)
hckrman101 (Migrated from github.com) approved these changes 2026-11-11 00:16:00 +00:00
hckrman101 (Migrated from github.com) left a comment

Nice, small and well contained. I went through the patch, the host side and the menu, and compared the hook places against a local lift.

What I checked:

  • Off means off. At 1x, or with the debug tools off (the menu sends 100), the host stores no controller, so the hook returns on its first compare and the speed passes through untouched. An older renderer answers 0 for the new option, which the host clamps to 100, so not bumping MKWVK_API_VERSION is right: only an enum value was added, and neither interface struct changed. The hooks go in at lift time, in the supported build's lift only (like the other debug-tool patches). Whether to scale is decided every time the speed is set, so changing the row takes effect right away with no relift. The refactor of the controller walk in oot_update_player_cheats behaves the same as before (the vtable has to match before the +0xe8 check, so using the constant is equivalent).
  • No double scaling. The two places in Link's move and the component helper each inline the setter only when the virtual setter is that same function. Otherwise they call through to another controller type, which can't be Link's. So a speed is scaled once.
  • Collisions. Only the forward speed the physics step sweeps with is scaled, never a position, so walls and floors still stop him. The 15 m/s limit on the unscaled speed caps a frame's step at about 0.75 m even at 3x, and his 3x run is about 0.23 m a frame. I don't see a way through walls from this. Vertical motion (jumps, falls) isn't touched.
  • Option id 36 is free on main (32 to 35 are the other cheats, 40 to 45 are HDR, Discord and camera centering, with 44 reserved) and in the open PRs (10, 44, and 48 in #1945). No collision.
  • Not saved, not in a preset. link_speed isn't in opts, so it isn't written to the ini, it's back to 1x at every launch, and the Quality preset can't touch it. Normal players never see it.
  • Tests. test_movement_speed.py is plain Python with no arguments, so run_all picks it up without a RUN_ALL_* declaration. The test_input_background.py change makes that check stricter, not weaker: the cut-out state now has to include the new flag, which the harness needs so the cut-out npad_states compiles. Nothing it asserts was dropped.

Things to look at:

  1. The stick gate is "stick pushed", not "the player has control". A walk the game scripts is still scaled whenever the stick happens to be held. The common case is walking through a loading zone and keeping the stick forward into the arrival walk. The Lake Hylia test reads as if the stick was let go. Could you try it holding forward through the transition? If it overshoots, it's probably fine for a debug tool, but worth a line in the row's help.
  2. Untested movement types. Swimming is intended. Pushing or pulling a block, ladders and vines, crawlspaces, the hookshot pull and Epona aren't in the testing notes. Epona is probably a different controller, so unaffected. A block push at 3x is the one I'd check, since Link could get ahead of the block.
  3. CI. The Conventional Commit title check fails: the title needs a type prefix, e.g. feat(debug): Movement speed choice for Link (1x to 3x). The Windows Python failures (test_log_batch.py, and test_bake_skeleton.py timing out) fail the same way on main, so they aren't from this PR.
  4. Nit, inline: a comment moved to the wrong line.

Fine to merge once the title is fixed. Point 1 is a question, not a blocker.

Nice, small and well contained. I went through the patch, the host side and the menu, and compared the hook places against a local lift. What I checked: - **Off means off.** At 1x, or with the debug tools off (the menu sends 100), the host stores no controller, so the hook returns on its first compare and the speed passes through untouched. An older renderer answers 0 for the new option, which the host clamps to 100, so not bumping `MKWVK_API_VERSION` is right: only an enum value was added, and neither interface struct changed. The hooks go in at lift time, in the supported build's lift only (like the other debug-tool patches). Whether to scale is decided every time the speed is set, so changing the row takes effect right away with no relift. The refactor of the controller walk in `oot_update_player_cheats` behaves the same as before (the vtable has to match before the `+0xe8` check, so using the constant is equivalent). - **No double scaling.** The two places in Link's move and the component helper each inline the setter only when the virtual setter is that same function. Otherwise they call through to another controller type, which can't be Link's. So a speed is scaled once. - **Collisions.** Only the forward speed the physics step sweeps with is scaled, never a position, so walls and floors still stop him. The 15 m/s limit on the unscaled speed caps a frame's step at about 0.75 m even at 3x, and his 3x run is about 0.23 m a frame. I don't see a way through walls from this. Vertical motion (jumps, falls) isn't touched. - **Option id 36** is free on main (32 to 35 are the other cheats, 40 to 45 are HDR, Discord and camera centering, with 44 reserved) and in the open PRs (10, 44, and 48 in #1945). No collision. - **Not saved, not in a preset.** `link_speed` isn't in `opts`, so it isn't written to the ini, it's back to 1x at every launch, and the Quality preset can't touch it. Normal players never see it. - **Tests.** `test_movement_speed.py` is plain Python with no arguments, so run_all picks it up without a `RUN_ALL_*` declaration. The `test_input_background.py` change makes that check stricter, not weaker: the cut-out state now has to include the new flag, which the harness needs so the cut-out `npad_states` compiles. Nothing it asserts was dropped. Things to look at: 1. **The stick gate is "stick pushed", not "the player has control".** A walk the game scripts is still scaled whenever the stick happens to be held. The common case is walking through a loading zone and keeping the stick forward into the arrival walk. The Lake Hylia test reads as if the stick was let go. Could you try it holding forward through the transition? If it overshoots, it's probably fine for a debug tool, but worth a line in the row's help. 2. **Untested movement types.** Swimming is intended. Pushing or pulling a block, ladders and vines, crawlspaces, the hookshot pull and Epona aren't in the testing notes. Epona is probably a different controller, so unaffected. A block push at 3x is the one I'd check, since Link could get ahead of the block. 3. **CI.** The *Conventional Commit title* check fails: the title needs a type prefix, e.g. `feat(debug): Movement speed choice for Link (1x to 3x)`. The Windows Python failures (`test_log_batch.py`, and `test_bake_skeleton.py` timing out) fail the same way on main, so they aren't from this PR. 4. Nit, inline: a comment moved to the wrong line. Fine to merge once the title is fixed. Point 1 is a question, not a blocker.
hckrman101 (Migrated from github.com) commented 2026-11-11 00:16:00 +00:00

Nit: this comment belongs to link_age two lines up (the age report from the game side), not to link_speed. It looks like the new block was inserted between the declaration and its comment. Move it back to link_age?

Nit: this comment belongs to `link_age` two lines up (the age report from the game side), not to `link_speed`. It looks like the new block was inserted between the declaration and its comment. Move it back to `link_age`?
@ -442,0 +452,4 @@
if (!controller || controller != __atomic_load_n(&oot_speed_controller, __ATOMIC_RELAXED)) return;
const int percent = __atomic_load_n(&oot_speed_percent, __ATOMIC_RELAXED);
int hos_move_stick_held(void); /* host/nn_hid.c */
if (percent <= 100 || percent > 400 || !hos_move_stick_held()) return;
hckrman101 (Migrated from github.com) commented 2026-11-11 00:16:00 +00:00

This gate is "the stick is pushed", so a walk the game scripts while the player holds the stick (e.g. holding forward through a loading zone into the arrival walk) is still scaled. Probably fine for a debug tool. Worth a quick try, and a word in the row's help if it overshoots.

This gate is "the stick is pushed", so a walk the game scripts while the player holds the stick (e.g. holding forward through a loading zone into the arrival walk) is still scaled. Probably fine for a debug tool. Worth a quick try, and a word in the row's help if it overshoots.
Commenting is not possible because the repository is archived.
No description provided.