Restore native camera distance after aiming with direct mouse input

This commit is contained in:
null15564 committed 2026-11-07 05:55:00 +07:00
1 parent eda417fd1d
commit 2a937319b4
3 files changed
+125 -28

No files matched your search

+2
View File
@@ -111,6 +111,8 @@ site mouse_direct main 874cb4 bd042289 94c9acc7
site mouse_direct main 874d7c 6d40a3e9 94c9ac91
site mouse_direct main 18e444 f9400408 94e546db
site mouse_direct main 18f108 bd4047ec 94e543a6
# Keep native camera-distance easing when direct mouse orbit bypasses the stick integrators.
site mouse_direct main 18ea34 b9418a88 94e5454f # orbit state: ldr w8, [x20, #0x188]
site mouse_direct main 1d59460 b9004be8 94761acc
# The aiming, scope, ability, horse and Purah Pad camera cameras: per axis, the float load the stick turn is computed
# from (the gamepad's part only) and the load of the angle it is added to (plus the mouse's degrees), each a bl to page
+49 -16
View File
@@ -119,6 +119,7 @@ void input_direct_mouse_row(int32_t dx, float sensitivity, uint64_t now, InputMo
#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)
#define HOST_MOUSE_ORBIT_STATE (RT_PATCH_PAGE + 0xf70)
static _Thread_local uint64_t mouse_input_object;
// Where a game version keeps what the hooks read and write (recomp/patches-v143.txt, patches-v121.txt). The camera
@@ -162,8 +163,8 @@ static void sample_mark_taken(uint64_t generation) {
static _Thread_local struct { uint64_t object; MouseSample mouse; } orbit;
typedef struct {
uint64_t object, generation, input, seen;
float pitch, original_target;
int hold_pitch, target_saved, resume;
float pitch;
int hold_pitch, resume;
} OrbitTrack;
static OrbitTrack orbit_seen[16];
@@ -268,19 +269,17 @@ static uint32_t player_state_logged(void) {
return f;
}
// 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.
// The orbit output adds (1 - +0x88) * +0x10c to its filtered pitch at +0x108.
// Hold the final angle in orbit_angles. Do not overwrite the configured target
// at +0x15c: main+0x191534 also uses it as the reference for camera distance.
// Replacing that reference with the held pitch changes the native return from
// aiming and collision close-ups, even though no distance field is written.
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 = track->resume = 0;
track->hold_pitch = track->resume = 0;
}
// The pitch the camera has now (the filtered pitch plus its vertical offset), as orbit_angles leaves it
@@ -288,8 +287,8 @@ static float orbit_pitch_now(uint64_t object) {
return wrap_degrees(guest_float(object + 0x108) + orbit_pitch_offset(object));
}
// The camera stays where it is now: the mouse holds the pitch the camera has (orbit_begin writes its target), so
// neither the game's default target nor the mouse's target from before an aim or a lock-on pulls it back.
// Resume from the pitch the camera reached during the handoff. orbit_angles
// holds this final angle without replacing the native configuration reference.
static void orbit_pitch_settle(OrbitTrack *track) {
track->pitch = orbit_pitch_now(track->object);
track->hold_pitch = 1, track->resume = 0;
@@ -471,7 +470,7 @@ static void orbit_begin(Cpu *c) {
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 = track->resume = 0;
if (track->input != input) track->hold_pitch = track->resume = 0;
uint64_t now = plat_now_ns();
// A gamepad's vertical input gives the pitch back to the game's own target. The game's lock-on or override
// (aiming) drives the camera itself: the mouse lets go without restoring the target it had before, and
@@ -484,7 +483,6 @@ static void orbit_begin(Cpu *c) {
track->seen = now;
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]);
@@ -500,6 +498,42 @@ static void orbit_begin(Cpu *c) {
c->x[8] = guest_pointer(object + 8); // displaced ldr x8, [x0, #8]
}
// The native manual-orbit state (4) integrates both pitch and distance from
// the stick. Direct mouse orbit bypasses those integrators, so entering that
// state during an aim return can latch the close-up's radius indefinitely.
// Select the native automatic-distance state (3) at the existing state decision
// point, before its transition initializer runs (main+0x18ea34, 1.4.3).
// This retains the game's own radius curve, transition easing and collision.
static void orbit_state(Cpu *c) {
uint64_t object = c->x[20];
uint32_t override;
memcpy(&override, (void *)(uintptr_t)(object + 0x188), sizeof override);
if (rt_game_version == 143 && direct_mode() && (uint32_t)c->x[10] == 4 && !override) {
MouseSample m = sample_read();
uint64_t input = guest_pointer(object + 0x20);
if (m.object == input && m.camera_enabled && plat_now_ns() - m.when < 150000000ull &&
m.pad[0] == 0 && m.pad[1] == 0 &&
!(input_buttons_last() & ((1ull << B_ZR) | (1ull << B_R)))) {
uint64_t held = guest_pointer(input + 0x148);
uint8_t flags;
memcpy(&flags, (void *)(uintptr_t)(input + 0x15c), 1);
plat_mutex_lock(&sample_lock);
OrbitTrack *track = orbit_track(object);
int owned = track->input == input && track->hold_pitch;
plat_mutex_unlock(&sample_lock);
if (owned && !(held & 0x20) && !(flags & 0x40)) {
c->x[10] = 3;
static int logged;
if (!logged) {
logged = 1;
rt_log("mouse: native distance transition retained for direct orbit");
}
}
}
}
c->x[8] = override; // displaced ldr w8, [x20, #0x188]
}
// The final pitch and yaw are on the stack (1.4.3 sp + 0x44 / 0x48, 1.2.1 sp + 0x3c / 0x40)
static void orbit_angles(Cpu *c) {
const MouseLayout *ml = mouse_layout();
@@ -526,9 +560,7 @@ static void orbit_angles(Cpu *c) {
guest_float_store(yaw_at, 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;
@@ -1062,6 +1094,7 @@ __attribute__((constructor)) static void mouse_direct_register(void) {
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);
if (rt_game_version == 143) rt_register_host(HOST_MOUSE_ORBIT_STATE, orbit_state);
rt_register_host(HOST_MOUSE_ORBIT_ANGLES, orbit_angles);
rt_register_host(HOST_MOUSE_HAND, hand_input);
rt_register_host(HOST_MOUSE_LOAD, camera_load);
+74 -12
View File
@@ -18,7 +18,7 @@ const int rt_game_version = TEST_GAME_VERSION;
static int raw_x, raw_y, takes, captured=1, foreground=1, allowed=1, mode=1, patched=1, selecting;
static float ratio=0.5f;
static uint64_t now=1000000000;
static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook, load_hook;
static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook, load_hook, state_hook;
const char *cfg_get(const char *key) { (void)key; return mode ? "direct" : "stick"; }
const char *cfg_value(const char *key) { return cfg_get(key); }
double cfg_double(const char *key) { (void)key; return 1; }
@@ -40,13 +40,14 @@ void *rt_patch_page(void) { return &ratio; }
void rt_register_host(uint64_t addr, GuestFn fn) {
if(addr==HOST_MOUSE_DIRECT) hook=fn; else if(addr==HOST_MOUSE_ACTIVITY) activity_hook=fn;
else if(addr==HOST_MOUSE_ORBIT_BEGIN) begin_hook=fn; else if(addr==HOST_MOUSE_ORBIT_ANGLES) angles_hook=fn;
else if(addr==HOST_MOUSE_HAND) hand_hook=fn; else if(addr==HOST_MOUSE_LOAD) load_hook=fn; else abort();
else if(addr==HOST_MOUSE_ORBIT_STATE) state_hook=fn; else if(addr==HOST_MOUSE_HAND) hand_hook=fn; else if(addr==HOST_MOUSE_LOAD) load_hook=fn; else abort();
}
void plat_mutex_lock(PlatMutex *m) { (void)m; }
void plat_mutex_unlock(PlatMutex *m) { (void)m; }
void rt_log(const char *fmt, ...) { (void)fmt; }
float cam_speed_applied(void) { return 1; }
uint64_t input_buttons_last(void) { return 0; }
static uint64_t buttons;
uint64_t input_buttons_last(void) { return buttons; }
int mem_guest_readable(uint64_t a, uint64_t n) { (void)a; (void)n; return 0; } // no player: on-foot gains
static float scalar(__uint128_t v) { float f; memcpy(&f,&v,4); return f; }
#if TEST_GAME_VERSION == 143
@@ -109,6 +110,7 @@ int main(void) {
uint64_t input_address=(uintptr_t)target; memcpy(camera+0x20,&input_address,8);
for(int rate=0;rate<3;rate++) {
memset(orbit_seen,0,sizeof orbit_seen);
guest_float_store((uintptr_t)camera+0x15c,0);
ratio=30/rates[rate]; c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[9]=c.v[10]=0;
raw_x=100; raw_y=-50; hook(&c);
guest_float_store((uintptr_t)camera+0x108,30); guest_float_store((uintptr_t)camera+0x120,40);
@@ -118,16 +120,16 @@ int main(void) {
c.x[20]=(uintptr_t)camera; c.sp=(uintptr_t)frame; angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-(30-50*DEG_PER_COUNT))<0.0001f);
assert(fabsf(guest_float((uintptr_t)frame+0x48)-(40-100*DEG_PER_COUNT))<0.0001f);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-(30-50*DEG_PER_COUNT))<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==0);
c.x[0]=(uintptr_t)camera; begin_hook(&c); angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x48)-(40-100*DEG_PER_COUNT))<0.0001f); // one sample cannot repeat next update
// The next input frame is stationary and the game's parameter update rewrites the old default target.
// Restoring the mouse's desired target makes the camera filter stay at the new pitch, not spring to zero.
// The native target remains available for the distance controller.
// Holding pitch happens at the final output, after the native filter.
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; raw_x=raw_y=0; hook(&c);
guest_float_store((uintptr_t)camera+0x15c,0);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
float current=guest_float((uintptr_t)camera+0x108), goal=guest_float((uintptr_t)camera+0x15c);
assert(fabsf(goal-(30-50*DEG_PER_COUNT))<0.0001f && fabsf((current+goal)*0.5f-(30-50*DEG_PER_COUNT))<0.0001f);
assert(goal==0 && fabsf(current-(30-50*DEG_PER_COUNT))<0.0001f);
// A later game filter overwrites the pitch after the begin hook. Neither a stationary frame nor the next
// tiny mouse delta may integrate from that recentered result.
guest_float_store((uintptr_t)camera+0x108,0);
@@ -137,7 +139,7 @@ int main(void) {
guest_float_store((uintptr_t)frame+0x44,0);
c.x[20]=(uintptr_t)camera; angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-(30-50*DEG_PER_COUNT))<0.0001f);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-((30-50*DEG_PER_COUNT)-19))<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==0);
assert(fabsf(guest_float((uintptr_t)camera+0x108)-((30-50*DEG_PER_COUNT)-14))<0.0001f);
for(int i=0;i<100;i++) {
guest_float_store((uintptr_t)frame+0x44,0); angles_hook(&c);
@@ -161,16 +163,76 @@ int main(void) {
guest_float_store((uintptr_t)camera+0x108,3); // the game left the pitch at 3 + the offset 14
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-(17-14-5))<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==0);
guest_float_store((uintptr_t)frame+0x44,0);
c.x[20]=(uintptr_t)camera; angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-17)<0.0001f);
guest_float_store((uintptr_t)camera+0x15c,0); // the game's parameter update writes its default target
c.x[20]=(uintptr_t)target; hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-(17-14-5))<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==0);
// Aim/close-up handoff: the native camera can replace its parameter
// reference on every frame. Mouse pitch hold must never overwrite it,
// nor restore a stale reference when a controller takes over later.
for(int i=0;i<20;i++) {
float native_target=5+i, native_radius=2+i*0.1f;
guest_float_store((uintptr_t)camera+0x15c,native_target);
guest_float_store((uintptr_t)camera+0x164,native_radius);
guest_float_store((uintptr_t)camera+0x130,native_radius-0.5f);
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
assert(guest_float((uintptr_t)camera+0x15c)==native_target);
guest_float_store((uintptr_t)frame+0x44,0); // native pitch filter
c.x[20]=(uintptr_t)camera; angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-17)<0.0001f);
assert(guest_float((uintptr_t)camera+0x15c)==native_target);
assert(guest_float((uintptr_t)camera+0x164)==native_radius);
assert(guest_float((uintptr_t)camera+0x130)==native_radius-0.5f);
}
float pad_pitch=0.5f; c.x[20]=(uintptr_t)target;
memcpy(&c.v[10],&pad_pitch,4); hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
assert(guest_float((uintptr_t)camera+0x15c)==24);
c.v[10]=0;
memset(camera+0x88,0,4); memset(camera+0x10c,0,4); memset(camera+0x11c,0,4);
}
// The native manual-orbit branch keeps the distance it inherited from
// aiming. Mouse-owned orbit must select the native automatic-distance
// transition, before initialization; controller and scripted modes keep
// their own state. The host hook never writes a distance or position.
{
assert(state_hook);
c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[9]=c.v[10]=0;
raw_x=1; raw_y=0; hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
c.x[20]=(uintptr_t)camera; angles_hook(&c);
uint32_t manual=4, override=0;
memcpy(camera+0x180,&manual,4); memcpy(camera+0x188,&override,4);
c.x[10]=4; expected=c; state_hook(&c);
expected.x[8]=0; expected.x[10]=3;
assert(!memcmp(&expected,&c,sizeof c));
uint32_t unchanged; memcpy(&unchanged,camera+0x180,4); assert(unchanged==4);
// The game's transition initializer, rather than this hook, owns the state.
for(int state=0;state<=5;state++) if(state!=4) {
c.x[10]=state; state_hook(&c); assert(c.x[10]==(uint64_t)state);
}
buttons=1ull<<B_ZR; c.x[10]=4; state_hook(&c); assert(c.x[10]==4);
buttons=1ull<<B_R; state_hook(&c); assert(c.x[10]==4); buttons=0;
uint8_t forced=0x40; memcpy(target+0x15c,&forced,1);
state_hook(&c); assert(c.x[10]==4); forced=0; memcpy(target+0x15c,&forced,1);
uint64_t recenter=0x20; memcpy(target+0x148,&recenter,8);
state_hook(&c); assert(c.x[10]==4); recenter=0; memcpy(target+0x148,&recenter,8);
override=1; memcpy(camera+0x188,&override,4);
state_hook(&c); assert(c.x[10]==4 && c.x[8]==1);
override=0; memcpy(camera+0x188,&override,4);
// Any real controller input retains the native manual-distance behaviour.
c.x[20]=(uintptr_t)target; float pad=0.25f;
memcpy(&c.v[9],&pad,4); c.v[10]=0; hook(&c);
c.x[20]=(uintptr_t)camera; c.x[10]=4; state_hook(&c); assert(c.x[10]==4);
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
c.x[20]=(uintptr_t)camera; c.x[10]=4; now+=150000000ull;
state_hook(&c); assert(c.x[10]==4); now++;
}
// A gamepad-only step (no mouse motion, no pitch hold) leaves the camera input and the angles as the game made them.
memset(orbit_seen,0,sizeof orbit_seen);
c.x[20]=(uintptr_t)target; c.x[21]=0; raw_x=raw_y=0; c.v[9]=c.v[10]=0;
@@ -233,7 +295,7 @@ int main(void) {
assert(takes==before && raw_x==42); selecting=0; raw_x=0;
puts("direct mouse: register contract, routing, linearity, frame ratio and capture checks passed");
puts("direct mouse: final orbit angles, single-count motion, equal axes and held-object routing passed");
puts("direct mouse: persistent pitch target, radial cursor hold and gamepad-only steps untouched passed");
puts("direct mouse: pitch hold with native distance reference, radial cursor hold and gamepad-only steps untouched passed");
puts("direct mouse: final-pitch recenter rejection, explicit handoff and bounded quick-list steps passed");
// A shake: two direction changes of swings of at least 10 degrees within 0.35 s, then the stick follows the mouse
// side by side and lets go when it rests; nothing plays on after the mouse
@@ -437,7 +499,7 @@ int main(void) {
#else
// 1.2.1: the same hooks with that version's registers and stack slots (MouseLayout layout_121) and its load sites
int main(void) {
assert(hook && activity_hook && begin_hook && angles_hook && hand_hook && load_hook);
assert(hook && activity_hook && begin_hook && angles_hook && hand_hook && load_hook && !state_hook);
(void)takes;
Cpu c, expected;
unsigned char target[0x500]={0}, camera[0x300]={0}, hand[0x300]={0};