Skip to content

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

Merged
hckrman101 merged 1 commit into
NotRexed:mainfrom
ArmstrongThomas:feat/debug-link-speed
Nov 11, 2026
Merged

hckrman101 merged 1 commit into
NotRexed:mainfrom
ArmstrongThomas:feat/debug-link-speed

Conversation

@ArmstrongThomas

@ArmstrongThomas ArmstrongThomas commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

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

A debug tool beside flight and heart protection: Link walks, runs and
swims faster while the player steers him. The forward speed the game sets
on his character controller is scaled at the places it is set
(nx2/oot_patches.py patch_cct_speed: his move f_809552f0 and the
controller's setter), so the physics step still moves him with its
collisions. Speeds over 15 m/s (a ledge climb's) and scene walks keep
their pace. Off with the debug tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread gpu/vk/vk_menu.cpp
constexpr int link_speeds[] = {100, 125, 150, 200, 300};
constexpr const char* link_speed_names[] = {"1x", "1.25x", "1.5x", "2x", "3x"};
constexpr int kLinkSpeeds = sizeof link_speeds / sizeof link_speeds[0];
std::atomic<int> link_speed{0}; // (the game side's report, OOT_CHEAT_AGE_ID: 0 child, 1 adult, -1 no save loaded)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread host/hos_main.c
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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hckrman101
hckrman101 merged commit a780cc0 into NotRexed:main Nov 11, 2026
9 of 11 checks passed
hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
## Changes

Two fixes to the Debug tab's flight, both from playing with it on.

The first: an event that takes Link (a map change, the age toggle's
reload, a cutscene) waits for him to stand, and the flight moves him to
its own position every tick, so he never stood. Flying into an area's
exit or toggling his age in the air left the screen black until the
flight was switched off. A teleport sent from the Debug tab while flying
could do the same, and a second request logged "teleport waits until the
current event ends". While the event manager has an event playing, the
flight now lets him go, so he lands and the event goes on. When it has
ended the flight takes him again from where he stands.

The second: a respawn after a fall, or a warp, keeps Link's actor, so
the flight put him straight back where it had him, over the same drop.
With the flight on at an owl cutscene he fell, respawned and fell again
until it was switched off. When he is more than 2 units from where the
flight last placed him (it moves him 4 a second), the game moved him,
and the flight starts again from there.

`tests/test_flight_yields_to_events.py` checks both pieces of the host
code.

## Testing

RTX 4070 Ti SUPER, driver 621.14, Windows 11.

Build: the Windows game builds with these two commits on fc375e9 plus
#2020 (the same `host/hos_main.c` as this branch apart from #2020's
conflict, resolved by keeping both functions).
`test_flight_yields_to_events`, `test_movement_speed` and
`test_input_background` pass on Linux.

Play, on that build: flying, teleported from the Debug tab to Sacred
Forest Meadow as adult. Link landed, the map loaded and the picture was
normal. On the build before, the same kind of map change while flying
gave a black screen until the flight was switched off, several times in
one session.

Not tested in play yet: the respawn case (the owl), and flying into an
exit or toggling age in the air on the fixed build.
hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
…of-the-game, 1406 pipelines (#2064)

More play added to my contributed list from #2029, mostly adult areas
and dungeons I had not reached before. The data, plus one test change.

The list file was re-made with the tool from #2040:
```
python -I nx2/pipeline_states.py add jarynclouatre-most-of-the-game <my lists> --build ... --settings ... --route ...
```
It keeps only pipelines no other contributed list has. Of 8,021
pipelines in my recordings, 1,406 are new, so my file goes from 7,251 to
8,657 pipelines and the merged list from 12,800 to 14,206. The `.md`
note lists the new areas: Graveyard, Hyrule Field, Kokiri Forest, Lost
Woods, Sacred Forest Meadow, Forest Temple, Death Mountain Trail and
Crater, Goron City, Fire Temple and its boss, Zora's River, Domain and
Fountain, and the Ice Cavern, all as adult.

Recorded on Windows 11, RTX 4070 Ti SUPER (driver 621.14), no mods,
2560x1440 with dynamic resolution, on main with #1961 (some runs also
with #2020 and #2050, which change no shaders). As before it holds only
hashes, formats and draw state, no game code.

**Checked:**
- `nx2/pipeline_states.py check` passes.
- `tests/test_pipeline_states.py`, `tests/test_ship_state_list.py` and
`tests/test_pipeline_state_sources.py` pass.
- Based on main at 6cecdd1; it merges cleanly with the tour list
(#2065).

`test_split_list_lost_nothing` pinned each migrated list's exact size,
so growing a list failed it. It now takes each list's first n pipelines,
since `add` keeps a list's own pipelines first, and still checks their
count, that they don't overlap, and their hash. The same file grown by
`add` passes, and main's form of the test fails on it.

I have not timed the first start's shader preparation with this list
yet.
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.

2 participants