Move the sprint button's stance host off the water ripples' patch page slot, and fix how presses and timings reach it.

The stance site called page +0xf50, which the 0.10.0 line gives the water ripples above 60 fps: the second
registration ends every 1.1.1 start with "registered twice", the sprint button off included. It calls +0xf30 now, and
the comments say which slots the other sets hold. The set's switch is NX2NAVI_SPRINT_BUTTON_PATCH, not a name another
open request uses for a set of its own.

The stand-still timings count 60 Hz frames (frame_step_frames), so a toggled sprint no longer ends on a short stop at
high frame rates. A press is decided at its start for its whole length: kept from the game and the sprint's when it
begins in gameplay (B: while Link moves), else the game's alone, so a held B is not handed to the game half way and a
B that closes the pause menu starts no sprint. A sprint switched off with the button setting does not come back by
itself, Link's state is read only where the memory is readable, and the row, nx2navi.ini.example and the controls
page say that B is the jump under the game's Swap Jump Controls and name the button without assuming the layout.
tests\sprint_check.py covers the press latch, the menu press, 240 fps and the switch-off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-12 22:57:56 -05:00
parent aab082ff51
commit d64a9b689c
8 changed files with 97 additions and 29 deletions

View File

@@ -151,7 +151,7 @@ The log says which it is when a gamepad is taken: `input: HD rumble available fo
## Sprint button
On 1.1.1, `NX2NAVI_SPRINT_BUTTON` (`sprint_button`, the *Sprint button* row on the Controls tab, live) gives a button that makes Link sprint without rolling first: `l3` (the left stick's click, which has no other use), `b` (the game's B button, the bottom face button of the Nintendo layout), or `off` (the default). `NX2NAVI_SPRINT_MODE` (`sprint_mode`, the *Sprint mode* row) is `toggle` (the default: a press starts the sprint, the next press ends it) or `hold` (Link sprints while the button is held). Link sprints while his input flags hold the dash input; the set `sprint` of `recomp/patches-v111.txt` calls `runtime/sprint.c` after Link's movement update and the stance update that follows load those flags, and the host functions add the dash input while the sprint is on, so the game's own sprint runs with its speed, animation and camera. A toggled sprint goes on through jumps, slides and landings and ends when Link's state is anything else (an attack, a roll, an item, swimming, climbing, a cutscene), when ZL is held, or after four frames standing still (ninety after a press made while standing, before Link has moved). During play (Link's update ran within the last eight controller polls) the button is kept from the game, so it does not also fire its own action; B is kept only while Link moves, so it puts the weapon away as usual otherwise. In the settings menu, a menu of the game or a cutscene every button keeps its role. The set runs only while a button is chosen, so with the sprint button off its sites cost nothing. `NX2NAVI_SPRINT_PATCH=0` (or a mod whose code patches the same sites) keeps the patch set off and hides the rows.
On 1.1.1, `NX2NAVI_SPRINT_BUTTON` (`sprint_button`, the *Sprint button* row on the Controls tab, live) gives a button that makes Link sprint without rolling first: `l3` (the left stick's click, which has no other use), `b` (the game's B button, the bottom face button of the Nintendo layout), or `off` (the default). `NX2NAVI_SPRINT_MODE` (`sprint_mode`, the *Sprint mode* row) is `toggle` (the default: a press starts the sprint, the next press ends it) or `hold` (Link sprints while the button is held). Link sprints while his input flags hold the dash input; the set `sprint` of `recomp/patches-v111.txt` calls `runtime/sprint.c` after Link's movement update and the stance update that follows load those flags, and the host functions add the dash input while the sprint is on, so the game's own sprint runs with its speed, animation and camera. A toggled sprint goes on through jumps, slides and landings and ends when Link's state is anything else (an attack, a roll, an item, swimming, climbing, a cutscene), when ZL is held, or after four 60 Hz frames standing still (ninety after a press made while standing, before Link has moved), at any frame rate. During play (Link's update ran within the last eight controller polls) the button is kept from the game, so it does not also fire its own action; a B press is kept only when it begins while Link moves, and then until it is released, so B puts the weapon away as usual otherwise and the game never gets half a press. In the settings menu, a menu of the game or a cutscene every button keeps its role, and a press made there starts no sprint (B closes the pause menu). With the game's Swap Jump Controls on, B is the jump, which a `b` sprint button keeps from the game while Link moves: use `l3` then. The set's host functions are on the patch page at +0xf40 and +0xf30. The set runs only while a button is chosen, so with the sprint button off its sites cost nothing. `NX2NAVI_SPRINT_BUTTON_PATCH=0` (or a mod whose code patches the same sites) keeps the patch set off and hides the rows.
## Nintendo Switch 2 controllers

View File

@@ -822,7 +822,7 @@
# rumble = 1
# Sprint button (game 1.1.1): Link sprints without rolling first. off (the default), l3 (the left stick's click, which
# has no other use) or b (the game's B button, the bottom one of the Nintendo layout: it sprints only while Link moves
# and puts the weapon away as usual otherwise). sprint_mode: toggle (press to sprint and again to stop; the sprint
# and puts the weapon away as usual otherwise; with the game's Swap Jump Controls B is the jump, so use l3). sprint_mode: toggle (press to sprint and again to stop; the sprint
# also ends when Link stops, attacks, rolls, targets, uses an item, swims or climbs) or hold. The Controls tab's
# Sprint rows set both; changes take effect at once.
# sprint_button = off

View File

@@ -244,9 +244,10 @@ site occlusion_off main 11b82f4 39400008 2a1f03e8 # its update's store
# main+0x4ae798 into x10 and tests bit 4 twice from it: at main+0x4ae7a0 to start the sprint (Link +0x2c bit 20, when
# his state is Walk) and at main+0x4afa90 to keep it. The update of the camera and stance that follows
# (main+0xc025d0) loads the flags' low byte at main+0xc07f3c into w8 and tests the same bit at main+0xc07f40. Each site
# keeps its load and calls the page's host function after it (+0xf40, +0xf50), which ORs bit 4 into x10 or w8 while
# keeps its load and calls the page's host function after it (+0xf40, +0xf30; +0xf50 up to +0xff0 are taken, see
# runtime/patches.c, mouse_direct.c and ov_zstd.c), which ORs bit 4 into x10 or w8 while
# the sprint button's sprint is on for Link (sprint_link, sprint_stance); x19 is Link at both. x30 is the function's
# own saved one at both sites: both functions call others before and after, and restore x30 from their frames.
set sprint NX2NAVI_SPRINT_PATCH 1
set sprint NX2NAVI_SPRINT_BUTTON_PATCH 1
site sprint main 4ae798 f940014a f940014a 9527c9ea # ldr x10, [x10] -> ldr x10, [x10]; bl page+0xf40
site sprint main c07f3c 39400108 39400108 950a6405 # ldrb w8, [x8] -> ldrb w8, [x8]; bl page+0xf50
site sprint main c07f3c 39400108 39400108 950a63fd # ldrb w8, [x8] -> ldrb w8, [x8]; bl page+0xf30

View File

@@ -55,7 +55,7 @@ static const char *const known[] = {
"HDR_HIGHLIGHTS", "HDR_HUE", "HDR_HUE_CLIP", "HDR_MATCH_SLOPE", "HDR_MIDGRAY", "HDR_SATURATION", "HDR_SHADOWS",
"HDR_TONEMAP", "HDR_TONE_SLOPE", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
"BIG_BUILD_FREE_MB", "BIG_BUILD_MAX_MB", "BUILD_FREE_MB", "BUILD_RESERVE_MB", "LIB_PREBUILD_BIG", "LIB_PREBUILD_BIG_BG", "COMPUTE_WARM_REUSE", "LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LIB_WARM", "LINE_WIDTH", "POOL_SHOWN_FIRST", "POOL_DATA_FIRST", "TEX_REUSE_NAMED", "TEX_OVERWRITTEN_USED", "PIPE_LINK_MS", "FRAME_HOLD_MS", "PACE_SMOOTH", "OQ_BATCH", "CLEAR_SMALL_CPU", "GATE_WAIT_MS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MEM_CENSUS", "MEM_NEED_MB", "MENU_LANG", "MENU_SOUND_VOLUME", "MENU_TEST",
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_CLICK", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MICROPHONE", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MONITOR", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOUSE_TRACE", "MOVIE_HOST", "MOVIE_PACE", "MOVIE_CLOCK", "TEST_HITCH", "SPRINT_BUTTON", "SPRINT_MODE", "SPRINT_PATCH",
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_CLICK", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MICROPHONE", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MONITOR", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOUSE_TRACE", "MOVIE_HOST", "MOVIE_PACE", "MOVIE_CLOCK", "TEST_HITCH", "SPRINT_BUTTON", "SPRINT_MODE", "SPRINT_BUTTON_PATCH",
"MOTION_GUARD_DEGREES", "MOTION_POLICY_TRACE", "SELECTIVE_MOTION", "SELECTIVE_MOTION_RAW", // motion_owner.c, vk_fg_pose.inc
"SKIN_MOTION", // vk_skin_motion.inc
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",

View File

@@ -7,7 +7,13 @@
// layout, or l3 the left stick's click, which has no other use); sprint_mode is toggle (a press starts the sprint, the
// next press or Link's state ends it) or hold (sprint while the button is held). During gameplay the button is kept
// from the game (sprint_pad, called by ov_input.c npad_states), so it does not also fire its own action. B is kept only
// while Link moves (Walk, Jump, Land, Fall, Clamber), so it puts the weapon away as usual otherwise.
// for a press that begins while Link moves (Walk, Jump, Land, Fall, Clamber), and then until it is released, so it
// puts the weapon away as usual otherwise and the game never sees half a press. Presses made in a menu, a cutscene or
// a load (no update of Link within GAMEPLAY_POLLS polls) start nothing: B closes the pause menu, and that press is the
// menu's. Under the game's Swap Jump Controls B is the jump, which a B sprint button keeps from the game while Link
// moves: L3 is the one to use then.
// Patch page slots: the set's sites call +0xf40 and +0xf30; +0xf50 to +0xff0 belong to other sets (runtime/patches.c,
// mouse_direct.c, ov_zstd.c). A slot registered twice ends the start (dispatch.c rt_register_host).
//
// Link's state: the id at [[[[Link +0x18] +0x270] +0x188] +0x1c0] +0x28 (+0x4c when that reads 0x11a, a state that
// wraps another), the same field main+0x4aed50 compares with 1 (Walk) before the game starts a sprint.
@@ -32,8 +38,11 @@ static const uint64_t button_bits[] = {0, 1u << 1, 1u << 4}; // HidNpadButton
#define DASH_INPUT 0x10u
#define LINK_VTABLE 0x4b4dfc0ull // main offset: Link's actor class
#define GAMEPLAY_POLLS 8 // the button is kept from the game while Link's update ran within this many polls
#define WAIT_STOP_FRAMES 4 // standing still this many frames ends a toggled sprint that has moved
#define WAIT_START_FRAMES 90 // ... or this many after a press while standing, which has not moved yet
// Standing still this long ends a toggled sprint that has moved, or this long after a press while standing, which has
// not moved yet. In 60 Hz frames: above 60 fps Link updates once a rendered frame, each step frame_step_frames() of
// them, so counting updates would end the sprint on any short stop at 240 fps (four updates are 17 ms there).
#define WAIT_STOP_FRAMES 4.0
#define WAIT_START_FRAMES 90.0
// What Link's state does to a sprint: MOVE keeps it (and is when the bottom button sprints), KEEP keeps it,
// WAIT ends it after a few frames, STOP ends it at once. A state not listed stops it too.
@@ -56,7 +65,9 @@ static _Atomic int zl_held; // ZL (targeting) held at the last poll
static _Atomic int polls = GAMEPLAY_POLLS; // polls since Link's last update (none yet: not gameplay)
static _Atomic int moving; // Link's state was a MOVE one at his last update
static _Atomic int active; // the sprint is on: the dash input goes in
static int moved, wait_frames; // sprint_link only: the sprint has moved since it started; frames stood still
static _Atomic int latched; // the press now held began in gameplay (and, for B, while Link moved): kept from the game
static int moved; // sprint_link only: the sprint has moved since it started
static double wait_frames; // sprint_link only: 60 Hz frames stood still
static int last_button = -1; // sprint_link only: the button setting at the last update
static int setting_button(void) {
@@ -71,7 +82,9 @@ static int setting_hold(void) {
return v && !strcmp(v, "hold");
}
static uint64_t ld64(uint64_t a) { return a ? *(const uint64_t *)(uintptr_t)a : 0; }
// Guest reads through Link's pointers: a stale pointer in the chain reads 0 instead of faulting.
static uint64_t ld64(uint64_t a) { return a && mem_readable(a, 8) ? *(const uint64_t *)(uintptr_t)a : 0; }
static int32_t ld32(uint64_t a) { return a && mem_readable(a, 4) ? *(const int32_t *)(uintptr_t)a : 0; }
// 1 when x19 is the Link the player controls. Link's class has one more instance the player does not control: the
// game's actor manager ([[main+0x4c7e0d0]], class main+0x4bdfe68) names the controlled one at +0x470 (class
@@ -91,15 +104,15 @@ static int state_kind(uint64_t link) {
if (p) p = ld64(p + 0x188);
if (p) p = ld64(p + 0x1c0);
if (!p) return STOP;
int32_t id = *(const int32_t *)(uintptr_t)(p + 0x28);
if (id == 0x11a) id = *(const int32_t *)(uintptr_t)(p + 0x4c);
int32_t id = ld32(p + 0x28);
if (id == 0x11a) id = ld32(p + 0x4c);
for (size_t i = 0; i < sizeof states / sizeof *states; i++)
if (states[i].id == id) return states[i].kind;
return STOP;
}
// The set runs only while a button is chosen, so with the sprint button off its sites cost nothing. Whether it may run
// at all is read once, after the start: NX2NAVI_SPRINT_PATCH=0 or a mod whose code patches the same sites (mods.c
// at all is read once, after the start: NX2NAVI_SPRINT_BUTTON_PATCH=0 or a mod whose code patches the same sites (mods.c
// mods_exefs_switch) leaves it off then, and switching it on later must not undo that.
static _Atomic int allowed = -1;
static int sprint_set(void) { return rt_patch_set("sprint"); }
@@ -116,18 +129,27 @@ void sprint_pad(uint64_t *buttons) {
if (!sprint_available()) return;
int b = setting_button(), set = sprint_set();
if (rt_patch_enabled(set) != (b != BTN_OFF)) rt_patch_switch(set, b != BTN_OFF);
if (b == BTN_OFF) return;
if (b == BTN_OFF) { // the sites stop calling in: nothing may stay on for when a button is chosen again
atomic_store(&active, 0), atomic_store(&pressed, 0), atomic_store(&latched, 0);
return;
}
static uint64_t last;
uint64_t bit = button_bits[b], now = *buttons;
if (now & ~last & bit) atomic_fetch_or(&pressed, bit);
int n = atomic_load(&polls), gameplay = n < GAMEPLAY_POLLS;
if (n <= GAMEPLAY_POLLS) atomic_store(&polls, n + 1);
// A press is decided at its start, for its whole length: kept from the game, and the sprint's, when it begins in
// gameplay (B: while Link moves); else the game's alone, and no sprint's.
if (now & ~last & bit) {
int mine = gameplay && (b != BTN_B || atomic_load(&moving));
atomic_store(&latched, mine);
if (mine) atomic_fetch_or(&pressed, bit);
} else if (!(now & bit)) {
atomic_store(&latched, 0);
}
last = now;
atomic_store(&held, (now & bit) != 0);
atomic_store(&zl_held, (now & BUTTON_ZL) != 0);
int n = atomic_load(&polls);
if (n <= GAMEPLAY_POLLS) atomic_store(&polls, n + 1);
if (n >= GAMEPLAY_POLLS) return; // a menu, a cutscene or a load: the button keeps its role
if (b == BTN_B && !atomic_load(&moving)) return;
*buttons &= ~bit;
if (atomic_load(&latched) && gameplay) *buttons &= ~bit;
}
// The sprint for this update of Link, from his state and the button's presses: 1 when it is on.
@@ -137,7 +159,7 @@ int sprint_step(int kind, int button, int hold) {
if (atomic_load(&zl_held)) kind = STOP;
atomic_store(&moving, kind == MOVE);
if (button == BTN_OFF) on = 0;
else if (hold) on = atomic_load(&held) && (button != BTN_B || kind == MOVE || on);
else if (hold) on = atomic_load(&held) && atomic_load(&latched) && kind != STOP;
else {
if (press) {
if (on) on = 0;
@@ -145,7 +167,8 @@ int sprint_step(int kind, int button, int hold) {
} else if (on) {
if (kind == STOP) on = 0;
else if (kind == WAIT) {
if (++wait_frames > (moved ? WAIT_STOP_FRAMES : WAIT_START_FRAMES)) on = 0;
wait_frames += frame_step_frames();
if (wait_frames > (moved ? WAIT_STOP_FRAMES : WAIT_START_FRAMES)) on = 0;
} else {
wait_frames = 0;
moved |= kind == MOVE;
@@ -172,6 +195,6 @@ static void sprint_stance(Cpu *c) {
__attribute__((constructor)) static void sprint_register(void) {
if (!RT_V111) return; // the set's sites are in 1.1.1's main only
rt_register_host(RT_PATCH_PAGE + 0xf40, sprint_link);
rt_register_host(RT_PATCH_PAGE + 0xf50, sprint_stance);
rt_register_host(RT_PATCH_PAGE + 0xf30, sprint_stance);
}
#endif

View File

@@ -7,6 +7,6 @@
void sprint_pad(uint64_t *buttons);
// One update of Link's sprint: kind is how his state treats it (sprint.c), button and hold the settings. 1 while on.
int sprint_step(int kind, int button, int hold);
// 1 when the game version has the patch set sprint and nothing keeps it off (NX2NAVI_SPRINT_PATCH=0, a mod patching
// 1 when the game version has the patch set sprint and nothing keeps it off (NX2NAVI_SPRINT_BUTTON_PATCH=0, a mod patching
// its sites): the Controls tab shows the sprint rows then. The set itself runs only while a button is chosen.
int sprint_available(void);

View File

@@ -329,8 +329,9 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_SPRINT_BUTTON] = "Sprint button",
[UI_MENU_SPRINT_BUTTON_DESC] = "A button that makes Link sprint without rolling first. L3, the left stick's "
"click, has no other use. The game's B button sprints only while Link moves and "
"puts the weapon away as usual otherwise.",
[UI_MENU_SPRINT_OFF] = "Off", [UI_MENU_SPRINT_B] = "Bottom button (Nintendo B)",
"puts the weapon away as usual otherwise; with the game's Swap Jump Controls on, B "
"is the jump, so use L3.",
[UI_MENU_SPRINT_OFF] = "Off", [UI_MENU_SPRINT_B] = "The game's B button",
[UI_MENU_SPRINT_L3] = "L3 (left stick)",
[UI_MENU_SPRINT_MODE] = "Sprint mode",
[UI_MENU_SPRINT_MODE_DESC] = "Toggle: press once to sprint and again to stop. The sprint goes on through jumps "

View File

@@ -25,10 +25,14 @@ int rt_patch_enabled(int set) { return set == 3 && set_on; }
void rt_patch_switch(int set, int on) { assert(set == 3); set_on = on != 0; }
void rt_register_host(uint64_t addr, GuestFn fn) {
if (addr == RT_PATCH_PAGE + 0xf40) link_fn = fn;
else if (addr == RT_PATCH_PAGE + 0xf50) stance_fn = fn;
else if (addr == RT_PATCH_PAGE + 0xf30) stance_fn = fn;
else abort();
}
static float step = 1.0f; // frame_step_frames: the 60 Hz frames of one update of Link
float frame_step_frames(void) { return step; }
int mem_readable(uint64_t a, uint64_t n) { (void)n; return a != 0; }
// Link: vtable, +0x18 -> owner whose +0x270 -> a with +0x188 -> b with +0x1c0 -> the state, id at +0x28
static uint64_t link[8], owner[0x50], a[0x40], b[0x40], state_obj[0x20], other[8];
static uint64_t manager[0x90], controlled[8];
@@ -72,7 +76,7 @@ int main(int argc, char **argv) {
test_main_base = (uintptr_t)img;
assert(link_fn && stance_fn);
if (argc > 1 && !strcmp(argv[1], "kept-off")) {
// off at the start (NX2NAVI_SPRINT_PATCH=0, or a mod patching its sites): no rows, never switched on, and the
// off at the start (NX2NAVI_SPRINT_BUTTON_PATCH=0, or a mod patching its sites): no rows, never switched on, and the
// button always reaches the game
set_on = 0, button = "l3";
assert(!sprint_available());
@@ -202,6 +206,45 @@ int main(int argc, char **argv) {
assert(frame(0, WALK, link) == 0 && !dashing());
frame(0, WAIT_ID, link);
assert(frame(BB, WAIT_ID, link) == BB && !dashing());
// a press begun while standing stays the game's for its whole length, also once Link moves: no sprint, no strip
assert(frame(BB, WALK, link) == BB && !dashing());
assert(frame(BB, WALK, link) == BB && !dashing());
frame(0, WALK, link);
// a press begun while moving stays the sprint's when Link stops: the game gets no B from it
assert(frame(BB, WALK, link) == 0 && dashing());
assert(frame(BB, WAIT_ID, link) == 0);
frame(0, WAIT_ID, link);
// a press made in a menu (no update of Link for more than 8 polls) starts nothing when play goes on
reset("l3", "toggle");
frame(0, WALK, link);
{
uint64_t bt = 0;
for (int i = 0; i < 10; i++) bt = 0, sprint_pad(&bt);
bt = L3, sprint_pad(&bt);
assert(bt == L3); // the menu's
bt = 0, sprint_pad(&bt);
}
frame(0, WALK, link);
assert(!dashing());
// the stand-still timings count 60 Hz frames: at 240 fps (a quarter frame an update) a stop of four updates goes on
reset("l3", "toggle");
step = 0.25f;
frame(L3, WALK, link), frame(0, WALK, link);
for (int i = 0; i < 4; i++) frame(0, WAIT_ID, link);
assert(dashing());
for (int i = 0; i < 16; i++) frame(0, WAIT_ID, link);
assert(!dashing());
step = 1.0f;
// switched off with the sprint on, and on again: it does not come back by itself
reset("l3", "toggle");
frame(L3, WALK, link), frame(0, WALK, link);
assert(dashing());
button = "off", frame(0, WALK, link);
button = "l3", frame(0, WALK, link);
assert(!dashing());
puts("sprint_check: ok");
free(img);