// Direct mouse input for NX2navi 1.4.3. The camera controller's input update (main+0x874c1c) turns HID stick floats // into a radial dead zone, clamps the radius to one and applies StickExponent. The mouse enters after all three, // at main+0x874cb4, before the game's X/Y inversion and input-activity flags. It never becomes a HID stick value. // Free orbit removes that sample again before the camera's additional vertical square/velocity filters and adds // raw-count angular deltas at its spherical-angle output instead. Object manipulation has a separate post-curve hook. // The ordinary stick's result stays, so a controller still uses the game's own response. // // This is deliberately not a render-camera override: the game remains responsible for aiming, camera collision, // lock-on and scripted cameras. Which of its other modes consume this camera input needs in-game verification. #include #include #include #include #include "cfg.h" #include "host.h" #include "input.h" #include "input_wsys.h" #include "platform/platform.h" #if __has_include("patches.h") #include "patches.h" #endif static int direct_mode(void) { // cfg_value: unset is the settings table's default (direct), as the menu shows it return !strcmp(cfg_value("NX2NAVI_MOUSE_MODE"), "direct") && rt_patch_enabled(rt_patch_set("mouse_direct")); } static uint64_t last_update; static uint64_t hand_update; static int accepts_mouse; static int hand_active(uint64_t now) { uint64_t last = __atomic_load_n(&hand_update, __ATOMIC_ACQUIRE); return last && now - last < 150000000ull; } int input_direct_mouse_active(void) { uint64_t last = __atomic_load_n(&last_update, __ATOMIC_ACQUIRE); return direct_mode() && (__atomic_load_n(&accepts_mouse, __ATOMIC_RELAXED) || hand_active(plat_now_ns())) && last && plat_now_ns() - last < 150000000ull; } 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; cursor[1] -= dy * (double)sensitivity / 200; double length = hypot(cursor[0], cursor[1]); if (length > 1) { cursor[0] /= length; cursor[1] /= length; } out[0] = (int32_t)lrint(cursor[0] * 32767); out[1] = (int32_t)lrint(cursor[1] * 32767); } void input_direct_mouse_row(int32_t dx, float sensitivity, uint64_t now, InputMouseRow *row, int32_t out[2]) { const float step_counts = 32; out[0] = out[1] = 0; if (!(sensitivity > 0)) { *row = (InputMouseRow){0}; return; } if (dx) { // Reversing direction cancels any remainder from the previous drag. Bound a fast flick to one pending step; // a stopped mouse never starts another pulse, regardless of how often HID is polled. if ((dx < 0 && row->distance > 0) || (dx > 0 && row->distance < 0)) row->distance = 0; row->distance = fmaxf(-2 * step_counts, fminf(2 * step_counts, row->distance + dx * sensitivity)); } if (row->direction) { if (now < row->pulse_until && (!dx || (dx > 0 ? 1 : -1) == row->direction)) { out[0] = row->direction * 32767; return; } row->direction = 0; row->neutral_until = now + 32000000ull; return; // publish neutral even when a slow frame skipped the pulse's deadline } if (!dx || now < row->neutral_until || fabsf(row->distance) < step_counts) return; row->direction = row->distance > 0 ? 1 : -1; row->distance = fmodf(row->distance, step_counts); row->pulse_until = now + 32000000ull; out[0] = row->direction * 32767; } #ifdef RT_PATCH_PAGE #define HOST_MOUSE_DIRECT (RT_PATCH_PAGE + 0xfd0) #define HOST_MOUSE_ACTIVITY (RT_PATCH_PAGE + 0xfc0) #define HOST_MOUSE_ORBIT_BEGIN (RT_PATCH_PAGE + 0xfb0) #define HOST_MOUSE_ORBIT_ANGLES (RT_PATCH_PAGE + 0xfa0) #define HOST_MOUSE_HAND (RT_PATCH_PAGE + 0xf90) static _Thread_local uint64_t mouse_input_object; typedef struct { uint64_t object, generation, when; float pad[2], motion[2], angle[2]; // post-inversion controller input, mouse input, mouse yaw/pitch in degrees int camera_enabled; } MouseSample; static MouseSample sample; static PlatMutex sample_lock = PLAT_MUTEX_INIT; static _Thread_local struct { uint64_t object; MouseSample mouse; } orbit; typedef struct { uint64_t object, generation, input; float pitch, original_target; int hold_pitch, target_saved; } OrbitTrack; static OrbitTrack orbit_seen[16]; // sample_lock held. Camera objects can coexist while the game blends between their outputs. static OrbitTrack *orbit_track(uint64_t object) { int oldest = 0; for (int i = 0; i < 16; i++) { if (orbit_seen[i].object == object) return &orbit_seen[i]; if (orbit_seen[i].generation < orbit_seen[oldest].generation) oldest = i; } orbit_seen[oldest] = (OrbitTrack){.object = object}; return &orbit_seen[oldest]; } static int orbit_sample_once(uint64_t object, uint64_t generation) { plat_mutex_lock(&sample_lock); OrbitTrack *track = orbit_track(object); int fresh = track->generation != generation; if (fresh) track->generation = generation; plat_mutex_unlock(&sample_lock); return fresh; } static float guest_float(uint64_t at) { float f; memcpy(&f, (void *)(uintptr_t)at, sizeof f); return f; } static void guest_float_store(uint64_t at, float f) { memcpy((void *)(uintptr_t)at, &f, sizeof f); } static uint64_t guest_pointer(uint64_t at) { uint64_t p; memcpy(&p, (void *)(uintptr_t)at, sizeof p); return p; } static MouseSample sample_read(void) { plat_mutex_lock(&sample_lock); MouseSample m = sample; plat_mutex_unlock(&sample_lock); return m; } static float wrap_degrees(float a) { return remainderf(a, 360.0f); } // main+0x191d78 builds the pitch target from +0x15c and +0x11c; the orbit update filters +0x108 toward it and adds // (1 - +0x88) * +0x10c at output. Updating only +0x108 makes the camera spring back to the unchanged target. static float orbit_pitch_offset(uint64_t object) { return (1 - guest_float(object + 0x88)) * guest_float(object + 0x10c); } static void orbit_pitch_target(uint64_t object, float pitch) { guest_float_store(object + 0x15c, pitch - orbit_pitch_offset(object) - guest_float(object + 0x11c)); } static void orbit_pitch_release(OrbitTrack *track) { if (track->target_saved) guest_float_store(track->object + 0x15c, track->original_target); track->hold_pitch = track->target_saved = 0; } // A host call at an instruction site, not an AAPCS64 function replacement. Preserve every guest register except // s9/s10, which carry the processed camera input, and perform the displaced store of s9 at [x20 + 0x420]. static void camera_input(Cpu *c) { mouse_input_object = 0; uint32_t flags = (uint32_t)c->x[21]; int accept = !(flags & 1) && !input_direct_mouse_selector_active(); __atomic_store_n(&accepts_mouse, accept, __ATOMIC_RELAXED); __atomic_store_n(&last_update, plat_now_ns(), __ATOMIC_RELEASE); MouseSample next = {.object = c->x[20], .when = plat_now_ns(), .camera_enabled = accept}; memcpy(&next.pad[0], &c->v[9], sizeof(float)); memcpy(&next.pad[1], &c->v[10], sizeof(float)); if (flags & 2) next.pad[0] = -next.pad[0]; if (flags & 4) next.pad[1] = -next.pad[1]; if (direct_mode()) { int32_t dx, dy; input_direct_mouse_sample(!input_direct_mouse_selector_active() && (accept || hand_active(plat_now_ns())), &dx, &dy); float sensitivity = (float)cfg_double("NX2NAVI_MOUSE_SENS"); sensitivity = fminf(10, fmaxf(0, sensitivity)); // The game multiplies camera input by its 30 Hz frame ratio later. Divide by that exact ratio, rather // than wall-clock time: an equal distance of mouse motion then gives equal input at every frame rate. float ratio = 1; if (frame_step_realtime()) memcpy(&ratio, rt_patch_page(), sizeof ratio); else { double hz = frame_step_game_hz(); // frame generation's frames are not the game's if (hz > 0) ratio = (float)(30 / hz); } if (isfinite(ratio) && ratio > 0 && sensitivity > 0 && (dx || dy)) { if (accept) mouse_input_object = c->x[20]; float x, y; memcpy(&x, &c->v[9], sizeof x); memcpy(&y, &c->v[10], sizeof y); float scale = 0.01f * sensitivity / ratio; next.motion[0] = dx * scale * (flags & 2 ? -1 : 1); next.motion[1] = -dy * scale * (flags & 4 ? -1 : 1); next.angle[0] = -dx * 0.08f * sensitivity * (flags & 2 ? -1 : 1); next.angle[1] = dy * 0.08f * sensitivity * (flags & 4 ? -1 : 1); if (accept) { x += dx * scale; y -= dy * scale; } uint32_t xb, yb; memcpy(&xb, &x, sizeof xb); memcpy(&yb, &y, sizeof yb); c->v[9] = xb; c->v[10] = yb; static int logged; if (!logged) { logged = 1; rt_log("mouse: direct camera input after the stick dead zone, clamp and exponent"); } } } plat_mutex_lock(&sample_lock); next.generation = sample.generation + 1; sample = next; plat_mutex_unlock(&sample_lock); uint32_t original = (uint32_t)c->v[9]; memcpy((void *)(uintptr_t)(c->x[20] + 0x420), &original, sizeof original); } // Free camera has another signed-square curve for its vertical axis, plus mode-dependent velocity filters. // Remove only this mouse sample before that controller runs. At its final spherical-angle output, add degrees // directly and move its persistent pitch/yaw by the same amounts so the next update starts at the new orientation. // The controller still handles its gamepad input, following, collision and focus normally. static void orbit_begin(Cpu *c) { orbit.object = 0; uint64_t object = c->x[0]; if (direct_mode()) { MouseSample m = sample_read(); uint64_t input = guest_pointer(object + 0x20); if (m.object == input && m.camera_enabled) { // The hooks take a step when the mouse moved in it, or while the mouse holds the pitch. A step of the // gamepad alone (no mouse motion, no hold: a player who never moved the mouse) keeps the game's input. int moved = m.motion[0] != 0 || m.motion[1] != 0, take; uint64_t held = guest_pointer(input + 0x148); uint8_t override; memcpy(&override, (void *)(uintptr_t)(input + 0x15c), 1); plat_mutex_lock(&sample_lock); OrbitTrack *track = orbit_track(object); if (track->input != input) track->hold_pitch = track->target_saved = 0; if (m.pad[1] != 0 || (held & 0x20) || (override & 0x40)) orbit_pitch_release(track); take = moved || track->hold_pitch; if (take) track->input = input; if (track->hold_pitch) orbit_pitch_target(object, track->pitch); plat_mutex_unlock(&sample_lock); if (moved) { // the mouse's part of this step's stick goes to the final angles instead guest_float_store(input + 0x420, m.pad[0]); guest_float_store(input + 0x424, m.pad[1]); } if (take) orbit.object = object, orbit.mouse = m; } } else { plat_mutex_lock(&sample_lock); orbit_pitch_release(orbit_track(object)); plat_mutex_unlock(&sample_lock); } c->x[8] = guest_pointer(object + 8); // displaced ldr x8, [x0, #8] } static void orbit_angles(Cpu *c) { uint64_t object = c->x[20]; if (orbit.object == object) { float yaw_delta = orbit.mouse.angle[0], pitch_delta = orbit.mouse.angle[1]; int fresh = plat_now_ns() - orbit.mouse.when < 150000000ull && orbit_sample_once(object, orbit.mouse.generation); if (!fresh) yaw_delta = pitch_delta = 0; plat_mutex_lock(&sample_lock); OrbitTrack *track = orbit_track(object); if (track->hold_pitch || yaw_delta || pitch_delta) { float pitch = guest_float(c->sp + 0x44), yaw = guest_float(c->sp + 0x48); float low = guest_float(object + 0xf8), high = guest_float(object + 0xfc); if (!isfinite(low) || !isfinite(high) || low >= high) low = -89.9f, high = 89.9f; low = fmaxf(-89.9f, low); high = fminf(89.9f, high); // The game can recalculate its pitch after orbit_begin (following, mode changes and another filter). // Integrate from our last requested angle, then correct the final output even on stationary frames. float base = track->hold_pitch ? track->pitch : pitch; float new_pitch = fminf(high, fmaxf(low, base + pitch_delta)); guest_float_store(c->sp + 0x44, new_pitch); guest_float_store(c->sp + 0x48, wrap_degrees(yaw + yaw_delta)); guest_float_store(object + 0x108, wrap_degrees(new_pitch - orbit_pitch_offset(object))); guest_float_store(object + 0x120, wrap_degrees(guest_float(object + 0x120) + yaw_delta)); if (!track->target_saved) track->original_target = guest_float(object + 0x15c), track->target_saved = 1; track->pitch = new_pitch; track->input = orbit.mouse.object; track->hold_pitch = 1; orbit_pitch_target(object, new_pitch); static int logged; if (!logged) { logged = 1; rt_log("mouse: free-camera angles directly, 0.08 degrees per count at sensitivity 1, equal X/Y"); } } plat_mutex_unlock(&sample_lock); orbit.object = 0; } uint32_t bits; memcpy(&bits, (const void *)(uintptr_t)(c->sp + 0x44), sizeof bits); c->v[12] = bits; // displaced ldr s12, [sp, #0x44] } // Object manipulation reads the player's manipulation input rather than the camera's processed stick. Insert the mouse // after that controller's per-axis power curves (main+0x1d59460), preserving the keyboard/controller contribution. static void hand_input(Cpu *c) { __atomic_store_n(&hand_update, plat_now_ns(), __ATOMIC_RELEASE); static _Thread_local uint64_t object, generation; if (direct_mode()) { MouseSample m = sample_read(); if (m.generation != generation || object != c->x[19]) { float x = guest_float(c->sp + 0x60), y; memcpy(&y, &c->v[15], sizeof y); x += m.motion[0]; y += m.motion[1]; guest_float_store(c->sp + 0x60, x); uint32_t bits; memcpy(&bits, &y, sizeof bits); c->v[15] = bits; object = c->x[19]; generation = m.generation; static int logged; if (!logged && (m.motion[0] || m.motion[1])) { logged = 1; rt_log("mouse: held-object input after the manipulation controller's power curves"); } } } uint32_t original = (uint32_t)c->x[8]; memcpy((void *)(uintptr_t)(c->sp + 0x48), &original, sizeof original); } // The game also uses a 10% activity threshold after processing the stick. A one-count mouse movement must count // as active, even when its displacement is smaller than that threshold. The site is the displaced ldp d9, d8, [sp, #8] // just before the update's last `str w8, [x20, #0x418]`: w8 holds the flags then (bit 0 raw input, bit 4 any input, // bits 3 and 5 processed input below the threshold), so they are changed in w8; a write to +0x418 would be overwritten // by that store. No flag is changed for controller-only input. static void camera_activity(Cpu *c) { if (mouse_input_object && mouse_input_object == c->x[20]) { uint32_t flags = (uint32_t)c->x[8]; c->x[8] = (flags | 0x11u) & ~0x28u; mouse_input_object = 0; } uint64_t d9, d8; memcpy(&d9, (const void *)(uintptr_t)(c->sp + 8), sizeof d9); memcpy(&d8, (const void *)(uintptr_t)(c->sp + 16), sizeof d8); c->v[9] = d9; 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); rt_register_host(HOST_MOUSE_ORBIT_ANGLES, orbit_angles); rt_register_host(HOST_MOUSE_HAND, hand_input); } #endif