Make the direct mouse camera work on game 1.1.1.

mouse_mode = direct is the default, but on 1.1.1 it did nothing: only the 1.4.3 patch table had the sites, so the mouse always went through the stick emulation, which gives no turn at slow mouse speeds and tops out at about 107 degrees a second. The 1.1.1 table now has the set mouse_direct (on by default): at main+0x37c45c, where the camera's input update holds the processed stick in s1 and s0, a host call performs the displaced store and adds the raw mouse counts to both, so the mouse meets no dead zone and no curve; at main+0x37c4f0 a second call clears the idle bit for an update the mouse moved in. The counts are divided by the frames the game steps per update and by the stick camera speed, so the same hand movement turns the same angle at every frame rate. The Controls tab gets the mouse mode and mouse sensitivity rows, and mouse_mode = stick or NX2NAVI_MOUSE_DIRECT_PATCH=0 gives the old behaviour.

Checked here: the generator lifts the 1.1.1 game with the two sites (it compares each site's original word), the lifted game compiles and links with the runtime, and tests/mouse_direct_check.py passes its 1.1.1 harness (register contract, displaced store and load, equal turn at every frame rate and stick speed, the reset pause, capture and mode). Not checked here: the game was not run, so aiming with the mouse was not tried, and the differential tests against Unicorn were not run because no Python on this PC has Unicorn. The contributor measured 0.0800 degrees per count on both axes in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit bb8fecfbe43517006eafd7be9c22e8dbe9601de7)
This commit is contained in:
chrg6
2026-11-11 00:04:18 -05:00
parent 1e01f90db5
commit 861357b2a4
14 changed files with 260 additions and 19 deletions

View File

@@ -343,6 +343,13 @@ A change to the step Ocarina is given each frame (`runtime/frame_rate.c` `ocarin
display times: a held 60 fps limit gives exactly one frame every frame, a 240 fps limit never less than a quarter
frame, uneven frame times one steady step, and game time follows the clock. 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
calls the camera hooks as their patch sites do, for each game version: the registers a site hands over and gets back,
the same turn for the same mouse distance at every frame rate and stick camera speed, a single count, the idle bit, the
pause after a camera reset, the capture and the mode, and scripted mouse counts. No game files.
A change to which waits a signalled event or an ended thread wakes (`runtime/kernel.c` `wake_sync_waiters`, the
`WaitSynchronization` service) passes `python tests\sync_wake_check.py`, which builds `kernel.c` with the platform
layer and the rest of the runtime stubbed and runs guest threads whose entries are host functions: a signal wakes the

View File

@@ -126,7 +126,9 @@ bind_Y = pad:north, key:mouse_l
Mouse motion (raw input) becomes right-stick deflection. Each presented frame takes the counts moved since the last frame, divides by the frame time and sets the deflection to `counts per second * NX2NAVI_MOUSE_SENS / 3000` of full, held for that frame, so the turn rate does not depend on the frame rate. This is not mouselook: the game's stick curve and dead zone still apply, so very slow movements do nothing and a fast flick saturates. With a camera speed other than 1 (`NX2NAVI_STICK_SENS`) the deflection is divided by that factor before it is limited to full tilt: the turn for a given movement stays the same, and the fastest turn a flick reaches is the game's top speed with the factor in it. The mouse deflection competes with the gamepad and arrow keys by the larger-deflection rule.
`NX2NAVI_MOUSE_SENS` (0 to 10, default 1; higher turns faster, 0 turns the mouse camera and the cursor capture off). While the window is the foreground window the cursor is hidden and confined to the client area. It is released on focus loss, minimise, while F11 changes the window, during a size or move loop and while a text entry is open. Esc releases it by hand: the cursor shows, the mouse camera and mouse buttons stop, and the window can be moved, minimised or closed. A mouse click inside the client area captures it again; that click does not reach the game. Activating the window by a click on its title bar or border does not capture, so it can be dragged or closed right away; Alt+Tab or a click in the client area captures as before. `NX2NAVI_INPUT=none` disables the capture too.
`NX2NAVI_MOUSE_MODE` (`mouse_mode`, the *Mouse camera* row on the Controls tab, live) chooses that right-stick deflection (`stick`) or mouselook (`direct`, the default). In `direct` the camera's input update (`main+0x37c300`) takes the raw counts itself, after the game's stick dead zone, clamp and response curve (`recomp/patches-v111.txt`, set `mouse_direct`; `runtime/mouse_direct.c` `camera_input_oot`). The camera turns by how far the mouse moved, the same on both axes: about 0.08 degrees a count at `NX2NAVI_MOUSE_SENS` 1, at any frame rate and any `NX2NAVI_STICK_SENS`, with no dead zone and no top speed. The game's own camera sensitivity (its Options) scales the mouse as it scales the stick. A gamepad's right stick and the arrow keys still go through the game's response and add to the mouse. While the game takes no camera input (it switched camera input off, or the pause after a camera reset runs) the mouse is the right stick as above. `NX2NAVI_MOUSE_DIRECT_PATCH=0` turns the patch set off, and the mouse is the right stick whatever the mode. `NX2NAVI_MOUSE_TRACE=1` logs each camera input update (the stick, the counts taken and what they added) and, with `dlaa` on (DLAA, DLSS or FSR 3.1, which read the game's camera), the camera's direction in each frame, for measuring the turn per count.
`NX2NAVI_MOUSE_SENS` (the *Mouse camera sensitivity* row, live; 0 to 10, default 1; higher turns faster, 0 turns the mouse camera and the cursor capture off, the row goes down to 0.05). While the window is the foreground window the cursor is hidden and confined to the client area. It is released on focus loss, minimise, while F11 changes the window, during a size or move loop and while a text entry is open. Esc releases it by hand: the cursor shows, the mouse camera and mouse buttons stop, and the window can be moved, minimised or closed. A mouse click inside the client area captures it again; that click does not reach the game. Activating the window by a click on its title bar or border does not capture, so it can be dragged or closed right away; Alt+Tab or a click in the client area captures as before. `NX2NAVI_INPUT=none` disables the capture too.
`NX2NAVI_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it. `NX2NAVI_LSTICK_DEADZONE` and `NX2NAVI_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows under *Analog sticks* on the Controls tab) set one stick each and fall back to `NX2NAVI_STICK_DEADZONE` while unset, so a worn stick can get a larger zone than the other. With a gamepad connected each row shows its stick's deflection now, before the dead zone (*stick now*): let go of the stick and set the row a few percent above the value it rests at.

View File

@@ -652,10 +652,12 @@
# lstick_deadzone = 8
# rstick_deadzone = 8
# Mouse camera mode: direct (default) or stick. Direct turns the camera by the raw mouse motion, the same on both axes,
# with no stick dead zone or response curve. Stick makes the
# with no stick dead zone or response curve, at any frame rate and however fast the mouse moves. Stick makes the
# mouse a right stick. Neither changes the gamepad. The Controls tab changes this while the game runs.
# mouse_mode = direct
# Mouse camera sensitivity (0 to 10; 0 = no mouse camera and no cursor capture).
# Mouse camera sensitivity (0 to 10; 0 = no mouse camera and no cursor capture). In direct mode a count of the mouse
# turns the camera about 0.08 degrees at 1; the game's own camera sensitivity (Options) scales it as it scales the
# stick. The Controls tab changes this while the game runs.
# mouse_sens = 1
# The keyboard's sticks are all or nothing, so the game only sees a full tilt from them; while this key is held the
# keyboard-driven stick values shrink and the keyboard can walk instead of run. Key names as in the bind_ lines below,

View File

@@ -59,6 +59,20 @@ site osolver main 38afd20 1e380001 1e280001
# manager, last read at main+0xa682ec, and every path from the site reaches a call before it reads x13 again.
site opickup main a68300 1e38000c 1e38000c 5ac0118d 53057dad 0b0d018c # fcvtzs w12, s0; clz w13, w12; lsr w13, w13, #5; add w12, w12, w13
# Mouse camera: the camera's stick input update (main+0x37c300) puts the right stick through a radial dead zone of
# 0.005, scales the radius to one, clamps it and raises it to StickExponent (2, the default of
# game__component__PlayerCameraParam); then come the reset timer at +0x5e0 (the stick reads zero while it runs), the X/Y
# inversion (w21 bits 1 and 2) and the idle test (flags +0x530 bit 4: the processed stick under 10%). At 37c45c s1 and
# s0 hold the processed x and y for the stores at +0x538 and +0x53c: the host callback performs the displaced store of
# s1 and adds the raw mouse counts to both (runtime/mouse_direct.c camera_input_oot), so the mouse never becomes a stick
# value and meets no dead zone or curve. At 37c4f0, the displaced ldp d9, d8, [sp, #0x10] before the update's last
# `str w8, [x20, #0x530]`, w8 holds the flags: a second callback clears the idle bit for an update the mouse moved in
# (camera_activity_oot). x30 is the function's own saved one at both sites. The callbacks' addresses are page +0xfd0
# and +0xfc0; the page is main+0x4ea0000 on this build.
set mouse_direct NX2NAVI_MOUSE_DIRECT_PATCH 1
site mouse_direct main 37c45c bd053a81 952c92dd # str s1, [x20, #0x538] -> bl page+0xfd0
site mouse_direct main 37c4f0 6d4123e9 952c92b4 # ldp d9, d8, [sp, #0x10] -> bl page+0xfc0
# Game image: the graphics setup (main+0x1202ce0) fills the renderer's configuration with the size of the picture the
# game presents, 2560x1440 at every console mode (config +0x14, copied to renderer +0x9c, which main+0x346be50 reads
# for the main render target set, main+0xf49040), and the display settings object (main+0x31f3890) stores the same size

View File

@@ -52,7 +52,7 @@ static const char *const known[] = {
"HDR_HUE_CLIP", "HDR_LIVE", "HEAP_LAZY", "HDR_MATCH_SLOPE", "HDR_MIDGRAY",
"HDR_PEAK_NITS", "HDR_SATURATION", "HDR_SHADOWS", "HDR_TONEMAP", "HDR_TONE_SLOPE", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
"BIG_BUILD_FREE_MB", "BIG_BUILD_MAX_MB", "LIB_PREBUILD_BIG", "LIB_PREBUILD_BIG_BG", "LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LIB_WARM", "PIPE_LINK_MS", "FRAME_HOLD_MS", "PACE_SMOOTH", "OQ_BATCH", "GATE_WAIT_MS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MEM_CENSUS", "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", "MOVIE_HOST", "MOVIE_PACE",
"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",
"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",
"MV_SURVEY",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PACE_HZ", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_RES_FIXED", "PATCH_SCENE", "PATCH_WIDE_ASPECT", "PATCH_WIDE_HUD", "PATCH_WIDE_MOVIE", "PATCH_WIDE_OUT", "PATCH_WIDE_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPELINE_CACHE_MAX_MB", "PIPE_BINARIES", "PIPE_BINARIES_MAX_MB", "PIPE_FEEDBACK", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT", "POWER_FULL", "TEX_SAME", "PRESENT_HELD", "HELD_SYNC",

View File

@@ -434,6 +434,16 @@ static double ocarina_step(double dt) {
return step;
}
// The 60 Hz frames the game steps at each update where the set ostep gives them (Ocarina); 1 elsewhere, the game's own
// nominal frame. The mouse camera divides by it (mouse_direct.c): the game multiplies camera input by this step.
float frame_step_frames(void) {
if (!ostep_page) return 1.0f;
uint32_t bits = atomic_load_explicit(ostep_page, memory_order_acquire);
float f;
memcpy(&f, &bits, 4);
return f > 0 ? f : 1.0f;
}
static void ocarina_step_write(double dt) {
static int on = -1;
if (on < 0) {

View File

@@ -386,6 +386,7 @@ int frame_limit_adjustable(void); // the frame limit set
int frame_free_running(void); // no fps60/realtime patch set: the game steps by the clock
double frame_step_limit_hz(void); // the frame rate the limit gives (overlay); 0 unknown
double frame_step_game_hz(void); // the rate the game draws at under the limit; 0 unknown
float frame_step_frames(void); // the 60 Hz frames the game steps per update (set ostep), else 1
// rtss_limit.c: RivaTuner Statistics Server takes the frame limit where it runs
void rtss_limit_want(int64_t mhz); // the pacer's limit in millihertz: >0, 0 none, -1 keep RTSS out
int rtss_limit_active(void); // RTSS limits to the wanted rate: the pacer holds no frames

View File

@@ -931,8 +931,12 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
input_map_stick(down, 1, k);
if (walk) k[0] = (int32_t)(k[0] * walk_scale), k[1] = (int32_t)(k[1] * walk_scale);
pick(&sr[0], &sr[1], k[0], k[1]);
int selector = b & (1ull << B_L) ? 1 : b & (1ull << B_UP) ? 2 :
b & (1ull << B_LEFT) ? 3 : b & (1ull << B_RIGHT) ? 4 : 0;
// The buttons whose selector the right stick picks from. None in Ocarina (1.1.1): its D-pad acts at once
// (ocarina, accessory, Navi, boots), and with L or R held the camera input update goes on taking the
// mouse; where an update stops taking it, the mouse is the right stick again (mouse_camera).
int selector = rt_game_version == 111 ? 0
: b & (1ull << B_L) ? 1 : b & (1ull << B_UP) ? 2 :
b & (1ull << B_LEFT) ? 3 : b & (1ull << B_RIGHT) ? 4 : 0;
mouse_camera(k, selector);
pick(&sr[0], &sr[1], k[0], k[1]);
int32_t shake = shake_stick(input_map_held(down, B_SHAKE));

View File

@@ -14,6 +14,14 @@ int input_kbm_last(void);
// separately: a persistent radial cursor for the ability wheel, discrete steps for horizontal quick lists.
int input_direct_mouse_active(void);
int input_direct_mouse_enabled(void);
int input_direct_mouse_available(void); // the game version has the patch set and it is on: the mode can be chosen
// Ocarina 1.1.1's camera input update (mouse_direct.c camera_input_oot; recomp/patches-v111.txt). Measured with a
// scripted stick, its processed stick at one turns the camera 1 degree up or down and 1.78 degrees sideways
// (StickSensitivityLat2LngRatio) per 60 Hz frame; the game's own camera sensitivity option scales both.
// INPUT_OOT_COUNT_X and _Y, what one mouse count adds to the processed stick per frame at sensitivity 1, make a count
// 0.08 degrees on either axis.
#define INPUT_OOT_COUNT_X (0.08 / 1.78)
#define INPUT_OOT_COUNT_Y 0.08
// A radial selector uses a persistent virtual cursor while its opening button is held.
void input_direct_mouse_radial(int32_t dx, int32_t dy, float sensitivity, float cursor[2], int32_t out[2]);
// Horizontal quick selectors receive short, separated steps instead of a latched radial-cursor direction.

View File

@@ -44,6 +44,8 @@ int input_direct_mouse_active(void) {
int input_direct_mouse_enabled(void) { return direct_mode(); }
int input_direct_mouse_available(void) { return rt_patch_enabled(rt_patch_set("mouse_direct")); }
void input_direct_mouse_radial(int32_t dx, int32_t dy, float sensitivity, float cursor[2], int32_t out[2]) {
if (!(sensitivity > 0)) { cursor[0] = cursor[1] = 0; out[0] = out[1] = 0; return; }
cursor[0] += dx * (double)sensitivity / 200;
@@ -330,7 +332,90 @@ static void camera_activity(Cpu *c) {
c->v[8] = d8;
}
// ---- Ocarina 1.1.1 (recomp/patches-v111.txt, set mouse_direct)
// Its camera input update is main+0x37c300: the same steps as above with other registers and offsets. At the site s1
// and s0 hold the processed x and y (both zero while the camera-reset timer at +0x5e0 runs), x20 the input object and
// x21 the flags (bit 0 camera input off, bit 1 invert x, bit 2 invert y). The two stores, the inversion and the idle
// test that follow read s1 and s0, so the counts added here go through them as the stick's part does.
#define OOT_STICK 0x538 // the processed stick: x, and y at +0x53c
#define OOT_RESET_TIMER 0x5e0 // 60 Hz frames left of the pause after a camera reset (right stick click)
#define OOT_FLAG_IDLE 0x10u // flags (+0x530): the processed stick is under the game's 10% idle threshold
// NX2NAVI_MOUSE_TRACE=1 (test runs): one line per camera input update, with the stick the update made, the counts taken
// and what they added. vk_dlaa.c adds the camera's direction per frame, so a scripted run gives degrees per count.
static int mouse_trace(void) {
static int on = -1;
if (on < 0) {
const char *e = cfg_get("NX2NAVI_MOUSE_TRACE");
on = e && *e && strcmp(e, "0");
}
return on;
}
static void camera_input_oot(Cpu *c) {
mouse_input_object = 0;
uint64_t object = c->x[20], now = plat_now_ns();
uint32_t flags = (uint32_t)c->x[21];
float timer = guest_float(object + OOT_RESET_TIMER);
int accept = !(flags & 1) && !(timer > 0) && !input_direct_mouse_selector_active();
__atomic_store_n(&accepts_mouse, accept, __ATOMIC_RELAXED);
__atomic_store_n(&last_update, now, __ATOMIC_RELEASE);
float x, y, add[2] = {0, 0}, frames = frame_step_frames();
memcpy(&x, &c->v[1], sizeof x);
memcpy(&y, &c->v[0], sizeof y);
int32_t dx = 0, dy = 0;
if (direct_mode()) {
input_direct_mouse_sample(accept, &dx, &dy);
float sensitivity = fminf(10, fmaxf(0, (float)cfg_double("NX2NAVI_MOUSE_SENS")));
// The game multiplies the processed stick by the frames it steps and by its camera speeds, which
// NX2NAVI_STICK_SENS scales (camera_speed.c): divided by both, a count turns the camera by the same angle at
// every frame rate and stick speed. Nothing after the site clamps the sum, so a fast flick is not cut short.
float per = frames * cam_speed_applied();
if (accept && sensitivity > 0 && isfinite(per) && per > 0 && (dx || dy)) {
mouse_input_object = object;
add[0] = dx * (float)INPUT_OOT_COUNT_X * sensitivity / per;
add[1] = -dy * (float)INPUT_OOT_COUNT_Y * sensitivity / per;
float nx = x + add[0], ny = y + add[1];
uint32_t xb, yb;
memcpy(&xb, &nx, sizeof xb);
memcpy(&yb, &ny, sizeof yb);
c->v[1] = xb;
c->v[0] = yb;
static int logged;
if (!logged) {
logged = 1;
rt_log("mouse: direct camera input after the stick dead zone, clamp and exponent (Ocarina 1.1.1)");
}
}
}
if (mouse_trace())
rt_log("mousetrace: in t=%.3f frames=%.4f flags=%x accept=%d timer=%.2f stick=%.5f,%.5f counts=%d,%d add=%.5f,%.5f",
now / 1e6, frames, flags, accept, timer, x, y, dx, dy, add[0], add[1]);
uint32_t original = (uint32_t)c->v[1];
memcpy((void *)(uintptr_t)(object + OOT_STICK), &original, sizeof original); // displaced str s1, [x20, #0x538]
}
// The idle test follows the first site and already counts the mouse's part, but one slow count adds less than the
// game's 10% threshold: an update the mouse moved in is not idle. The site is the displaced ldp d9, d8, [sp, #0x10]
// before the update's last `str w8, [x20, #0x530]`, with the flags in w8.
static void camera_activity_oot(Cpu *c) {
if (mouse_input_object && mouse_input_object == c->x[20]) {
c->x[8] = (uint32_t)c->x[8] & ~OOT_FLAG_IDLE;
mouse_input_object = 0;
}
uint64_t d9, d8;
memcpy(&d9, (const void *)(uintptr_t)(c->sp + 0x10), sizeof d9);
memcpy(&d8, (const void *)(uintptr_t)(c->sp + 0x18), sizeof d8);
c->v[9] = d9;
c->v[8] = d8;
}
__attribute__((constructor)) static void mouse_direct_register(void) {
if (rt_game_version == 111) { // Ocarina: two sites of its own, no orbit or held-object site
rt_register_host(HOST_MOUSE_DIRECT, camera_input_oot);
rt_register_host(HOST_MOUSE_ACTIVITY, camera_activity_oot);
return;
}
rt_register_host(HOST_MOUSE_DIRECT, camera_input);
rt_register_host(HOST_MOUSE_ACTIVITY, camera_activity);
rt_register_host(HOST_MOUSE_ORBIT_BEGIN, orbit_begin);

View File

@@ -1411,6 +1411,15 @@ static void restart_confirm(float scale) {
static void tab_controls(float scale) {
opt_begin("##controls", scale);
// The mouse camera first, where a player on keyboard and mouse looks for it; both rows are live. How the mouse
// reaches the game, where the game version has the direct mode's patch set (mouse_direct.c; the values in their
// cfg.c order, stick then direct), and its sensitivity. The slider stops short of 0, which nx2navi.ini takes to
// switch the mouse camera and the cursor capture off.
if (input_direct_mouse_available()) {
static const UiTextId mouse_mode[] = {UI_MENU_MOUSE_STICK, UI_MENU_MOUSE_DIRECT};
opt_choice("NX2NAVI_MOUSE_MODE", UI_MENU_MOUSE_MODE, mouse_mode, 2, UI_MENU_MOUSE_MODE_DESC);
}
opt_slider("NX2NAVI_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0.05f, 10, UI_MENU_MOUSE_SENS_DESC, 0.05f, "%.2f");
static const UiTextId layout[] = {UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO, UI_MENU_PAD_LAYOUT_XBOX,
UI_MENU_PAD_LAYOUT_PLAYSTATION};
opt_choice("NX2NAVI_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 4, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note());

View File

@@ -301,7 +301,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_STICK_DEADZONE_DESC] = "How far a stick must move to register. Raise it if a stick drifts.",
[UI_MENU_STICK_DEADZONE_NOW] = "stick now: %.0f %%",
[UI_MENU_MOUSE_MODE] = "Mouse camera",
[UI_MENU_MOUSE_MODE_DESC] = "Direct: the camera turns by how far the mouse moves, as in a PC game, with no stick dead zone or curve (game 1.4.3; other versions use stick emulation). Stick emulation: the mouse acts as the right stick.",
[UI_MENU_MOUSE_MODE_DESC] = "Direct: the camera turns by how far the mouse moves, as in a PC game, with no stick dead zone or curve (games 1.1.1 and 1.4.3; other versions use stick emulation). Stick emulation: the mouse acts as the right stick.",
[UI_MENU_MOUSE_STICK] = "Stick emulation",
[UI_MENU_MOUSE_DIRECT] = "Direct",
[UI_MENU_MENU_CURSOR] = "Mouse in the game's menus",
@@ -1012,7 +1012,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_STICK_DEADZONE_DESC] = "Насколько нужно сдвинуть стик, чтобы он сработал. Увеличьте при дрейфе.",
[UI_MENU_STICK_DEADZONE_NOW] = "стик сейчас: %.0f %%",
[UI_MENU_MOUSE_MODE] = "Управление мышью",
[UI_MENU_MOUSE_MODE_DESC] = "Прямое: камера поворачивается на столько, на сколько сдвинута мышь, как в играх для ПК, без мёртвой зоны и кривой стика (версия игры 1.4.3; на других версиях эмуляция стика). Эмуляция стика: мышь работает как правый стик.",
[UI_MENU_MOUSE_MODE_DESC] = "Прямое: камера поворачивается на столько, на сколько сдвинута мышь, как в играх для ПК, без мёртвой зоны и кривой стика (версии игры 1.1.1 и 1.4.3; на других версиях эмуляция стика). Эмуляция стика: мышь работает как правый стик.",
[UI_MENU_MOUSE_STICK] = "Эмуляция стика",
[UI_MENU_MOUSE_DIRECT] = "Прямое",
[UI_MENU_MENU_CURSOR] = "Мышь в меню игры",
@@ -1818,7 +1818,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_PROMPTS_PLAYSTATION] = "PlayStation",
[UI_MENU_PROMPTS_PC] = "Teclado e mouse",
[UI_MENU_MOUSE_MODE] = "Câmera com o mouse",
[UI_MENU_MOUSE_MODE_DESC] = "Direta: a câmera gira o quanto o mouse se move, como num jogo de PC, sem zona morta nem curva do analógico (jogo 1.4.3; as outras versões usam a emulação do analógico). Emulação do analógico: o mouse age como o analógico direito.",
[UI_MENU_MOUSE_MODE_DESC] = "Direta: a câmera gira o quanto o mouse se move, como num jogo de PC, sem zona morta nem curva do analógico (jogos 1.1.1 e 1.4.3; as outras versões usam a emulação do analógico). Emulação do analógico: o mouse age como o analógico direito.",
[UI_MENU_MOUSE_STICK] = "Emulação do analógico",
[UI_MENU_MOUSE_DIRECT] = "Direta",
[UI_MENU_MENU_CURSOR] = "Mouse nos menus do jogo",
@@ -2635,7 +2635,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_PROMPTS_PLAYSTATION] = "PlayStation",
[UI_MENU_PROMPTS_PC] = "键盘和鼠标",
[UI_MENU_MOUSE_MODE] = "鼠标视角",
[UI_MENU_MOUSE_MODE_DESC] = "直接:镜头按鼠标移动的距离转动,与电脑游戏相同,没有摇杆死区和响应曲线(游戏 1.4.3;其他版本使用摇杆模拟)。摇杆模拟:鼠标相当于右摇杆。",
[UI_MENU_MOUSE_MODE_DESC] = "直接:镜头按鼠标移动的距离转动,与电脑游戏相同,没有摇杆死区和响应曲线(游戏 1.1.1 和 1.4.3;其他版本使用摇杆模拟)。摇杆模拟:鼠标相当于右摇杆。",
[UI_MENU_MOUSE_STICK] = "摇杆模拟",
[UI_MENU_MOUSE_DIRECT] = "直接",
[UI_MENU_MENU_CURSOR] = "在游戏菜单中使用鼠标",

View File

@@ -936,6 +936,15 @@ static void fg_camera(const float *v, const double m[16]) {
c->fov_y = P[5] != 0 ? 2.0f * (float)atan(1.0 / fabs(P[5])) : 1.0f;
c->aspect = P[0] != 0 ? (float)fabs(P[5] / P[0]) : 1.777f;
D.fg_cam_ok = 1;
// NX2NAVI_MOUSE_TRACE=1 (test runs, mouse_direct.c): the camera's direction each frame
static int trace = -1;
if (trace < 0) {
const char *e = cfg_get("NX2NAVI_MOUSE_TRACE");
trace = e && *e && strcmp(e, "0");
}
if (trace)
rt_log("mousetrace: cam t=%.3f fwd=%.6f,%.6f,%.6f pos=%.3f,%.3f,%.3f", plat_now_ns() / 1e6, c->fwd[0], c->fwd[1],
c->fwd[2], c->pos[0], c->pos[1], c->pos[2]);
}
// Frames without a camera (dlaa_cam_keep, NX2NAVI_DLAA_CAM_KEEP; 1 by default, 0 restores the old handling). Measured in

View File

@@ -9,17 +9,21 @@ import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
# What both harnesses stand in for the rest of the runtime with.
STUBS = r'''
#include <assert.h>
#include <stdio.h>
#include "mouse_direct.c"
static int raw_x, raw_y, takes, captured=1, foreground=1, allowed=1, mode=1, patched=1, selecting;
static float ratio=0.5f;
static float ratio=0.5f, frames=1, speed=1;
static double sens=1;
static uint64_t now=1000000000;
static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook;
const char *cfg_get(const char *key) { (void)key; return mode ? "direct" : "stick"; }
const char *cfg_get(const char *key) { return strcmp(key, "NX2NAVI_MOUSE_MODE") ? NULL : mode ? "direct" : "stick"; }
const char *cfg_value(const char *key) { return cfg_get(key); }
double cfg_double(const char *key) { (void)key; return 1; }
double cfg_double(const char *key) { (void)key; return sens; }
float frame_step_frames(void) { return frames; }
float cam_speed_applied(void) { return speed; }
int rt_patch_set(const char *s) { (void)s; return 0; }
int rt_patch_enabled(int s) { (void)s; return patched; }
int ikb_captured(void) { return captured; }
@@ -44,8 +48,12 @@ void plat_mutex_lock(PlatMutex *m) { (void)m; }
void plat_mutex_unlock(PlatMutex *m) { (void)m; }
void rt_log(const char *fmt, ...) { (void)fmt; }
static float scalar(__uint128_t v) { float f; memcpy(&f,&v,4); return f; }
'''
HARNESS = STUBS + r'''
const int rt_game_version = 143;
int main(void) {
assert(hook && activity_hook);
assert(hook && activity_hook && begin_hook && angles_hook && hand_hook);
Cpu c, expected;
unsigned char target[0x500] = {0};
memset(&c, 0x5a, sizeof c);
@@ -208,6 +216,82 @@ int main(void) {
}
'''
# Ocarina 1.1.1 (recomp/patches-v111.txt, set mouse_direct): the two sites of camera_input_oot and camera_activity_oot.
OOT_HARNESS = STUBS + r'''
const int rt_game_version = 111;
int main(void) {
assert(hook && activity_hook && !begin_hook && !angles_hook && !hand_hook); // no orbit or held-object site
Cpu c, expected;
unsigned char target[0x600] = {0};
memset(&c, 0x5a, sizeof c);
c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[1]=c.v[0]=0;
expected=c;
// s1 and s0 carry the processed x and y. 100 counts to the right and 50 up, at one frame per update.
raw_x=100; raw_y=-50;
hook(&c);
assert(fabsf(scalar(c.v[1])-100*(float)INPUT_OOT_COUNT_X)<1e-5f);
assert(fabsf(scalar(c.v[0])-50*(float)INPUT_OOT_COUNT_Y)<1e-5f);
// The displaced store of s1 at +0x538 carries the mouse's part; the store of s0 at +0x53c is the game's own.
float stored; memcpy(&stored,target+0x538,4); assert(stored==scalar(c.v[1]));
memcpy(&stored,target+0x53c,4); assert(stored==0);
expected.v[1]=c.v[1]; expected.v[0]=c.v[0]; assert(!memcmp(&expected,&c,sizeof c));
assert(input_direct_mouse_active() && input_direct_mouse_available());
// The next update sees no repeated movement.
expected=c; hook(&c); assert(!memcmp(&expected,&c,sizeof c));
// The game multiplies the processed stick by the frames it steps and by its camera speed: an equal mouse distance
// gives an equal turn at 30, 60 and 144 fps and at every stick camera speed, and nothing clamps a fast flick.
float steps[]={2,1,60/144.0f}, speeds[]={1,3};
for(int i=0;i<3;i++) for(int j=0;j<2;j++) {
frames=steps[i]; speed=speeds[j]; raw_x=10000; raw_y=10000; c.v[1]=c.v[0]=0; hook(&c);
assert(fabsf(scalar(c.v[1])*frames*speed-10000*(float)INPUT_OOT_COUNT_X)<0.01f);
assert(fabsf(scalar(c.v[0])*frames*speed+10000*(float)INPUT_OOT_COUNT_Y)<0.01f);
}
frames=speed=1;
// One count is not discarded by a dead zone, and the sensitivity scales it.
raw_x=1; raw_y=0; c.v[1]=c.v[0]=0; hook(&c); assert(scalar(c.v[1])>0);
float one=scalar(c.v[1]);
sens=2.5; raw_x=1; c.v[1]=c.v[0]=0; hook(&c); assert(fabsf(scalar(c.v[1])-2.5f*one)<1e-6f);
sens=0; raw_x=1; c.v[1]=c.v[0]=0; hook(&c); assert(c.v[1]==0 && c.v[0]==0); sens=1;
// The stick's own processed value stays and the mouse adds to it.
{ float px=0.25f, py=-0.5f; uint32_t b; memcpy(&b,&px,4); c.v[1]=b; memcpy(&b,&py,4); c.v[0]=b; }
raw_x=10; raw_y=0; hook(&c);
assert(fabsf(scalar(c.v[1])-(0.25f+10*(float)INPUT_OOT_COUNT_X))<1e-6f && scalar(c.v[0])==-0.5f);
// The idle bit of the flags (w8 at the second site) is cleared for an update the mouse moved in; the site's
// displaced ldp d9, d8, [sp, #0x10] is performed. The game's next instruction stores w8, so memory is not touched.
uint64_t stack[4]={0,0,0x123456789abcdef0ull,0x456789abcdef0123ull}; c.sp=(uintptr_t)stack;
uint32_t idle=0x38, mem=0x77; memcpy(target+0x530,&mem,4);
c.x[8]=idle; activity_hook(&c);
assert((uint32_t)c.x[8]==0x28);
uint32_t after; memcpy(&after,target+0x530,4); assert(after==mem);
assert(c.v[9]==stack[2] && c.v[8]==stack[3]);
// A controller-only update keeps the game's idle bit.
c.v[1]=c.v[0]=0; hook(&c); c.x[8]=idle; activity_hook(&c); assert((uint32_t)c.x[8]==idle);
// While the pause after a camera reset runs (+0x5e0) or camera input is off (w21 bit 0), the game reads no stick:
// the counts are left where they are and the registers as the game made them.
float pause=12; memcpy(target+0x5e0,&pause,4);
raw_x=42; int before=takes; c.v[1]=c.v[0]=0; expected=c; hook(&c);
assert(takes==before && raw_x==42 && !memcmp(&expected,&c,sizeof c));
assert(!input_direct_mouse_active());
pause=0; memcpy(target+0x5e0,&pause,4);
c.x[21]=1; hook(&c);
assert(takes==before && raw_x==42 && !input_direct_mouse_active()); c.x[21]=0;
selecting=1; hook(&c); assert(takes==before && raw_x==42); selecting=0;
// Focus loss and host menus do not move the camera.
captured=0; hook(&c); assert(takes==before); captured=1;
foreground=0; hook(&c); assert(takes==before); foreground=1;
allowed=0; hook(&c); assert(takes==before); allowed=1;
// Stick emulation: the counts stay for the stick path.
mode=0; hook(&c);
assert(takes==before && raw_x==42 && !input_direct_mouse_active()); mode=1;
patched=0; hook(&c);
assert(takes==before && !input_direct_mouse_active() && !input_direct_mouse_available()); patched=1;
raw_x=0; hook(&c); assert(input_direct_mouse_active());
now+=150000001; assert(!input_direct_mouse_active());
puts("direct mouse 1.1.1: register contract, displaced store and load, equal turn at every frame rate and stick speed passed");
puts("direct mouse 1.1.1: single count, sensitivity, stick kept, idle bit, reset pause, capture and mode checks passed");
}
'''
INPUT_HARNESS = r'''
#include <assert.h>
#include "input.c"
@@ -216,6 +300,11 @@ InputMap input_map;
// Other input entry points are linked by PE unwind metadata; reaching them is outside this script-only test.
void ikb_cursor_update(void) { abort(); }
void ikb_menu_closed(void) { abort(); }
// The gamepad thread's (sdl_enter, probe_gamepad): not reached by a script.
void plat_wait_ns(uint64_t ns) { (void)ns; abort(); }
int plat_mutex_trylock(PlatMutex *m) { (void)m; abort(); }
int cfg_setenv(const char *name, const char *value) { (void)name; (void)value; abort(); }
const char *SDL_GetGamepadName(SDL_Gamepad *g) { (void)g; abort(); }
int cfg_bool(const char *key) { (void)key; abort(); }
int input_menu_gate(int *h,int open,uint64_t *b,int32_t s[4]) { (void)h;(void)open;(void)b;(void)s;abort(); }
int input_overlay_gate(int *h,uint64_t *b) { (void)h;(void)b;abort(); }
@@ -292,12 +381,13 @@ def main():
folder = Path(tmp)
(folder / 'patches.h').write_text('#define RT_PATCH_PAGE 0x7003adf000ull\n')
source = folder / 'check.c'
source.write_text(HARNESS)
exe = folder / ('check.exe' if os.name == 'nt' else 'check')
link = [] if os.name == 'nt' else ['-lm']
subprocess.run([args.cc, '-O2', '-Wall', '-Wextra', '-I', str(folder), '-I', str(ROOT/'runtime'),
str(source), *link, '-o', str(exe)], check=True)
subprocess.run([str(exe)], check=True)
for harness in (HARNESS, OOT_HARNESS):
source.write_text(harness)
subprocess.run([args.cc, '-O2', '-Wall', '-Wextra', '-I', str(folder), '-I', str(ROOT/'runtime'),
str(source), *link, '-o', str(exe)], check=True)
subprocess.run([str(exe)], check=True)
if args.deps:
source.write_text(INPUT_HARNESS)
script = folder / 'input.txt'