diff --git a/docs/controls.md b/docs/controls.md index 1f62ac68..a5537942 100644 --- a/docs/controls.md +++ b/docs/controls.md @@ -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 diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 68d02509..a8602c23 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -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 diff --git a/recomp/patches-v111.txt b/recomp/patches-v111.txt index 3b95679e..b4eee4ca 100644 --- a/recomp/patches-v111.txt +++ b/recomp/patches-v111.txt @@ -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 diff --git a/runtime/cfg.c b/runtime/cfg.c index c56c396a..21d101db 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -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", diff --git a/runtime/sprint.c b/runtime/sprint.c index d86f4c16..02364a6b 100644 --- a/runtime/sprint.c +++ b/runtime/sprint.c @@ -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 diff --git a/runtime/sprint.h b/runtime/sprint.h index 9cdb04ca..f719d2e2 100644 --- a/runtime/sprint.h +++ b/runtime/sprint.h @@ -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); diff --git a/runtime/ui_text.c b/runtime/ui_text.c index d72037a0..0374dc5e 100644 --- a/runtime/ui_text.c +++ b/runtime/ui_text.c @@ -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 " diff --git a/tests/sprint_check.c b/tests/sprint_check.c index 2502eb78..a3d3bea1 100644 --- a/tests/sprint_check.c +++ b/tests/sprint_check.c @@ -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);