mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Add a sprint button: a setting that makes Link sprint without rolling first, as a toggle or while held.
This commit is contained in:
@@ -482,6 +482,12 @@ not queued; the silence before the first prefill and across a stop is not owed,
|
||||
up. The render thread's own timing (`runtime/audren.c` `render_thread`) is not built by it. No game files and no audio
|
||||
device.
|
||||
|
||||
A change to the sprint button (`runtime/sprint.c`, the set `sprint` of `recomp/patches-v111.txt`, its call in
|
||||
`runtime/ov_input.c` `npad_states`) passes `python tests\sprint_check.py`, which builds `sprint.c` with the rest of the
|
||||
runtime stubbed and plays frames against a stand-in Link: toggle and hold on each kind of button, the states that keep
|
||||
and end a sprint, ZL, the button kept from the game only during play (the bottom button only while Link moves) and a
|
||||
Link the player does not control. No game files.
|
||||
|
||||
A change to the mouse camera (`runtime/mouse_direct.c`, the mouse path of `runtime/input.c`, the set `mouse_direct` of
|
||||
`recomp/patches-v111.txt` or `patches-v143.txt`) passes `python tests\mouse_direct_check.py --cc <clang> --deps
|
||||
<ucrt64>\include`, which builds `mouse_direct.c`, and `input.c` with `--deps`, with the rest of the runtime stubbed and
|
||||
|
||||
@@ -149,6 +149,10 @@ The game makes its vibration for linear actuators: per side a low and a high ban
|
||||
|
||||
The log says which it is when a gamepad is taken: `input: HD rumble available for this gamepad: a Pro Controller, through SDL (rumble_hd = 1)`, or `not available` with the reason; the row on the Controls tab is greyed out for a gamepad that cannot take it. With `rumble_hd = 0` nothing is written to the controller and the line says that its driver is asked once the setting is on. The first report a gamepad gets is logged with its bytes (`input: HD rumble: first report written (left ..., right ...)`); a report the controller or its driver refuses is logged too, and the gamepad then rumbles through its motors again. Frequencies are clamped to what the actuators take (41 to 626 Hz in the low band, 82 to 1253 Hz in the high band) and amplitudes to 0..1, the actuators' safe maximum. `rumble_strength` scales the amplitude (its default, 130, is the game's own: the default is above 100 for the motors' sake, and this path divides by it), `rumble_curve` does not apply, and `rumble = 0` silences this path too. A report is written at most every 15 ms (`rumble_hd_ms`, 5 to 50: raise it if a Bluetooth controller stutters or disconnects with HD rumble on), with the newest value only; a value that holds is written again every 30 ms, and the neutral value goes out when the game stops sending for 250 ms, when another gamepad takes over, when the setting is turned off and when the game exits. While HD rumble is on the game makes no dual-motor rumble call for that gamepad. A DualShock 3 on the direct reader keeps its motors either way.
|
||||
|
||||
## 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: `b` (the bottom face button), `l`, `r`, `l3` or `r3`, 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; the bottom button is kept only while Link moves, so it attacks as usual otherwise. In the settings menu, a menu of the game or a cutscene every button keeps its role. `NX2NAVI_SPRINT_PATCH=0` turns the patch set off and hides the rows.
|
||||
|
||||
## Nintendo Switch 2 controllers
|
||||
|
||||
Windows does not present the Switch 2 Pro Controller as a gamepad SDL can open (the log says `input: no gamepad`), so it needs a bridge program that wakes it and creates a virtual gamepad, for example one that emulates a DualSense: that gives buttons, rumble, gyro and accelerometer. The log then shows `input: gamepad "DualSense Wireless Controller" ... (rumble, gyro and accelerometer, ...)` and `input: motion sensors on`. Bridges that output a DualShock 4 whose gyro and accelerometer never change give buttons and rumble but no motion. Run one bridge at a time; Steam Input can also claim the controller, so close Steam if the bridge does not find it. With several virtual and real gamepads connected the game follows whichever one is pressed.
|
||||
|
||||
@@ -820,6 +820,13 @@
|
||||
# Gamepad motion controls (0 = off) and vibration (0 = off).
|
||||
# motion = 1
|
||||
# rumble = 1
|
||||
# Sprint button (game 1.1.1): Link sprints without rolling first. off (the default), b (the bottom face button: it
|
||||
# sprints only while Link moves and attacks as usual otherwise), l, r, l3 or r3. During play the button only sprints:
|
||||
# L or R no longer uses its item, R3 no longer resets the camera. 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
|
||||
# sprint_mode = toggle
|
||||
# Rumble strength in percent (0 to 300; 0 = off) and the exponent on the amplitude (0.2 to 1.5; lower = weak pulses
|
||||
# stronger, 1 = linear). rumble_triggers = 1 also drives the impulse triggers of an Xbox pad.
|
||||
# rumble_strength = 130
|
||||
|
||||
@@ -238,3 +238,15 @@ site occlusion_off main 11b82a4 39400008 2a1f03e8 # its update's store
|
||||
site occlusion_off main 11acec8 39400008 2a1f03e8 # the samples-passed query's first write
|
||||
site occlusion_off main 11b82dc 39400008 2a1f03e8 # its update's compare
|
||||
site occlusion_off main 11b82f4 39400008 2a1f03e8 # its update's store
|
||||
|
||||
# Sprint button (runtime/sprint.c). Link sprints while his input flags (the 64-bit word at *(Link +0x50)) have bit 4,
|
||||
# the dash input. Link's movement update (main+0x4aaa80) loads that word at
|
||||
# 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
|
||||
# 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
|
||||
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
|
||||
|
||||
@@ -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",
|
||||
"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",
|
||||
"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",
|
||||
@@ -125,7 +125,7 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
|
||||
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"}, {"AUDIO_DEVICE", "audio"}, {"MICROPHONE", "audio"}, {"MENU_SOUND_VOLUME", "audio"},
|
||||
{"AUDIO_RAMP", "audio"}, {"AUDIO_CATCHUP", "audio"},
|
||||
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"}, {"RUMBLE_HD", "input"}, {"DS3", "input"},
|
||||
{"MOTION", "input"}, {"SPRINT_BUTTON", "input"}, {"SPRINT_MODE", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"}, {"RUMBLE_HD", "input"}, {"DS3", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
|
||||
{"STICK_SENS", "input"}, {"DEBUG_MENU", "input"},
|
||||
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
|
||||
@@ -243,6 +243,8 @@ static const CfgMeta metas[] = {
|
||||
{"MICROPHONE", "1", "0|1", RESTART}, // ov_audio_input.c mic_on: 0 refuses the game's input streams
|
||||
{"AUDIO_RAMP", "1", "0|1", RESTART}, // audio_mixer.c accumulate_ramp: a changed volume is reached across the frame
|
||||
{"AUDIO_CATCHUP", "1", "0|1", RESTART}, // audren.c render_thread: frames owed after a stall are rendered and dropped
|
||||
// sprint.c: the sprint button (b the bottom face button) and toggle or hold; the Controls tab's Sprint rows (1.1.1)
|
||||
{"SPRINT_BUTTON", "off", "off|b|l|r|l3|r3", LIVE}, {"SPRINT_MODE", "toggle", "toggle|hold", LIVE},
|
||||
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
|
||||
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "nintendo", "auto|nintendo|xbox|playstation", LIVE},
|
||||
{"DS3", "0", "0|1", LIVE}, // input_ds3_win.c ds3_read: read a DualShock 3 directly; off: no HID scan
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "input.h"
|
||||
#include "ov.h"
|
||||
#include "platform/platform.h"
|
||||
#include "sprint.h"
|
||||
|
||||
typedef struct { int64_t sampling; uint64_t buttons; int32_t lx, ly, rx, ry; uint32_t attributes, pad; } NpadState;
|
||||
typedef struct { int64_t sampling; int32_t x, y, dx, dy, wx, wy; uint32_t buttons, attributes; } MouseState;
|
||||
@@ -101,6 +102,7 @@ static void npad_states(Cpu *c, uint32_t want) {
|
||||
}
|
||||
dbg_tick(c); // the debug menu's changes to the game, between two of its frames (debug_tools.c)
|
||||
if (live) dbg_pad(&s.buttons, &s.lx, &s.ly, &s.rx, &s.ry); // its free camera takes the sticks while it flies
|
||||
if (live) sprint_pad(&s.buttons); // the sprint button's presses, kept from the game during gameplay (sprint.c)
|
||||
*out = s;
|
||||
ret_x(c, 1);
|
||||
}
|
||||
|
||||
163
runtime/sprint.c
Normal file
163
runtime/sprint.c
Normal file
@@ -0,0 +1,163 @@
|
||||
// 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 bottom face button, l, r, l3, r3); 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: L or R its item, R3 the camera reset. The bottom button is kept only while Link moves (Walk, Jump,
|
||||
// Land, Fall, Clamber), so it attacks as usual otherwise.
|
||||
//
|
||||
// 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_L, BTN_R, BTN_L3, BTN_R3 };
|
||||
static const char *const button_names[] = {"off", "b", "l", "r", "l3", "r3"};
|
||||
static const uint64_t button_bits[] = {0, 1u << 1, 1u << 6, 1u << 7, 1u << 4, 1u << 5}; // 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
|
||||
#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
|
||||
|
||||
// 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 int moved, wait_frames; // sprint_link only: the sprint has moved since it started; 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");
|
||||
}
|
||||
|
||||
static uint64_t ld64(uint64_t a) { return a ? *(const uint64_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 = *(const int32_t *)(uintptr_t)(p + 0x28);
|
||||
if (id == 0x11a) id = *(const int32_t *)(uintptr_t)(p + 0x4c);
|
||||
for (size_t i = 0; i < sizeof states / sizeof *states; i++)
|
||||
if (states[i].id == id) return states[i].kind;
|
||||
return STOP;
|
||||
}
|
||||
|
||||
int sprint_available(void) { return rt_patch_enabled(rt_patch_set("sprint")); }
|
||||
|
||||
void sprint_pad(uint64_t *buttons) {
|
||||
int b = setting_button();
|
||||
if (b == BTN_OFF) return;
|
||||
static uint64_t last;
|
||||
uint64_t bit = button_bits[b], now = *buttons;
|
||||
if (now & ~last & bit) atomic_fetch_or(&pressed, bit);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) && (button != BTN_B || kind == MOVE || on);
|
||||
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) {
|
||||
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 + 0xf50, sprint_stance);
|
||||
}
|
||||
#endif
|
||||
11
runtime/sprint.h
Normal file
11
runtime/sprint.h
Normal file
@@ -0,0 +1,11 @@
|
||||
// Sprint button (sprint.c): the patch set sprint's host functions and the button's input.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// Called with the buttons of every controller poll the game gets (ov_input.c npad_states): notes the sprint button's
|
||||
// presses and keeps it from the game during gameplay.
|
||||
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 it is on: the Controls tab shows the sprint rows then.
|
||||
int sprint_available(void);
|
||||
@@ -54,6 +54,7 @@ extern "C" {
|
||||
#include "perfmon.h"
|
||||
#include "platform/platform.h"
|
||||
#include "save_share.h"
|
||||
#include "sprint.h"
|
||||
#include "sysinfo.h"
|
||||
#include "ui/menu.h"
|
||||
#include "ui_text.h"
|
||||
@@ -1631,6 +1632,15 @@ static void tab_controls(float scale) {
|
||||
bool restart;
|
||||
opt_choice("NX2NAVI_PROMPTS", UI_MENU_PROMPTS, prompts, 5, UI_MENU_PROMPTS_DESC, prompts_note(&restart));
|
||||
opt_toggle("NX2NAVI_MOTION", UI_MENU_MOTION, UI_MENU_MOTION_DESC);
|
||||
// The sprint button (sprint.c), where the game version has the patch set sprint; both rows are live. The names
|
||||
// follow the values in their cfg.c order.
|
||||
if (sprint_available()) {
|
||||
static const UiTextId sprint_button[] = {UI_MENU_SPRINT_OFF, UI_MENU_SPRINT_B, UI_MENU_SPRINT_L,
|
||||
UI_MENU_SPRINT_R, UI_MENU_SPRINT_L3, UI_MENU_SPRINT_R3};
|
||||
static const UiTextId sprint_mode[] = {UI_MENU_SPRINT_TOGGLE, UI_MENU_SPRINT_HOLD};
|
||||
opt_choice("NX2NAVI_SPRINT_BUTTON", UI_MENU_SPRINT_BUTTON, sprint_button, 6, UI_MENU_SPRINT_BUTTON_DESC);
|
||||
opt_choice("NX2NAVI_SPRINT_MODE", UI_MENU_SPRINT_MODE, sprint_mode, 2, UI_MENU_SPRINT_MODE_DESC);
|
||||
}
|
||||
opt_slider("NX2NAVI_RUMBLE_STRENGTH", UI_MENU_RUMBLE_STRENGTH, 0, 300, UI_MENU_RUMBLE_STRENGTH_DESC, 10, "%.0f %%");
|
||||
opt_slider("NX2NAVI_RUMBLE_CURVE", UI_MENU_RUMBLE_CURVE, 0.2f, 1.5f, UI_MENU_RUMBLE_CURVE_DESC, 0.05f, "%.2f");
|
||||
opt_toggle("NX2NAVI_RUMBLE_TRIGGERS", UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC);
|
||||
|
||||
@@ -326,6 +326,17 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_CHANNELS_AUTO] = "Automatic", [UI_MENU_CHANNELS_STEREO] = "Stereo", [UI_MENU_CHANNELS_51] = "5.1 surround",
|
||||
|
||||
[UI_MENU_MOTION] = "Motion controls", [UI_MENU_MOTION_DESC] = "Aim with the controller's gyro.",
|
||||
[UI_MENU_SPRINT_BUTTON] = "Sprint button",
|
||||
[UI_MENU_SPRINT_BUTTON_DESC] = "A button that makes Link sprint without rolling first. During play it only "
|
||||
"sprints: L or R no longer uses its item, R3 no longer resets the camera. The "
|
||||
"bottom button sprints only while Link moves and attacks as usual otherwise.",
|
||||
[UI_MENU_SPRINT_OFF] = "Off", [UI_MENU_SPRINT_B] = "Bottom button", [UI_MENU_SPRINT_L] = "L",
|
||||
[UI_MENU_SPRINT_R] = "R", [UI_MENU_SPRINT_L3] = "L3 (left stick)", [UI_MENU_SPRINT_R3] = "R3 (right 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 "
|
||||
"and ends when Link stops, attacks, rolls, targets, uses an item, swims or climbs. "
|
||||
"Hold: Link sprints while the button is held.",
|
||||
[UI_MENU_SPRINT_TOGGLE] = "Toggle", [UI_MENU_SPRINT_HOLD] = "Hold",
|
||||
[UI_MENU_RUMBLE_STRENGTH] = "Vibration strength",
|
||||
[UI_MENU_RUMBLE_STRENGTH_DESC] = "How strong the vibration is.",
|
||||
[UI_MENU_RUMBLE_CURVE] = "Vibration curve",
|
||||
|
||||
@@ -105,6 +105,9 @@ typedef enum {
|
||||
|
||||
// Controls
|
||||
UI_MENU_MOTION, UI_MENU_MOTION_DESC,
|
||||
UI_MENU_SPRINT_BUTTON, UI_MENU_SPRINT_BUTTON_DESC, UI_MENU_SPRINT_OFF, UI_MENU_SPRINT_B, UI_MENU_SPRINT_L,
|
||||
UI_MENU_SPRINT_R, UI_MENU_SPRINT_L3, UI_MENU_SPRINT_R3, UI_MENU_SPRINT_MODE, UI_MENU_SPRINT_MODE_DESC,
|
||||
UI_MENU_SPRINT_TOGGLE, UI_MENU_SPRINT_HOLD,
|
||||
UI_MENU_RUMBLE_STRENGTH, UI_MENU_RUMBLE_STRENGTH_DESC,
|
||||
UI_MENU_RUMBLE_CURVE, UI_MENU_RUMBLE_CURVE_DESC,
|
||||
UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC,
|
||||
|
||||
188
tests/sprint_check.c
Normal file
188
tests/sprint_check.c
Normal file
@@ -0,0 +1,188 @@
|
||||
// The sprint button (runtime/sprint.c) against a stand-in Link: the presses and the button the game gets from each
|
||||
// controller poll (sprint_pad), and the dash input the patch set's two host functions put into Link's flags, frame by
|
||||
// frame, for toggle and hold, each button, Link's states and a Link the player does not control. Built by
|
||||
// tests/sprint_check.py, which writes the stand-in layout.h and patches.h.
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
uint64_t test_main_base; // layout.h's MOD_MAIN_BASE: a buffer standing in for main's image
|
||||
const int rt_game_version = 111;
|
||||
|
||||
#include "sprint.c"
|
||||
|
||||
static const char *button = "off", *mode = "toggle";
|
||||
static GuestFn link_fn, stance_fn;
|
||||
const char *cfg_value(const char *key) {
|
||||
if (!strcmp(key, "NX2NAVI_SPRINT_BUTTON")) return button;
|
||||
if (!strcmp(key, "NX2NAVI_SPRINT_MODE")) return mode;
|
||||
return "";
|
||||
}
|
||||
int rt_patch_set(const char *name) { return strcmp(name, "sprint") ? -1 : 3; }
|
||||
int rt_patch_enabled(int set) { return set == 3; }
|
||||
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 abort();
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
static void set_state(int32_t id) { memcpy((char *)state_obj + 0x28, &id, 4); }
|
||||
|
||||
// One frame: a controller poll with these buttons, then Link's update. Returns what the game got from the poll.
|
||||
static uint64_t poll_buttons, flags_x10, flags_w8;
|
||||
static uint64_t frame(uint64_t buttons, int32_t state, uint64_t *who) {
|
||||
set_state(state);
|
||||
sprint_pad(&buttons);
|
||||
poll_buttons = buttons;
|
||||
Cpu c;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.x[19] = (uintptr_t)who;
|
||||
c.x[10] = 0x40; // other flag bits stay
|
||||
link_fn(&c);
|
||||
flags_x10 = c.x[10];
|
||||
c.x[8] = 0x01;
|
||||
stance_fn(&c);
|
||||
flags_w8 = c.x[8];
|
||||
return buttons;
|
||||
}
|
||||
static int dashing(void) { return (flags_x10 & 0x10) && (flags_w8 & 0x10) && (flags_x10 & 0x40) && (flags_w8 & 1); }
|
||||
|
||||
enum { WALK = 1, JUMP = 13, SLIDE = 35, WAIT_ID = 40, ROLL = 99, ATTACK = 112, CUTSCENE = 0 };
|
||||
#define L3 (1u << 4)
|
||||
#define BB (1u << 1)
|
||||
#define LL (1u << 6)
|
||||
#define ZL (1u << 8)
|
||||
|
||||
static void reset(const char *btn, const char *md) {
|
||||
button = btn, mode = md;
|
||||
for (int i = 0; i < 12; i++) frame(0, CUTSCENE, link); // the settings change and every sprint ends
|
||||
assert(!dashing());
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
char *img = calloc(1, 0x4c80000);
|
||||
assert(img);
|
||||
test_main_base = (uintptr_t)img;
|
||||
assert(link_fn && stance_fn && sprint_available());
|
||||
link[0] = test_main_base + 0x4b4dfc0, link[3] = (uintptr_t)owner;
|
||||
other[0] = test_main_base + 0x4b4dfc0, other[3] = (uintptr_t)a; // Link's class, another owner
|
||||
owner[0x270 / 8] = (uintptr_t)a, a[0x188 / 8] = (uintptr_t)b, b[0x1c0 / 8] = (uintptr_t)state_obj;
|
||||
// the actor manager names the controlled Link (the one whose +0x18 is owner)
|
||||
uint64_t *slot = (uint64_t *)(img + 0x4c7e0d0);
|
||||
uint64_t mgr_ptr = (uintptr_t)manager;
|
||||
*slot = (uintptr_t)&mgr_ptr;
|
||||
manager[0] = test_main_base + 0x4bdfe68, manager[0x470 / 8] = (uintptr_t)controlled;
|
||||
controlled[0] = test_main_base + 0x4b49698, controlled[3] = (uintptr_t)owner;
|
||||
|
||||
// off: nothing changes, the button reaches the game
|
||||
reset("off", "toggle");
|
||||
assert(frame(L3, WALK, link) == L3 && !dashing());
|
||||
|
||||
// toggle on L3: a press while walking starts it; the button never reaches the game during play
|
||||
reset("l3", "toggle");
|
||||
assert(frame(L3, WALK, link) == 0 && dashing());
|
||||
assert(frame(L3, WALK, link) == 0 && dashing()); // held: no second press
|
||||
assert(frame(0, WALK, link) == 0 && dashing());
|
||||
assert(frame(0, JUMP, link) == 0 && dashing()); // on through a jump
|
||||
assert(frame(0, SLIDE, link) == 0 && dashing());
|
||||
frame(L3, WALK, link); // the next press ends it
|
||||
assert(!dashing());
|
||||
frame(0, WALK, link);
|
||||
assert(!dashing());
|
||||
|
||||
// toggle: standing still ends it after 4 frames once it has moved
|
||||
frame(L3, WALK, link), frame(0, WALK, link);
|
||||
for (int i = 0; i < 4; i++) assert((frame(0, WAIT_ID, link), dashing()));
|
||||
frame(0, WAIT_ID, link);
|
||||
assert(!dashing());
|
||||
// ... a press while standing waits up to 90 frames for Link to move
|
||||
frame(L3, WAIT_ID, link), frame(0, WAIT_ID, link);
|
||||
assert(dashing());
|
||||
for (int i = 0; i < 60; i++) frame(0, WAIT_ID, link);
|
||||
assert(dashing());
|
||||
frame(0, WALK, link);
|
||||
assert(dashing());
|
||||
frame(L3, WALK, link);
|
||||
assert(!dashing());
|
||||
frame(L3, WAIT_ID, link), frame(0, WAIT_ID, link);
|
||||
for (int i = 0; i < 90; i++) frame(0, WAIT_ID, link);
|
||||
assert(!dashing());
|
||||
|
||||
// toggle: an attack, a roll, targeting or a cutscene ends it at once
|
||||
const int32_t enders[] = {ATTACK, ROLL, CUTSCENE};
|
||||
for (int k = 0; k < 3; k++) {
|
||||
frame(L3, WALK, link), frame(0, WALK, link);
|
||||
assert(dashing());
|
||||
frame(0, enders[k], link);
|
||||
assert(!dashing());
|
||||
frame(0, WALK, link);
|
||||
assert(!dashing());
|
||||
}
|
||||
frame(L3, WALK, link), frame(0, WALK, link);
|
||||
assert(frame(ZL, WALK, link) == ZL && !dashing()); // ZL reaches the game and targets
|
||||
|
||||
// a press during an attack does not start it
|
||||
frame(L3, ATTACK, link);
|
||||
assert(!dashing());
|
||||
|
||||
// Link's class without the player's control: no dash input, and the controlled Link keeps his sprint
|
||||
frame(0, WALK, link), frame(L3, WALK, link);
|
||||
assert(dashing());
|
||||
frame(0, WALK, other);
|
||||
assert(!(flags_x10 & 0x10) && !(flags_w8 & 0x10));
|
||||
frame(0, WALK, link);
|
||||
assert(dashing());
|
||||
frame(L3, WALK, link);
|
||||
|
||||
// a menu or a load: Link's update stops running, and after 8 polls the button reaches the game again
|
||||
{
|
||||
uint64_t got = 0;
|
||||
sprint_pad(&got); // released first, so the next poll is a press
|
||||
for (int i = 0; i < 9; i++) {
|
||||
uint64_t bt = L3;
|
||||
sprint_pad(&bt);
|
||||
got = bt;
|
||||
}
|
||||
assert(got == L3);
|
||||
frame(0, WALK, link); // the next update takes the press made meanwhile
|
||||
assert(dashing());
|
||||
frame(L3, WALK, link);
|
||||
assert(!dashing());
|
||||
}
|
||||
|
||||
// toggle on the bottom button: a sword press while not moving reaches the game and starts nothing
|
||||
reset("b", "toggle");
|
||||
assert(frame(BB, WAIT_ID, link) == BB && !dashing());
|
||||
frame(0, WALK, link); // Link moves: from the next poll the button is the sprint's
|
||||
assert(frame(BB, WALK, link) == 0 && dashing());
|
||||
assert(frame(0, WALK, link) == 0 && dashing());
|
||||
frame(BB, WALK, link);
|
||||
assert(!dashing() && poll_buttons == 0);
|
||||
frame(0, ATTACK, link);
|
||||
assert(frame(BB, ATTACK, link) == BB && !dashing()); // not moving: an attack as usual
|
||||
|
||||
// hold on L: sprint while held; L never reaches the game during play, R does
|
||||
reset("l", "hold");
|
||||
assert(frame(LL, WALK, link) == 0 && dashing());
|
||||
assert(frame(LL, WAIT_ID, link) == 0 && dashing());
|
||||
assert(frame(0, WALK, link) == 0 && !dashing());
|
||||
assert(frame(1u << 7, WALK, link) == (1u << 7) && !dashing());
|
||||
|
||||
// hold on the bottom button: starts only while moving, keeps going while held
|
||||
reset("b", "hold");
|
||||
frame(0, WALK, link);
|
||||
assert(frame(BB, WALK, link) == 0 && dashing());
|
||||
assert((frame(BB, JUMP, link), dashing()));
|
||||
assert(frame(0, WALK, link) == 0 && !dashing());
|
||||
frame(0, WAIT_ID, link);
|
||||
assert(frame(BB, WAIT_ID, link) == BB && !dashing());
|
||||
|
||||
puts("sprint_check: ok");
|
||||
free(img);
|
||||
return 0;
|
||||
}
|
||||
53
tests/sprint_check.py
Normal file
53
tests/sprint_check.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Build and run tests/sprint_check.c: the sprint button (runtime/sprint.c) against a stand-in Link, with the rest of
|
||||
the runtime stubbed. No game files: the module layout and patch page the runtime includes are stand-ins written to the
|
||||
test's build folder.
|
||||
|
||||
Usage: sprint_check.py
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
||||
|
||||
|
||||
def compiler():
|
||||
if os.environ.get('CC'):
|
||||
return os.environ['CC']
|
||||
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
|
||||
return str(MSYS_BIN / 'clang.exe')
|
||||
return shutil.which('clang') or 'clang'
|
||||
|
||||
|
||||
LAYOUT = '''// A stand-in for the generated layout.h (tests/sprint_check.py): main's image is a buffer of the test.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
extern uint64_t test_main_base;
|
||||
#define MOD_MAIN_BASE test_main_base
|
||||
'''
|
||||
PATCHES = '''// A stand-in for the generated patches.h (tests/sprint_check.py).
|
||||
#pragma once
|
||||
#define RT_PATCH_PAGE 0x10000ull
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
work = ROOT / 'build' / 'test' / 'sprint'
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
(work / 'layout.h').write_text(LAYOUT)
|
||||
(work / 'patches.h').write_text(PATCHES)
|
||||
exe = work / ('sprint_check.exe' if os.name == 'nt' else 'sprint_check')
|
||||
env = dict(os.environ)
|
||||
if os.name == 'nt' and MSYS_BIN.exists():
|
||||
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
||||
subprocess.check_call([compiler(), '-O1', '-Wall', '-Wno-unused-function', '-fno-strict-aliasing',
|
||||
'-I', str(work), '-I', str(ROOT / 'runtime'),
|
||||
str(ROOT / 'tests' / 'sprint_check.c'), '-o', str(exe)], env=env)
|
||||
return subprocess.run([str(exe)], env=env).returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -222,9 +222,9 @@ LLVM_LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lws2_32', '-luser32', '-lgdi32', '-ld
|
||||
DEFAULT_DEPS = ROOT / 'toolchain' / 'deps-mingw'
|
||||
# Runtime sources compiled per install, not in the library: romfs.c includes romfs_config.h, patches.c and ov_zstd.c
|
||||
# the patches.h of the player's game version (its patch page is the page after that version's main; ov_zstd.c's host
|
||||
# functions sit on it, and patches.c registers the debug menu's free camera there). mouse_direct.c also registers
|
||||
# host callbacks relative to that version's patch page.
|
||||
INSTALL_SIDE = {'romfs.c', 'patches.c', 'ov_zstd.c', 'mouse_direct.c'}
|
||||
# functions sit on it, and patches.c registers the debug menu's free camera there). mouse_direct.c and sprint.c also
|
||||
# register host callbacks relative to that version's patch page.
|
||||
INSTALL_SIDE = {'romfs.c', 'patches.c', 'ov_zstd.c', 'mouse_direct.c', 'sprint.c'}
|
||||
LIB_NAME = 'libnx2navirt.a'
|
||||
AR = UCRT / 'llvm-ar.exe' if WIN else on_path('llvm-ar')
|
||||
WINDRES = UCRT / 'windres.exe' # runtime/nx2navi.rc: the manifest of nx2navi.exe
|
||||
|
||||
Reference in New Issue
Block a user