Files
NX2navi/runtime/sprint.c
chrg6 d64a9b689c 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>
2026-11-12 22:57:56 -05:00

201 lines
9.8 KiB
C

// Sprint button for Ocarina of Time 1.1.1 (the patch set sprint, recomp/patches-v111.txt). Link sprints while his
// input flags hold the dash input (bit 4 of the word at *(Link +0x50)). The set's two sites call the host functions
// below right after the game loads those flags, and they OR the bit in while the sprint button's sprint is on, so the
// game's own sprint runs: its speed, animation, camera and every rule that ends it.
//
// Settings (live): sprint_button picks the button (off, b the game's B button, the bottom face button of the Nintendo
// 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
// 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.
#include <stdatomic.h>
#include <stdint.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "layout.h"
#include "sprint.h"
#if __has_include("patches.h")
#include "patches.h"
#endif
enum { BTN_OFF, BTN_B, BTN_L3 };
static const char *const button_names[] = {"off", "b", "l3"};
static const uint64_t button_bits[] = {0, 1u << 1, 1u << 4}; // HidNpadButton bits
#define BUTTON_ZL (1u << 8)
#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
// 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.
enum { MOVE, KEEP, WAIT, STOP };
static const struct { int32_t id; uint8_t kind; } states[] = {
{1, MOVE}, // Walk (also running)
{13, MOVE}, // Jump
{32, MOVE}, // Land
{33, MOVE}, // Fall
{68, MOVE}, // Clamber
{253, KEEP}, // StepOn
{35, KEEP}, // Slide
{36, KEEP}, // SlideLand
{40, WAIT}, // Wait
};
static _Atomic uint64_t pressed; // the button's presses since Link's last update (sprint_pad -> sprint_link)
static _Atomic int held; // the button held at the last poll
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 _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) {
const char *v = cfg_value("NX2NAVI_SPRINT_BUTTON");
for (int i = 0; i < (int)(sizeof button_names / sizeof *button_names); i++)
if (v && !strcmp(v, button_names[i])) return i;
return BTN_OFF;
}
static int setting_hold(void) {
const char *v = cfg_value("NX2NAVI_SPRINT_MODE");
return v && !strcmp(v, "hold");
}
// 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
// main+0x4b49698), whose +0x18 must be Link's own +0x18.
static int is_link(uint64_t link) {
if (!link || ld64(link) != MOD_MAIN_BASE + LINK_VTABLE) return 0;
uint64_t slot = ld64(MOD_MAIN_BASE + 0x4c7e0d0), mgr = slot ? ld64(slot) : 0;
if (!mgr || ld64(mgr) != MOD_MAIN_BASE + 0x4bdfe68) return 1;
uint64_t ctl = ld64(mgr + 0x470);
if (!ctl || ld64(ctl) != MOD_MAIN_BASE + 0x4b49698) return 1;
return ld64(ctl + 0x18) == ld64(link + 0x18);
}
static int state_kind(uint64_t link) {
uint64_t p = ld64(link + 0x18);
if (p) p = ld64(p + 0x270);
if (p) p = ld64(p + 0x188);
if (p) p = ld64(p + 0x1c0);
if (!p) return STOP;
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_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"); }
int sprint_available(void) {
int a = atomic_load(&allowed);
if (a < 0) {
a = rt_patch_enabled(sprint_set());
atomic_store(&allowed, a);
}
return a;
}
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) { // 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;
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);
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.
int sprint_step(int kind, int button, int hold) {
if (button != last_button) last_button = button, atomic_store(&active, 0), atomic_store(&pressed, 0);
int press = (atomic_exchange(&pressed, 0) & button_bits[button]) != 0, on = atomic_load(&active);
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) && atomic_load(&latched) && kind != STOP;
else {
if (press) {
if (on) on = 0;
else if (kind != STOP && (button != BTN_B || kind == MOVE)) on = 1, moved = kind == MOVE, wait_frames = 0;
} else if (on) {
if (kind == STOP) on = 0;
else if (kind == WAIT) {
wait_frames += frame_step_frames();
if (wait_frames > (moved ? WAIT_STOP_FRAMES : WAIT_START_FRAMES)) on = 0;
} else {
wait_frames = 0;
moved |= kind == MOVE;
}
}
}
atomic_store(&active, on);
return on;
}
// main+0x4ae798, after ldr x10, [x10]: x10 holds Link's input flags for the sprint's start and keep tests.
static void sprint_link(Cpu *c) {
if (!is_link(c->x[19])) return;
atomic_store(&polls, 0);
if (sprint_step(state_kind(c->x[19]), setting_button(), setting_hold())) c->x[10] |= DASH_INPUT;
}
// main+0xc07f3c, after ldrb w8, [x8]: the same flags' low byte for the stance and camera update.
static void sprint_stance(Cpu *c) {
if (atomic_load(&active) && is_link(c->x[19])) c->x[8] |= DASH_INPUT;
}
#ifdef RT_PATCH_PAGE
__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 + 0xf30, sprint_stance);
}
#endif