Merge PR #753 from Tempus3214: the direct mouse turns every player camera at one rate, the pitch no longer springs back after an aim, and a mouse shake detaches a fused part from what Ultrahand holds (fixes #734, #717)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valiantsin DzerakhandClaude Opus 5.5 committed 2026-11-05 17:32:19 +03:00
commit 184221f032
9 files changed
+1010 -53

No files matched your search

+63 -12
View File
@@ -110,26 +110,77 @@ bind_Y = pad:north, key:mouse_l
## Mouse camera
`mouse_mode = direct` (the default) turns the camera by the raw mouse motion on game 1.4.3, through hooks in the game's camera code (`runtime/mouse_direct.c`, the `mouse_direct` sites of `recomp/patches-v143.txt`). None of them changes a step of the gamepad alone. The free-orbit camera receives raw-count
angular increments at its final spherical-angle output: 0.08 degrees per count on both axes at sensitivity 1, with
no time multiplier, curve or carry into later frames. The controller's persistent pitch/yaw move with the output.
Mouse input is removed from its stick path first, so the camera's later vertical signed-square curve and velocity
filters cannot process it a second time. Other camera modes receive input after the radial dead zone, unit-circle
clamp and stick response exponent; Ultrahand has a separate hook after its manipulation input's power curves.
The game's camera inversion and gamepad response remain in effect. `mouse_sens` scales raw counts; its scale
differs from the stick-emulation mode. Start at 1.
angular increments at its final spherical-angle output: 70/1500 = 0.0467 degrees per count on both axes at
sensitivity 1 (the on-foot camera's full-stick 70 degrees a second for 1500 counts a second; about 24.5 cm a turn at
800 DPI), with no time multiplier, curve or carry into later frames. The controller's persistent pitch/yaw move with
the output. Mouse input is removed from its stick path first, so the camera's later vertical signed-square curve and
velocity filters cannot process it a second time.
The aiming camera (bow, throwing), the Purah Pad camera, the scope, the ability camera (Ultrahand, Fuse, Ascend,
Recall, Autobuild), the horse camera, lock-on, the bow drawn while locked on, climbing (walls and objects), the
waterfall's turn, shield surfing, the wakeboard and the dialogue cameras take the mouse the same way, at the same
degrees per count: each turns by
the stick times its own speeds and the frame time and adds that to a base angle, so the load of the stick it turns by
gets the gamepad's part only and the load of that base angle gets the mouse's degrees (`camera_load`, two sites per
axis per camera). Their speeds, time step, slow motion (bullet time) and, for the Purah Pad camera and the abilities,
the eased curve their pitch moves along do not touch the mouse; their own limits, collision and blends apply to the
result. Past the pitch limit of the scope or the ability camera the stick moves the camera's distance; the mouse's
motion past the limit keeps doing that through the stick. Recall's cursor is the ability camera's offset mode: the
camera keeps a direction, the stick moves the cursor across the screen (ReverseRecorderCursorSpeed, within
ReverseRecorderCursorRange) and the camera follows it by up to ReverseRecorderCameraRange (10 degrees across, 7.8 up).
The mouse moves the cursor so that its aim, the camera's follow plus the cursor's angle from the screen's centre (the
offset taken as the screen's half width or height at the camera's 50 degree view and aspect ratio), turns as many
degrees a count as the camera does elsewhere. The follow range is read from the game each step (until it has been read
once, the mouse is stick input there). The stick loads that only test for input keep the mouse in the stick. A step with mouse motion that no
direct hook took is logged (`mouse: step N with mouse motion took the stick path`, the first 30, then one in 600, with
the player's state), so the cameras still on stick input show in `logs/totk.log`.
Lock-on keeps pointing the camera at the target; the stick, and so the mouse, adds an offset around that direction,
which the game limits and lets return: the mouse's degrees are the step's turn the game adds to the offset and judges
the return by.
Shield surfing and the wakeboard turn their pitch by the square of the stick's y and move a second camera parameter
with it: their stick y keeps the mouse (that parameter and the camera's state follow it as before), the pitch gets
the degrees, and the pitch turn the game made from y is cut to the gamepad's share (p |p| / t |t|). Their stick x, and
the dialogue camera's (which is also its test for input), give a tiny value in the mouse's direction when the
gamepad's axis is at rest, so the camera still sees input without a stick turn.
Two inputs stay stick input: the waterfall's vertical axis moves the camera's height, not an angle, and the horse's
lock-on without a target (HorseLockOnEmpty) turns the stick's position into an offset that returns when the stick
does, which a mouse has no position for.
Those receive input after
the radial dead zone, unit-circle clamp and stick response exponent: x is 0.02 stick units a count over the frame
ratio (the stick turns yaw 70 x / 30 degrees a step) and y 70 / 51 times that, linear like their pitch (51 y / 30
degrees a step), so both axes turn by the same amount for the same motion. `stick_sens` (which scales the game's
stick turn rates) is divided out. While the player uses an ability and the ability camera's hooks do not take the
mouse, its part is scaled by 1 / 0.8 and 70 / (1.5 x 51), the ability camera's own stick scales.
The player's verbs are read every 50 ms from the player actor (the player root's GOT slot main+0x3969918, the
component array +0x228, its AS component +0x158 and the verbs of its five slots). Ultrahand has a separate hook after
its manipulation input's power curves; it gets the camera's stick value per count, at most three full tilts a step.
`mouse_aim_scale` (0.1 to 3, default 1) multiplies the mouse's motion while the bow is drawn (the verb UseBow or
DrawnBow with ZR held). The game's camera inversion and gamepad response remain in effect. `mouse_sens` scales raw
counts; its scale differs from the stick-emulation mode. Start at 1.
The Controls tab switches between *Direct* and *Stick emulation* (`mouse_mode = stick`).
The orbit camera keeps its desired pitch at the final angular output, including stationary frames, so a later game
filter cannot recenter it. The persistent filter state accounts for the camera's current vertical offset instead of
accumulating corrections. A gamepad vertical input or the game's lock-on/override releases that mouse target.
While L is held, mouse counts move the ability wheel's virtual cursor, which stays where it was when the mouse stops.
accumulating corrections. A gamepad vertical input releases that mouse target to the game's own. The game's
lock-on, recenter and override (aiming) drive the camera themselves: the mouse stops holding without restoring its
earlier target, and when they end (or after the camera went 100 ms without a step) it holds the pitch the camera has
then, so the camera does not spring back to a pitch from before the aim or to the game's default after a shot.
While the ability wheel is open (L held and its screen, ShortCutMainItem_00 or ShortCut_00, has the focus), mouse
counts move its virtual cursor, which stays where it was when the mouse stops. L or the D-pad held with no wheel or
strip on screen (L soothes the horse, turns menu tabs) leaves the mouse on the camera; with the menu cursor off
(`menu_cursor = 0`) the screens are not read and the buttons alone decide.
The horizontal quick lists (D-pad up/left/right: materials, shields and weapons) use separate 32 ms right-stick steps,
with at least 32 ms of neutral between them. At sensitivity 1, 32 horizontal counts request a step; a large flick
cannot queue continued scrolling after the mouse stops. Vertical movement is ignored in these lists. Switching or
releasing a selector resets its cursor/step state without sending the last movement to the camera. Selector input is
not consumed by the direct camera or Ultrahand hooks.
The mouse never stands in for a stick shake: to detach a fused part, press G (`bind_SHAKE`). A press swings the right
stick left and right for 0.8 s, holding the key keeps it swinging. The arrow keys or the gamepad's right stick shaken by
hand work too. A mouse gesture for it would also fire while a held object is turned back and forth.
To detach a fused part from what Ultrahand holds, shake the mouse left and right, or press G (`bind_SHAKE`). A press
swings the right stick left and right for 0.8 s, holding the key keeps it swinging. With `mouse_mode = direct` a shake
of the mouse while Ultrahand holds an object swings the right stick with it: once the swings (10 degrees of camera turn
or more each, about 214 counts at sensitivity 1) change direction twice within 0.35 s, the stick goes full tilt the
way the mouse moves, each side held at least two frames so the game reads it, and goes neutral as soon as the mouse
rests. Only your own swings reach the stick, so nothing keeps swinging after you stop; shake until the part comes off.
The camera hooks ignore that swing (the mouse turns the camera in degrees already). Turning a held object back and
forth at an ordinary pace is slower and does not count. The arrow keys or the gamepad's right stick shaken by hand
work too.
A step of the gamepad alone, with no mouse motion and no pitch the mouse holds, reaches the camera exactly as without
the hooks.
The mapping below is still used when the camera-input hook is unavailable or the game disables camera input.
+76
View File
@@ -112,6 +112,82 @@ site mouse_direct main 874d7c 6d40a3e9 94c9ac91
site mouse_direct main 18e444 f9400408 94e546db
site mouse_direct main 18f108 bd4047ec 94e543a6
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
# +0xf80, told apart by the return address (runtime/mouse_direct.c camera_load, load_sites). Every enclosing function
# saves x30 in its prologue, so the bl's link register write is harmless.
site mouse_direct main 23beb94 bd4426c1 945c84fb # Aiming: ldr s1, [x22, #0x424] (stick y)
site mouse_direct main 23bebc8 bd415e61 945c84ee # Aiming: ldr s1, [x19, #0x15c] (pitch)
site mouse_direct main 23bee2c bd4422c1 945c8455 # Aiming: ldr s1, [x22, #0x420] (stick x)
site mouse_direct main 23beeec bc5b83a1 945c8425 # Aiming: ldur s1, [x29, #-0x48] (yaw)
site mouse_direct main 23c0a10 bd4422e0 945c7d5c # AimingTelescope: ldr s0, [x23, #0x420] (stick x)
site mouse_direct main 23c0a1c bd4426eb 945c7d59 # AimingTelescope: ldr s11, [x23, #0x424] (stick y)
site mouse_direct main 23c0b00 bd416260 945c7d20 # AimingTelescope: ldr s0, [x19, #0x160] (pitch)
site mouse_direct main 23c0e80 bd405be1 945c7c40 # AimingTelescope: ldr s1, [sp, #0x58] (yaw)
site mouse_direct main 23d2eec bd4422e8 945c3425 # SpecialPower: ldr s8, [x23, #0x420] (stick x)
site mouse_direct main 23d2ef4 bd4426ee 945c3423 # SpecialPower: ldr s14, [x23, #0x424] (stick y)
site mouse_direct main 23d3158 bd416260 945c338a # SpecialPower: ldr s0, [x19, #0x160] (pitch)
site mouse_direct main 23d34cc bc5b03a1 945c32ad # SpecialPower: ldur s1, [x29, #-0x50] (yaw)
site mouse_direct main 23cb274 bd442288 945c5343 # Horse: ldr s8, [x20, #0x420] (stick x)
site mouse_direct main 23cb2a0 bd406a61 945c5338 # Horse: ldr s1, [x19, #0x68] (yaw)
site mouse_direct main 23cb3f8 bd4426a8 945c52e2 # Horse: ldr s8, [x21, #0x424] (stick y)
site mouse_direct main 23cb424 bd406661 945c52d7 # Horse: ldr s1, [x19, #0x64] (pitch)
site mouse_direct main 23d2f30 bd4422e8 945c3414 # SpecialPower offset mode: ldr s8, [x23, #0x420] (stick x)
site mouse_direct main 23d2f5c bd41da62 945c3409 # SpecialPower offset mode: ldr s2, [x19, #0x1d8] (offset x)
site mouse_direct main 23d2f6c bd4426e8 945c3405 # SpecialPower offset mode: ldr s8, [x23, #0x424] (stick y)
site mouse_direct main 23d2f98 bd41de62 945c33fa # SpecialPower offset mode: ldr s2, [x19, #0x1dc] (offset y)
site mouse_direct main 23d3210 bd400400 945c335c # SpecialPower: ldr s0, [x0, #0x4] (offset scale, read only)
site mouse_direct main 23c2890 bd4426ad 945c75bc # CameraFinder: ldr s13, [x21, #0x424] (stick y)
site mouse_direct main 23c28b4 bd405261 945c75b3 # CameraFinder: ldr s1, [x19, #0x50] (pitch)
site mouse_direct main 23c2934 bd4422ad 945c7593 # CameraFinder: ldr s13, [x21, #0x420] (stick x)
site mouse_direct main 23c2964 bd404be1 945c7587 # CameraFinder: ldr s1, [sp, #0x48] (yaw)
# The other player cameras (runtime/mouse_direct.c load_sites; a ROLE_WATCH load stays the game's and only names its
# mode in the log when the mouse was in it):
site mouse_direct main 23c3dd0 bd442301 945c706c # ClimbObj: ldr s1, [x24, #0x420]
site mouse_direct main 23c3dd8 bd409261 945c706a # ClimbObj: ldr s1, [x19, #0x90] (yaw)
site mouse_direct main 23c3d64 bd442701 945c7087 # ClimbObj: ldr s1, [x24, #0x424]
# ClimbObj's pitch is a pre-indexed load, ldr s1, [x22, #0x88]!, whose write-back the code after it needs (ldp and
# str through x22). A host call cannot write x22: the lifter keeps X19-X29 in locals across a call. So the mov before
# it adds the 0x88 instead (x22 = x19 + 0x88) and the hook loads [x22] with no write-back.
site mouse_direct main 23c3d5c aa1303f6 91022276 # ClimbObj: mov x22, x19 -> add x22, x19, #0x88
site mouse_direct main 23c3d70 bc488ec1 945c7084 # ClimbObj: ldr s1, [x22, #0x88]! (pitch), as ldr s1, [x22]
site mouse_direct main 23da694 bd4422c1 945c163b # Wakeboard: ldr s1, [x22, #0x420]
site mouse_direct main 23da698 bd4426c0 945c163a # Wakeboard: ldr s0, [x22, #0x424] (stick y, kept)
site mouse_direct main 23da840 bd40da60 945c15d0 # Wakeboard: ldr s0, [x19, #0xd8] (pitch)
site mouse_direct main 23da894 bd4017e0 945c15bb # Wakeboard: ldr s0, [sp, #0x14] (pitch turn, cut to the pad)
site mouse_direct main 23daed8 bd403fe9 945c142a # Wakeboard: ldr s9, [sp, #0x3c] (yaw)
site mouse_direct main 23dced8 bd4422cd 945c0c2a # WaterfallClimb: ldr s13, [x22, #0x420]
site mouse_direct main 23dd070 b85cc3a8 945c0bc4 # WaterfallClimb: ldur w8, [x29, #-0x34] (yaw)
site mouse_direct main 23dd008 bd4426c9 945c0bde # WaterfallClimb: ldr s9, [x22, #0x424]
site mouse_direct main 23d4b14 bd44228c 945c2d1b # Tail: ldr s12, [x20, #0x420]
site mouse_direct main 23d4b1c bd44268d 945c2d19 # Tail (climbing): ldr s13, [x20, #0x424] (stick y)
site mouse_direct main 23d4f34 bd40c260 945c2c13 # Tail (climbing): ldr s0, [x19, #0xc0] (yaw)
site mouse_direct main 23d5028 bd40b661 945c2bd6 # Tail (climbing): ldr s1, [x19, #0xb4] (pitch)
site mouse_direct main 23d756c bd442300 945c2285 # LockOn / Target: ldr s0, [x24, #0x420]
site mouse_direct main 23d7594 bd442301 945c227b # LockOn / Target: ldr s1, [x24, #0x420] (stick x)
site mouse_direct main 23d75b4 bd4023e0 945c2273 # LockOn / Target: ldr s0, [sp, #0x20] (yaw turn)
site mouse_direct main 23d75c4 bd4023e9 945c226f # LockOn / Target: ldr s9, [sp, #0x20] (yaw turn)
site mouse_direct main 23d7998 bd442701 945c217a # LockOn / Target: ldr s1, [x24, #0x424] (stick y)
site mouse_direct main 23d79b8 bd4023e0 945c2172 # LockOn / Target: ldr s0, [sp, #0x20] (pitch turn)
site mouse_direct main 23d79c8 bd4023ea 945c216e # LockOn / Target: ldr s10, [sp, #0x20] (pitch turn)
site mouse_direct main 23cecb4 bd4426a9 945c44b3 # LockOnAimingAt: ldr s9, [x21, #0x424] (stick y)
site mouse_direct main 23cecf8 bd419a61 945c44a2 # LockOnAimingAt: ldr s1, [x19, #0x198] (pitch offset)
site mouse_direct main 23ced0c bd4422a9 945c449d # LockOnAimingAt: ldr s9, [x21, #0x420] (stick x)
site mouse_direct main 23ced4c bd419e61 945c448d # LockOnAimingAt: ldr s1, [x19, #0x19c] (yaw offset)
site mouse_direct main 23cc4b4 bd44232b 945c4eb3 # HorseLockOnEmpty: ldr s11, [x25, #0x420]
site mouse_direct main 23c54d4 bd4422eb 945c6aab # EventMultiTalk: ldr s11, [x23, #0x420]
site mouse_direct main 23c542c bd4426eb 945c6ad5 # EventMultiTalk: ldr s11, [x23, #0x424] (stick y)
site mouse_direct main 23c546c bd405e61 945c6ac5 # EventMultiTalk: ldr s1, [x19, #0x5c] (pitch)
site mouse_direct main 23c5554 bd406661 945c6a8b # EventMultiTalk: ldr s1, [x19, #0x64] (yaw)
site mouse_direct main 23c90ac bd442709 945c5bb5 # EventTalkManual: ldr s9, [x24, #0x424] (stick y)
site mouse_direct main 23c90d8 bd407661 945c5baa # EventTalkManual: ldr s1, [x19, #0x74] (pitch)
site mouse_direct main 23c9140 bd442309 945c5b90 # EventTalkManual: ldr s9, [x24, #0x420] (stick x)
site mouse_direct main 23c9170 bd407e61 945c5b84 # EventTalkManual: ldr s1, [x19, #0x7c] (yaw)
site mouse_direct main 23d0100 bd4422c1 945c3fa0 # ShieldSurfing: ldr s1, [x22, #0x420] (stick x)
site mouse_direct main 23d0104 bd4426c0 945c3f9f # ShieldSurfing: ldr s0, [x22, #0x424] (stick y, kept)
site mouse_direct main 23d02ac bd40de60 945c3f35 # ShieldSurfing: ldr s0, [x19, #0xdc] (pitch)
site mouse_direct main 23d0a34 bd4013e0 945c3d53 # ShieldSurfing: ldr s0, [sp, #0x10] (pitch turn, cut to the pad)
site mouse_direct main 23d087c bd403fe9 945c3dc1 # ShieldSurfing: ldr s9, [sp, #0x3c] (yaw)
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
+3 -3
View File
@@ -45,7 +45,7 @@ static const char *const known[] = {
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE",
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_AIM_SCALE", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "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", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
@@ -85,7 +85,7 @@ static const struct { const char *key, *section; } sections[] = {
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
{"STICK_SENS", "input"},
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
{"MENU_CURSOR", "input"}, {"MOUSE_MODE", "input"}, {"MOUSE_SENS", "input"}, {"MOUSE_AIM_SCALE", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
{"BIND_PLUS", "input"}, {"BIND_MINUS", "input"}, {"BIND_LEFT", "input"}, {"BIND_UP", "input"},
@@ -150,7 +150,7 @@ static const CfgMeta metas[] = {
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", RESTART},
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},
{"MOUSE_MODE", "direct", "stick|direct", LIVE},
{"MOUSE_SENS", "1", "0..10", LIVE}, {"MENU_CURSOR", "1", "0|1", LIVE},
{"MOUSE_SENS", "1", "0..10", LIVE}, {"MOUSE_AIM_SCALE", "1", "0.1..3", LIVE}, {"MENU_CURSOR", "1", "0|1", LIVE},
{"WALK_KEY", "none", NULL, LIVE}, {"WALK_SCALE", "0.51", "0.2..0.9", LIVE},
{"MENU_LANG", "", "en|ru|pt-BR|zh-Hans", LIVE},
{"AMIIBO_DIR", "", NULL, LIVE},
+11
View File
@@ -608,6 +608,8 @@ int input_pad_stick_percent(double pct[2]) {
return ok;
}
static uint64_t g_buttons_last; // the merged buttons of the last poll (input_buttons_last)
static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
uint64_t b = 0;
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
@@ -646,6 +648,10 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
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;
// Only while the wheel or a strip is on screen (menu_cursor.c reads the focused screen): L also soothes the
// horse and turns menu tabs, the D-pad does other things too, and then the mouse stays on the camera.
// Before the screens were read (the menu cursor off) the buttons alone decide, as before.
if (selector && menu_cursor_shortcut() == 0) selector = 0;
mouse_camera(k, selector);
// On the map the left stick moves the cursor and the right one zooms: the mouse moves the cursor there,
// the scroll wheel zooms (map_zoom_stick). Elsewhere the wheel is unbound; its notches are dropped.
@@ -656,6 +662,8 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
if (zoom) pick(&sr[0], &sr[1], 0, zoom);
int32_t shake = shake_stick(input_map_held(down, B_SHAKE));
if (shake) sr[0] = shake, sr[1] = 0;
// A mouse shake (mouse_direct.c) moves the stick with the mouse, side by side, and lets go when it rests
else if (input_direct_mouse_shake()) sr[0] = input_direct_mouse_shake() * STICK_MAX, sr[1] = 0;
static uint32_t travel_seen;
uint32_t travel = ikb_mouse_travel();
if (travel - travel_seen >= MOUSE_USED) kbm_used = 1;
@@ -679,9 +687,12 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
if (overlay) menu_perf_cycle();
stick_bits(&b, sl[0], sl[1], B_SL_LEFT);
stick_bits(&b, sr[0], sr[1], B_SR_LEFT);
__atomic_store_n(&g_buttons_last, b, __ATOMIC_RELAXED);
*buttons = b; *lx = sl[0]; *ly = sl[1]; *rx = sr[0]; *ry = sr[1];
}
uint64_t input_buttons_last(void) { return __atomic_load_n(&g_buttons_last, __ATOMIC_RELAXED); }
// Text entry (swkbd_*.c): while it is open the game reads no input, as behind the keyboard applet; after it closes
// the game reads none until every button and stick direction is released, so the press that closed it stays there.
// g_block: 0 off, 1 text entry open, 2 closed, waiting for the release. The cursor is released while it is open.
+7
View File
@@ -23,6 +23,13 @@ int input_direct_mouse_selector_active(void);
// Consume direct mouse counts, including one-shot MOUSE events from an input script. enabled=0 discards scripted
// counts while leaving the real mouse for menu input. Live counts require cursor capture and foreground focus.
void input_direct_mouse_sample(int enabled, int32_t *dx, int32_t *dy);
// The buttons (B_* bits) of the last poll, keys and gamepad merged; any thread (mouse_direct.c: ZR for the bow's scale)
uint64_t input_buttons_last(void);
// mouse_direct.c: the right stick's side a mouse shake gives while Ultrahand holds an object (-1 / +1), 0 while the
// mouse does not shake; input.c puts it on the stick
int input_direct_mouse_shake(void);
// menu_cursor.c: the L ability wheel or a D-pad strip has the focus: 1 / 0, -1 with the menu cursor off (not read)
int menu_cursor_shortcut(void);
// menu_cursor.c: the pointer in the game's menus (1.4.3). poll: once per pad read, adds a click's A or B; pointer: a
// menu has the pointer, so the mouse camera's capture is released
void menu_cursor_poll(uint64_t *buttons);
+11
View File
@@ -385,3 +385,14 @@ void menu_cursor_poll(uint64_t *buttons) {
else pending_a = 1, pending_node = at, pending_until = now + 300;
}
}
// The L ability wheel or a D-pad strip (materials, weapons, shields, bows) has the focus: 1 / 0, -1 with the menu
// cursor off (the screens are not read). input.c asks while L or the D-pad is held, before it gives the mouse to their
// selectors: L also soothes the horse and turns menu tabs, the D-pad does other things too.
int menu_cursor_shortcut(void) {
if (!enabled()) return -1;
Focus f = focus_now();
char nm[48] = "";
if (f.screen) layout_name(f.screen, nm, sizeof nm);
return f.screen && (!strcmp(nm, "ShortCutMainItem_00") || !strcmp(nm, "ShortCut_00"));
}
+595 -14
View File
@@ -2,11 +2,13 @@
// 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. Ultrahand has a separate post-curve hook.
// raw-count angular deltas at its spherical-angle output instead. The aiming, scope, ability, horse and Purah Pad
// cameras do the same at the angle each integrates from (camera_load). Ultrahand has a separate post-curve hook.
// The other camera modes still take the mouse as stick, linear on both axes (mouse_stick_add).
// 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.
// lock-on and scripted cameras.
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
@@ -15,13 +17,36 @@
#include "cfg.h"
#include "host.h"
#include "input.h"
#include "input_map.h"
#include "input_wsys.h"
#include "layout.h"
#include "platform/platform.h"
#if __has_include("patches.h")
#include "patches.h"
#endif
// One turn rate for every camera, as a PC game has: degrees per raw count at sensitivity 1. The on-foot camera turns
// 70 degrees a second at a full stick, so this is a full stick's turn for 1500 counts a second. The stick rates and the
// ability camera's scales below are those Weeping of the Empire's engine/runtime/pc_mouse.cpp works with.
#define DEG_PER_COUNT (70.0f / 1500.0f)
// The stick turns yaw 70 x / 30 degrees per step at the 30 Hz frame ratio: a processed stick value of STICK_PER_COUNT a
// count turns yaw DEG_PER_COUNT in the cameras that still take the mouse as stick (mouse_stick_add).
#define STICK_PER_COUNT (DEG_PER_COUNT * 30.0f / 70.0f)
// Pitch turns 51 degrees a second a unit of y against yaw's 70 a unit of x (linearly in every camera but the free
// orbit, which squares y and takes degrees instead): the mouse's y is PITCH_GAIN times its x a count.
#define PITCH_GAIN (70.0f / 51.0f)
// The ability camera (ExecutePlayerCameraSpecialPower) turns the stored stick its own way: yaw x * 0.8 of on foot's
// rate and pitch y * 1.5 * 51, linear (no signed square). The mouse's part is scaled back to on foot's degrees a count:
// x / 0.8, and pitch linear at 70 / (1.5 * 51).
#define ABILITY_GAIN_X (1.0f / 0.8f)
#define ABILITY_GAIN_Y (70.0f / (1.5f * 51.0f))
// Ultrahand's held object moves by its post-curve input times its speed at a full tilt, as the camera turns by the
// stick: the mouse's part is the camera's stick value a count, so the object follows the hand as the camera would,
// at most HAND_MAX full tilts a step
#define HAND_PER_COUNT STICK_PER_COUNT
#define HAND_MAX 3.0f
static int direct_mode(void) {
// cfg_value: unset is the settings table's default (direct), as the menu shows it
return !strcmp(cfg_value("TOTK_MOUSE_MODE"), "direct") && rt_patch_enabled(rt_patch_set("mouse_direct"));
@@ -44,6 +69,11 @@ int input_direct_mouse_active(void) {
int input_direct_mouse_enabled(void) { return direct_mode(); }
// A shake of the mouse while Ultrahand holds an object (camera_input, mouse_shake_step): the right stick's side that
// follows it, -1 / +1, 0 while the mouse does not shake (input.c puts it on the right stick's x)
static volatile int32_t shake_side;
int input_direct_mouse_shake(void) { return __atomic_load_n(&shake_side, __ATOMIC_RELAXED); }
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;
@@ -92,14 +122,27 @@ 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;
uint32_t player; // player_state_logged's flags for this step (0 without mouse motion)
} MouseSample;
static MouseSample sample;
static PlatMutex sample_lock = PLAT_MUTEX_INIT;
// The generation of the last sample a direct hook turned a camera by (orbit_angles, load_stick); sample_lock. A sample
// with mouse motion that none took went to its camera as stick input only (mouse_stick_add): camera_input logs that.
static uint64_t sample_taken;
// The last sample with mouse motion a watched camera mode loaded as stick (ROLE_WATCH): its generation and mode
static uint64_t sample_watched;
static int sample_watched_mode = -1;
static const char *mode_name(int mode); // load_sites' camera modes, below
static void sample_mark_taken(uint64_t generation) {
plat_mutex_lock(&sample_lock);
sample_taken = generation;
plat_mutex_unlock(&sample_lock);
}
static _Thread_local struct { uint64_t object; MouseSample mouse; } orbit;
typedef struct {
uint64_t object, generation, input;
uint64_t object, generation, input, seen;
float pitch, original_target;
int hold_pitch, target_saved;
int hold_pitch, target_saved, resume;
} OrbitTrack;
static OrbitTrack orbit_seen[16];
@@ -134,6 +177,75 @@ static MouseSample sample_read(void) {
}
static float wrap_degrees(float a) { return remainderf(a, 360.0f); }
// ---- the player's state: using an ability, the bow in hand. The player's action verbs, read as Weeping of the
// Empire's engine/runtime/totk_player_state.cpp reads them, with its 1.4.3 placement (games/totk/patches/
// pins_v143.json, fields_v143.json): the player root's GOT slot main+0x3969918 (1.0.0 +0x4562308), its link +0x20
// (id +0x10, unit +8; the unit's id +0x48 and proc +0x40, the proc's state +0x24 not 7..11 while it is deleted), the
// actor's component array +0x228 (1.0.0 +0x220), its AS component +0x158, whose 5 slots (+0x1f0) hold the verb at +0x28
// (+0x4c when +0x28 is Invalid). Every pointer is checked before it is followed; any miss reads as "nothing".
#define PLAYER_ROOT_SLOT 0x3969918ull
#define MAIN_IMAGE_SIZE 0x3adf000ull
enum { VERB_USE_BOW = 59, VERB_USE_SPECIAL_POWER = 61, VERB_DRAWN_BOW = 205, VERB_INVALID = 284 };
enum { PLAYER_FOUND = 1, PLAYER_ABILITY = 2, PLAYER_BOW = 4 };
static int readable(uint64_t p, uint64_t n) {
return p >= 0x10000 && p < (1ull << 47) && !(p & 7) && mem_guest_readable(p, n);
}
static int32_t guest_s32(uint64_t at) { int32_t v; memcpy(&v, (void *)(uintptr_t)at, sizeof v); return v; }
static int game_object(uint64_t p, uint64_t n) { // readable, its vtable in main's image
if (!readable(p, n)) return 0;
uint64_t vt = guest_pointer(p);
return vt >= MOD_MAIN_BASE && vt < MOD_MAIN_BASE + MAIN_IMAGE_SIZE;
}
static uint32_t player_state(void) {
uint64_t slot = MOD_MAIN_BASE + PLAYER_ROOT_SLOT;
if (!readable(slot, 8)) return 0;
uint64_t var = guest_pointer(slot);
if (!readable(var, 8)) return 0;
uint64_t root = guest_pointer(var);
if (!readable(root, 0x38)) return 0;
uint64_t link = root + 0x20, unit = guest_pointer(link + 8);
int32_t id = guest_s32(link + 0x10);
if (id == -1 || !readable(unit, 0x50) || guest_s32(unit + 0x48) != id) return 0;
uint64_t proc = guest_pointer(unit + 0x40);
if (!game_object(proc, 0x230) || (uint32_t)(guest_s32(proc + 0x24) - 7) < 5) return 0;
uint64_t comps = guest_pointer(proc + 0x228);
if (!readable(comps, 0x160)) return 0;
uint64_t as = guest_pointer(comps + 0x158);
if (!game_object(as, 0x1f0 + 5 * 8)) return 0;
uint32_t f = PLAYER_FOUND;
for (int i = 0; i < 5; i++) {
uint64_t s = guest_pointer(as + 0x1f0 + 8ull * i);
if (!readable(s, 0x50)) continue;
int32_t v = guest_s32(s + 0x28);
if (v == VERB_INVALID) v = guest_s32(s + 0x4c);
if (v == VERB_USE_SPECIAL_POWER) f |= PLAYER_ABILITY;
if (v == VERB_USE_BOW || v == VERB_DRAWN_BOW) f |= PLAYER_BOW;
}
return f;
}
// The player's state for this camera step, read again every 50 ms (each read costs memory queries), and its changes in
// the log (the first reads, then each change, 40 lines)
static uint32_t player_state_logged(void) {
static uint32_t last = ~0u, f;
static uint64_t at;
static int lines;
uint64_t now = plat_now_ns();
if (at && now - at < 50000000ull) return f;
at = now;
f = player_state();
if (f != last && lines < 40) {
lines++;
rt_log("mouse: player %s%s%s", f & PLAYER_FOUND ? "found" : "not found",
f & PLAYER_ABILITY ? ", using an ability (ability camera gains)" : "",
f & PLAYER_BOW ? ", bow in hand" : "");
}
last = f;
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.
static float orbit_pitch_offset(uint64_t object) {
@@ -146,7 +258,80 @@ static void orbit_pitch_target(uint64_t object, float pitch) {
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;
track->hold_pitch = track->target_saved = track->resume = 0;
}
// The pitch the camera has now (the filtered pitch plus its vertical offset), as orbit_angles leaves it
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.
static void orbit_pitch_settle(OrbitTrack *track) {
track->pitch = orbit_pitch_now(track->object);
track->hold_pitch = 1, track->resume = 0;
}
// The mouse's part of the processed stick for dx / dy counts at k stick units a count. Only the free orbit turns its
// pitch by the signed square of y, and it never takes this value (orbit_begin removes it, orbit_angles adds degrees).
// The other cameras that read the stick turn both axes linearly, yaw at the orbit's rate and pitch at 51 / 70 of it
// (Horse, ClimbObj, WaterfallClimb: PlayerCamera +0xc9c for pitch against +0xc9c times the yaw factor for yaw), so y
// is linear with PITCH_GAIN. The cameras with load sites below take degrees instead; this is the fallback for the
// rest. The ability camera's linear scales are undone in its offset mode, which the load sites leave to the stick.
static void mouse_stick_add(int32_t dx, int32_t dy, float k, int ability, float *x, float *y) {
float v = -dy * k;
if (ability) {
*x += dx * k * ABILITY_GAIN_X;
*y += v * ABILITY_GAIN_Y;
} else {
*x += dx * k;
*y += v * PITCH_GAIN;
}
}
// The game detaches a fused part when the right stick swings left and right a few times; in direct mode the mouse
// never reaches that stick. Once the mouse shakes (horizontal swings of at least SHAKE_SWING_DEG of camera turn each,
// changing direction SHAKE_FOLLOW_TURNS times within SHAKE_WINDOW_NS of the first change), the right stick follows
// it: full tilt the way the mouse moves, each side held at least SHAKE_SIDE_NS (two frames at the game's 30 fps) so
// every side is read, neutral as soon as the mouse rests. Only the player's own swings reach the stick, nothing plays
// on after them; SHAKE_IDLE_NS without motion ends the shake. Turning a held object back and forth at an ordinary pace
// takes longer than the window, and a reversal after a shorter swing starts the count again.
#define SHAKE_SWING_DEG 10.0f
#define SHAKE_FOLLOW_TURNS 2
#define SHAKE_WINDOW_NS 350000000ull
#define SHAKE_SIDE_NS 66666667ull
#define SHAKE_IDLE_NS 150000000ull
typedef struct { int dir, turns, side; float swing; uint64_t first, side_from, moved; } MouseShake;
// deg: this step's horizontal motion in degrees of camera turn. Returns the right stick's side for this step: -1 / +1
// while it follows the mouse's shake, 0 otherwise.
static int mouse_shake_step(MouseShake *s, float deg, uint64_t now) {
int following = s->turns >= SHAKE_FOLLOW_TURNS;
if (!following && s->turns && now - s->first > SHAKE_WINDOW_NS) s->turns = 0;
if (deg != 0) {
int d = deg > 0 ? 1 : -1;
s->moved = now;
if (d == s->dir) {
s->swing += fabsf(deg);
} else {
if (following || (s->dir && s->swing >= SHAKE_SWING_DEG)) {
if (!s->turns) s->first = now;
s->turns++;
} else {
s->turns = 0;
}
s->dir = d, s->swing = fabsf(deg);
}
following = s->turns >= SHAKE_FOLLOW_TURNS;
} else if (following && now - s->moved >= SHAKE_IDLE_NS) { // the shake is over
*s = (MouseShake){0};
return 0;
}
if (!following) return 0;
if (s->side && now - s->side_from < SHAKE_SIDE_NS) return s->side; // a side is read before the next
int want = deg != 0 ? s->dir : 0; // neutral while the mouse rests
if (want != s->side) s->side = want, s->side_from = now;
return s->side;
}
// A host call at an instruction site, not an AAPCS64 function replacement. Preserve every guest register except
@@ -167,6 +352,28 @@ static void camera_input(Cpu *c) {
input_direct_mouse_sample(!input_direct_mouse_selector_active() && (accept || hand_active(plat_now_ns())), &dx, &dy);
float sensitivity = (float)cfg_double("TOTK_MOUSE_SENS");
sensitivity = fminf(10, fmaxf(0, sensitivity));
uint32_t player = (dx || dy) ? player_state_logged() : 0;
next.player = player;
int ability = (player & PLAYER_ABILITY) != 0;
// The bow drawn (in hand with ZR held): mouse_aim_scale times the mouse's motion on every path
if ((player & PLAYER_BOW) && (input_buttons_last() & (1ull << B_ZR)))
sensitivity *= (float)fminf(3, fmaxf(0.1, cfg_double("TOTK_MOUSE_AIM_SCALE")));
// Ultrahand holding an object: a shake of the mouse swings the right stick with it (mouse_shake_step)
static MouseShake shake;
static int last_side;
uint64_t step_now = plat_now_ns();
int side = 0;
if (hand_active(step_now)) side = mouse_shake_step(&shake, dx * DEG_PER_COUNT * sensitivity, step_now);
else shake = (MouseShake){0};
__atomic_store_n(&shake_side, side, __ATOMIC_RELAXED);
if (side) {
static int logged;
if (!logged) logged = 1, rt_log("mouse: a shake while Ultrahand holds an object swings the right stick with it");
}
// That swing is the mouse's own motion, which turns the camera in degrees already: the camera hooks take the
// right stick's x as 0 while it swings (the stick read of this step may still carry the last step's side)
if (side || last_side) next.pad[0] = 0;
last_side = side;
// 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;
@@ -180,12 +387,14 @@ static void camera_input(Cpu *c) {
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; }
float hand = HAND_PER_COUNT * sensitivity / ratio;
next.motion[0] = fminf(HAND_MAX, fmaxf(-HAND_MAX, dx * hand * (flags & 2 ? -1 : 1)));
next.motion[1] = fminf(HAND_MAX, fmaxf(-HAND_MAX, -dy * hand * (flags & 4 ? -1 : 1)));
next.angle[0] = -dx * DEG_PER_COUNT * sensitivity * (flags & 2 ? -1 : 1);
next.angle[1] = dy * DEG_PER_COUNT * sensitivity * (flags & 4 ? -1 : 1);
// stick_sens multiplies the game's stick turn rates (camera_speed.c): the mouse divides it out
if (accept) mouse_stick_add(dx, dy, STICK_PER_COUNT * sensitivity / (ratio * cam_speed_applied()), ability,
&x, &y);
uint32_t xb, yb;
memcpy(&xb, &x, sizeof xb);
memcpy(&yb, &y, sizeof yb);
@@ -199,9 +408,22 @@ static void camera_input(Cpu *c) {
}
}
plat_mutex_lock(&sample_lock);
MouseSample last = sample;
int untaken = last.camera_enabled && (last.angle[0] || last.angle[1]) && sample_taken != last.generation;
int watched = sample_watched == last.generation ? sample_watched_mode : -1;
next.generation = sample.generation + 1;
sample = next;
plat_mutex_unlock(&sample_lock);
// The camera modes still on stick input, as play reaches them: the first 30 steps, then one in 600
static unsigned untaken_count;
if (untaken && (++untaken_count <= 30 || untaken_count % 600 == 0)) {
uint32_t state = 0;
if (readable(last.object, 0x160)) memcpy(&state, (void *)(uintptr_t)(last.object + 0x15c), sizeof state);
rt_log("mouse: step %u with mouse motion took the stick path (no direct camera hook): camera %s, player "
"%s%s%s, input +0x15c %#x", untaken_count, watched >= 0 ? mode_name(watched) : "not a watched one",
last.player & PLAYER_FOUND ? "found" : "not found", last.player & PLAYER_ABILITY ? ", using an ability" : "",
last.player & PLAYER_BOW ? ", bow in hand" : "", state);
}
uint32_t original = (uint32_t)c->v[9];
memcpy((void *)(uintptr_t)(c->x[20] + 0x420), &original, sizeof original);
}
@@ -225,8 +447,17 @@ 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 = 0;
if (m.pad[1] != 0 || (held & 0x20) || (override & 0x40)) orbit_pitch_release(track);
if (track->input != input) track->hold_pitch = track->target_saved = 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
// once that ends (or after this camera went 100 ms without a step) settles on the pitch the camera has
if (m.pad[1] != 0) orbit_pitch_release(track);
else if ((held & 0x20) || (override & 0x40)) {
if (track->hold_pitch) track->hold_pitch = 0, track->resume = 1;
} else if (track->resume || (track->hold_pitch && now - track->seen > 100000000ull))
orbit_pitch_settle(track);
track->seen = now;
take = moved || track->hold_pitch;
if (take) track->input = input;
if (track->hold_pitch) orbit_pitch_target(object, track->pitch);
@@ -252,6 +483,7 @@ static void orbit_angles(Cpu *c) {
int fresh = plat_now_ns() - orbit.mouse.when < 150000000ull &&
orbit_sample_once(object, orbit.mouse.generation);
if (!fresh) yaw_delta = pitch_delta = 0;
if (yaw_delta || pitch_delta) sample_mark_taken(orbit.mouse.generation);
plat_mutex_lock(&sample_lock);
OrbitTrack *track = orbit_track(object);
if (track->hold_pitch || yaw_delta || pitch_delta) {
@@ -273,7 +505,8 @@ static void orbit_angles(Cpu *c) {
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");
rt_log("mouse: free-camera angles directly, %.4f degrees per count at sensitivity 1, equal X/Y",
DEG_PER_COUNT);
}
}
plat_mutex_unlock(&sample_lock);
@@ -312,6 +545,353 @@ static void hand_input(Cpu *c) {
memcpy((void *)(uintptr_t)(c->sp + 0x48), &original, sizeof original);
}
// ---- The other player cameras that turn by the stick: degrees at the angle they integrate from.
// Each of these camera modes (vtable slot 36 of its ExecutePlayerCamera* class, the per-step update) turns by the
// processed stick at input +0x420 (x) / +0x424 (y) times its own speeds and the frame time, and adds that to a base
// angle in degrees. Two loads per axis are replaced by a host call (the `mouse_direct` load sites of
// recomp/patches-v143.txt, all at page +0xf80; the return address tells them apart):
// stick: the load the turn is computed from gets the gamepad's part only (the sample's pad, post-inversion as the
// game stored it), so the mode's speeds, time step and slow motion never touch the mouse;
// base: the load of the angle the turn is added to gets the mouse's degrees added, DEG_PER_COUNT a count as the
// free orbit, once per sample. The game's clamps, collision and blends run on the result as usual.
// The stick loads that only test whether there is input (before the gyro branch) keep the mouse, so a mouse move
// still counts as stick input there. The stick load always runs before its base load; it leaves the degrees for that
// base (pending), so the two never disagree about a step.
// Aiming (bow, throwing; main+0x23be978): yaw = input +0x414 + turn ([x29-0x48]), pitch target = +0x15c + turn, the
// pitch follows its target at once (main+0x191970 with rate 1).
// AimingTelescope (the scope; main+0x23c08ac) and SpecialPower (abilities; main+0x23d2d40): yaw as
// Aiming ([sp+0x58], [x29-0x50]); pitch is +0x160 in degrees, taken to a position along an eased curve
// (main+0x23c11d8 / 0x23d38e0), moved by the stick and taken back (main+0x23c14dc / 0x23d3be4). Degrees added to
// +0x160 before that come back unchanged, so the curve no longer shapes the mouse. Beyond the pitch limits
// (+0x1ac / +0x1b0) the stick moves a second camera parameter instead; the mouse's part past the limit stays in
// the stick for that (LIMIT_160). That parameter is the camera's distance (the first of the (distance, pitch, yaw)
// main+0x2e2468 takes from the camera's look vector), not a turn.
// SpecialPower's offset mode (input +0x15c bit 8; SKIP_OFFSET on the sites above, ONLY_OFFSET below) is Recall's
// cursor: the camera keeps a direction (+0x54 yaw) and the stick moves the cursor, +0x1d8 += x *
// ReverseRecorderCursorSpeed * frame ratio and +0x1dc likewise, clamped to ReverseRecorderCursorRange; the camera
// follows it, yaw = +0x54 - +0x1d8 * P.x and pitch = ... - +0x1dc * P.y with P = ReverseRecorderCameraRange (the
// getters main+0x18a82a0, 0x18a824c, 0x18a8348; the step publishes the cursor at input +0x6c0). The cursor's base
// loads get the change that moves the aim, the camera's turn plus the cursor's angle from the screen's centre
// (o * P + atan(o * tan(half view angle)), the offset taken as the screen's half width / height), by the mouse's
// degrees: the world under the cursor moves DEG_PER_COUNT a count, as the camera turns elsewhere (recall_offset).
// P is read where the step loads P.y (ROLE_SCALE, both floats); until it was read once the mouse stays in the stick.
// CameraFinder (the Purah Pad camera; main+0x23c26c8): pitch = +0x50 - PlayerCamera +0xc9c * y * frame ratio,
// clamped; yaw = input +0x414 ([sp+0x48]) - the same with x.
// LockOn / Target (lock-on; main+0x23d7140): the game points the camera at the target and the stick adds an offset,
// yaw +0xbc and pitch +0xa4 += the step's turn ([sp+0x20], -PlayerCamera +0xc9c * stick * frame time), which the
// update then limits and lets return; the step's turn is read twice (ROLE_BASE_KEEP, then ROLE_BASE) and also
// decides that return, so it is the turn that gets the mouse's degrees. Where the camera points stays the game's.
// LockOnAimingAt (the bow drawn while locked on, BowLockOn / BowLockOn_Horse; its stick step main+0x23cec74):
// pitch offset +0x198 and yaw offset +0x19c += -PlayerCamera +0xc9c * stick * frame time, then clamped. The
// other bow cameras (Bow, Bow_Crouch, Bow_Zoom, Bow_Horse, Bow_Zoom_Horse) are Aiming.
// Tail (wall climbing, parameter set Climb; main+0x23d4950): the stick as angle and length; in its manual state
// (+0x16c == 4) yaw +0xc0 and pitch +0xb4 -= x / y * speed * frame ratio. ClimbObj (main+0x23c3a98): pitch +0x88
// (through x22, set by a patched mov) and yaw +0x90 likewise. WaterfallClimb (main+0x23dce00): yaw = the camera's
// yaw - x * speed * frame ratio, kept at [x29-0x34] and loaded into w8 (the base); its vertical axis moves the
// camera's height (ManualHeightMin..Max), not an angle, and stays stick input (watched).
// EventMultiTalk (main+0x23c531c) and EventTalkManual (main+0x23c8ffc): pitch / yaw fields -= stick * speed *
// frame ratio; MultiTalk's x load is also its input test (EPSILON).
// ShieldSurfing (main+0x23d0088) and Wakeboard (main+0x23da61c), one template: the stick as angle and length; yaw
// in the steering state = the camera's yaw ([sp+0x3c]) - x * speed * frame time; pitch = +0xdc / +0xd8 taken to an
// eased curve's position, moved by K * -y|y| ([sp+0x10] / [sp+0x14]) and taken back, and the same turn moves a
// second parameter. Stick y keeps the mouse (KEEP_STICK), the pitch base gets the degrees, the pitch's turn is cut
// to the pad's share (ROLE_SHARE); stick x is EPSILON, its length also choosing the steering state.
// HorseLockOnEmpty (ZL on a horse with no target) maps the stick's position to an offset that returns with it: no
// degrees a count to give, so it stays stick input (watched).
// Horse (main+0x23ca518, its helpers main+0x23cb1d4 and 0x23cb398): yaw = input +0x414 + turn (+0x68), pitch =
// input +0x410 + turn (+0x64), clamped to +0x5c..+0x60.
// Every one of them turns by -K x and -K y for a stick (x, y) and a positive K, as the free orbit does: the sample's
// angle[] (-x and -y in counts, times DEG_PER_COUNT) is added as it is.
#define HOST_MOUSE_LOAD (RT_PATCH_PAGE + 0xf80)
// ROLE_BASE_KEEP: a base load another base load follows. ROLE_SHARE: a turn the game computed from a stick load with
// KEEP_STICK, cut to the gamepad's share of it.
enum { ROLE_STICK, ROLE_BASE, ROLE_SCALE, ROLE_WATCH, ROLE_BASE_KEEP, ROLE_SHARE };
// Stick flags. EPSILON: the load is also the game's test for input, so with the gamepad's axis at rest and the mouse
// moving it gives +-1e-12 in the mouse's direction (input, no turn) instead of 0. KEEP_STICK: the value stays the
// game's, mouse and all (other parameters follow it); the turn it makes is cut to the pad's share at ROLE_SHARE.
enum { LIMIT_160 = 1, SKIP_OFFSET = 2, ONLY_OFFSET = 4, EPSILON = 8, KEEP_STICK = 16 };
enum {
MODE_AIM, MODE_SCOPE, MODE_ABILITY, MODE_HORSE, MODE_ABILITY_OFFSET, MODE_FINDER,
MODE_CLIMB, MODE_WAKEBOARD, MODE_WATERFALL, MODE_TAIL, MODE_LOCKON, MODE_LOCKON_AIM, MODE_HORSE_LOCKON, MODE_TALK,
MODE_TALK_MANUAL, MODE_SHIELD,
MODE_COUNT
};
static const char *const mode_names[MODE_COUNT] = {
"aiming (bow, throw)", "scope", "ability", "horse", "Recall cursor (ability offset mode)", "Purah Pad camera",
"climbing an object (ClimbObj)", "Wakeboard", "waterfall climbing (WaterfallClimb)", "climbing (Tail)",
"LockOn / Target", "bow lock-on (LockOnAimingAt)", "HorseLockOnEmpty", "dialogue (EventMultiTalk)",
"dialogue (EventTalkManual)", "shield surfing"};
static const char *mode_name(int mode) { return mode >= 0 && mode < MODE_COUNT ? mode_names[mode] : "?"; }
typedef struct {
uint32_t offset, insn; // main offset, the original load (ldr / ldur s<t>, [x<n>, #imm])
uint8_t mode, axis, role, input_reg, flags;
} LoadSite;
#define PAIR(m, a) ((m) * 2 + (a))
static const LoadSite load_sites[] = {
{0x23beb94, 0xbd4426c1, MODE_AIM, 1, ROLE_STICK, 22, 0}, // ldr s1, [x22, #0x424]
{0x23bebc8, 0xbd415e61, MODE_AIM, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x15c]
{0x23bee2c, 0xbd4422c1, MODE_AIM, 0, ROLE_STICK, 22, 0}, // ldr s1, [x22, #0x420]
{0x23beeec, 0xbc5b83a1, MODE_AIM, 0, ROLE_BASE, 0, 0}, // ldur s1, [x29, #-0x48]
{0x23c0a10, 0xbd4422e0, MODE_SCOPE, 0, ROLE_STICK, 23, 0}, // ldr s0, [x23, #0x420]
{0x23c0a1c, 0xbd4426eb, MODE_SCOPE, 1, ROLE_STICK, 23, LIMIT_160}, // ldr s11, [x23, #0x424]
{0x23c0b00, 0xbd416260, MODE_SCOPE, 1, ROLE_BASE, 0, 0}, // ldr s0, [x19, #0x160]
{0x23c0e80, 0xbd405be1, MODE_SCOPE, 0, ROLE_BASE, 0, 0}, // ldr s1, [sp, #0x58]
{0x23d2eec, 0xbd4422e8, MODE_ABILITY, 0, ROLE_STICK, 23, SKIP_OFFSET}, // ldr s8, [x23, #0x420]
{0x23d2ef4, 0xbd4426ee, MODE_ABILITY, 1, ROLE_STICK, 23, SKIP_OFFSET | LIMIT_160}, // ldr s14, [x23, #0x424]
{0x23d3158, 0xbd416260, MODE_ABILITY, 1, ROLE_BASE, 0, 0}, // ldr s0, [x19, #0x160]
{0x23d34cc, 0xbc5b03a1, MODE_ABILITY, 0, ROLE_BASE, 0, 0}, // ldur s1, [x29, #-0x50]
{0x23cb274, 0xbd442288, MODE_HORSE, 0, ROLE_STICK, 20, 0}, // ldr s8, [x20, #0x420]
{0x23cb2a0, 0xbd406a61, MODE_HORSE, 0, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x68]
{0x23cb3f8, 0xbd4426a8, MODE_HORSE, 1, ROLE_STICK, 21, 0}, // ldr s8, [x21, #0x424]
{0x23cb424, 0xbd406661, MODE_HORSE, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x64]
{0x23d2f30, 0xbd4422e8, MODE_ABILITY_OFFSET, 0, ROLE_STICK, 23, ONLY_OFFSET}, // ldr s8, [x23, #0x420]
{0x23d2f5c, 0xbd41da62, MODE_ABILITY_OFFSET, 0, ROLE_BASE, 0, 0}, // ldr s2, [x19, #0x1d8]
{0x23d2f6c, 0xbd4426e8, MODE_ABILITY_OFFSET, 1, ROLE_STICK, 23, ONLY_OFFSET}, // ldr s8, [x23, #0x424]
{0x23d2f98, 0xbd41de62, MODE_ABILITY_OFFSET, 1, ROLE_BASE, 0, 0}, // ldr s2, [x19, #0x1dc]
{0x23d3210, 0xbd400400, MODE_ABILITY_OFFSET, 1, ROLE_SCALE, 0, 0}, // ldr s0, [x0, #0x4] (P.y)
{0x23c2890, 0xbd4426ad, MODE_FINDER, 1, ROLE_STICK, 21, 0}, // ldr s13, [x21, #0x424]
{0x23c28b4, 0xbd405261, MODE_FINDER, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x50]
{0x23c2934, 0xbd4422ad, MODE_FINDER, 0, ROLE_STICK, 21, 0}, // ldr s13, [x21, #0x420]
{0x23c2964, 0xbd404be1, MODE_FINDER, 0, ROLE_BASE, 0, 0}, // ldr s1, [sp, #0x48]
{0x23c3dd0, 0xbd442301, MODE_CLIMB, 0, ROLE_STICK, 24, 0}, // ldr s1, [x24, #0x420]
{0x23c3dd8, 0xbd409261, MODE_CLIMB, 0, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x90] (yaw)
{0x23c3d64, 0xbd442701, MODE_CLIMB, 1, ROLE_STICK, 24, 0}, // ldr s1, [x24, #0x424]
// The game's pitch load is ldr s1, [x22, #0x88]! (bc488ec1). recomp/patches-v143.txt makes the mov before it
// set x22 = x19 + 0x88, so the hook performs it as the plain load below and writes no register but s1.
{0x23c3d70, 0xbd4002c1, MODE_CLIMB, 1, ROLE_BASE, 0, 0}, // ldr s1, [x22] (pitch +0x88)
{0x23da694, 0xbd4422c1, MODE_WAKEBOARD, 0, ROLE_STICK, 22, EPSILON}, // ldr s1, [x22, #0x420]
{0x23da698, 0xbd4426c0, MODE_WAKEBOARD, 1, ROLE_STICK, 22, KEEP_STICK}, // ldr s0, [x22, #0x424]
{0x23da840, 0xbd40da60, MODE_WAKEBOARD, 1, ROLE_BASE, 0, 0}, // ldr s0, [x19, #0xd8] (pitch)
{0x23da894, 0xbd4017e0, MODE_WAKEBOARD, 1, ROLE_SHARE, 0, 0}, // ldr s0, [sp, #0x14] (pitch turn)
{0x23daed8, 0xbd403fe9, MODE_WAKEBOARD, 0, ROLE_BASE, 0, 0}, // ldr s9, [sp, #0x3c] (yaw)
{0x23d0100, 0xbd4422c1, MODE_SHIELD, 0, ROLE_STICK, 22, EPSILON}, // ldr s1, [x22, #0x420]
{0x23d0104, 0xbd4426c0, MODE_SHIELD, 1, ROLE_STICK, 22, KEEP_STICK}, // ldr s0, [x22, #0x424]
{0x23d02ac, 0xbd40de60, MODE_SHIELD, 1, ROLE_BASE, 0, 0}, // ldr s0, [x19, #0xdc] (pitch)
{0x23d0a34, 0xbd4013e0, MODE_SHIELD, 1, ROLE_SHARE, 0, 0}, // ldr s0, [sp, #0x10] (pitch turn)
{0x23d087c, 0xbd403fe9, MODE_SHIELD, 0, ROLE_BASE, 0, 0}, // ldr s9, [sp, #0x3c] (yaw)
{0x23dced8, 0xbd4422cd, MODE_WATERFALL, 0, ROLE_STICK, 22, 0}, // ldr s13, [x22, #0x420]
{0x23dd070, 0xb85cc3a8, MODE_WATERFALL, 0, ROLE_BASE, 0, 0}, // ldur w8, [x29, #-0x34] (yaw, as bits)
{0x23dd008, 0xbd4426c9, MODE_WATERFALL, 1, ROLE_WATCH, 22, 0}, // ldr s9, [x22, #0x424] (height, stick)
{0x23d4b14, 0xbd44228c, MODE_TAIL, 0, ROLE_STICK, 20, 0}, // ldr s12, [x20, #0x420]
{0x23d4b1c, 0xbd44268d, MODE_TAIL, 1, ROLE_STICK, 20, 0}, // ldr s13, [x20, #0x424]
{0x23d4f34, 0xbd40c260, MODE_TAIL, 0, ROLE_BASE, 0, 0}, // ldr s0, [x19, #0xc0] (yaw)
{0x23d5028, 0xbd40b661, MODE_TAIL, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0xb4] (pitch)
{0x23d756c, 0xbd442300, MODE_LOCKON, 0, ROLE_WATCH, 24, 0}, // ldr s0, [x24, #0x420] (the input test)
{0x23d7594, 0xbd442301, MODE_LOCKON, 0, ROLE_STICK, 24, 0}, // ldr s1, [x24, #0x420]
{0x23d75b4, 0xbd4023e0, MODE_LOCKON, 0, ROLE_BASE_KEEP, 0, 0}, // ldr s0, [sp, #0x20] (the yaw turn)
{0x23d75c4, 0xbd4023e9, MODE_LOCKON, 0, ROLE_BASE, 0, 0}, // ldr s9, [sp, #0x20]
{0x23d7998, 0xbd442701, MODE_LOCKON, 1, ROLE_STICK, 24, 0}, // ldr s1, [x24, #0x424]
{0x23d79b8, 0xbd4023e0, MODE_LOCKON, 1, ROLE_BASE_KEEP, 0, 0}, // ldr s0, [sp, #0x20] (the pitch turn)
{0x23d79c8, 0xbd4023ea, MODE_LOCKON, 1, ROLE_BASE, 0, 0}, // ldr s10, [sp, #0x20]
{0x23cecb4, 0xbd4426a9, MODE_LOCKON_AIM, 1, ROLE_STICK, 21, 0}, // ldr s9, [x21, #0x424]
{0x23cecf8, 0xbd419a61, MODE_LOCKON_AIM, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x198] (pitch offset)
{0x23ced0c, 0xbd4422a9, MODE_LOCKON_AIM, 0, ROLE_STICK, 21, 0}, // ldr s9, [x21, #0x420]
{0x23ced4c, 0xbd419e61, MODE_LOCKON_AIM, 0, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x19c] (yaw offset)
{0x23cc4b4, 0xbd44232b, MODE_HORSE_LOCKON, 0, ROLE_WATCH, 25, 0}, // ldr s11, [x25, #0x420]
{0x23c542c, 0xbd4426eb, MODE_TALK, 1, ROLE_STICK, 23, 0}, // ldr s11, [x23, #0x424]
{0x23c546c, 0xbd405e61, MODE_TALK, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x5c] (pitch)
{0x23c54d4, 0xbd4422eb, MODE_TALK, 0, ROLE_STICK, 23, EPSILON}, // ldr s11, [x23, #0x420] (also the test)
{0x23c5554, 0xbd406661, MODE_TALK, 0, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x64] (yaw)
{0x23c90ac, 0xbd442709, MODE_TALK_MANUAL, 1, ROLE_STICK, 24, 0}, // ldr s9, [x24, #0x424]
{0x23c90d8, 0xbd407661, MODE_TALK_MANUAL, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x74] (pitch)
{0x23c9140, 0xbd442309, MODE_TALK_MANUAL, 0, ROLE_STICK, 24, 0}, // ldr s9, [x24, #0x420]
{0x23c9170, 0xbd407e61, MODE_TALK_MANUAL, 0, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x7c] (yaw)
};
#define LOAD_SITES (sizeof load_sites / sizeof load_sites[0])
// The original single-register 32-bit loads: LDR S (unsigned offset), LDUR S and LDUR W (a float's bits in a general
// register). Never a load with write-back: the hook is a call, and the lifted caller keeps X19-X29 in locals that a
// call does not reload (recomp/regs.py), so a base register written here would be lost. Their address:
static uint64_t load_address(const Cpu *c, uint32_t insn) {
unsigned n = (insn >> 5) & 31;
uint64_t base = n == 31 ? c->sp : c->x[n];
if ((insn & 0xffc00000u) == 0xbd400000u) return base + (((insn >> 10) & 0xfff) << 2);
int32_t imm9 = (int32_t)((insn >> 12) & 0x1ff);
if (imm9 & 0x100) imm9 -= 0x200;
return base + (int64_t)imm9;
}
static int load_to_general(uint32_t insn) { return (insn & 0xffe00c00u) == 0xb8400000u; }
// Degrees a stick load left for its base load, on this thread: per mode and axis, for one camera object
static _Thread_local struct { uint64_t object, generation; float degrees; } load_pending[MODE_COUNT * 2];
// A KEEP_STICK load's gamepad part and total, for its ROLE_SHARE load
static _Thread_local struct { uint64_t object; float pad, total; } load_share[MODE_COUNT * 2];
// The sample each camera object and axis took its degrees from: a sample turns a camera once
static struct { uint64_t object, generation[2]; } load_taken[16];
static int load_take_once(uint64_t object, int axis, uint64_t generation) {
plat_mutex_lock(&sample_lock);
int slot = 0;
for (int i = 0; i < 16; i++) {
if (load_taken[i].object == object) { slot = i; goto found; }
if (load_taken[i].generation[0] + load_taken[i].generation[1] <
load_taken[slot].generation[0] + load_taken[slot].generation[1]) slot = i;
}
load_taken[slot].object = object;
load_taken[slot].generation[0] = load_taken[slot].generation[1] = 0;
found:;
int fresh = load_taken[slot].generation[axis] != generation;
load_taken[slot].generation[axis] = generation;
plat_mutex_unlock(&sample_lock);
return fresh;
}
// The ability camera's offset scale P (degrees of turn per unit of offset, x and y), as its last step read it
static struct { uint64_t object; float scale[2]; } offset_scale;
// Recall's view: SpecialPower's Fovy (50 degrees, vertical; PlayerCameraAIParam SpecialPower) times the field-of-view
// factor and the camera's aspect ratio when those patch sets are on (resolution.c: page +60, +56), else 16:9. The tangent
// of the half view angle along an axis (0 across, 1 up).
#define RECALL_FOVY 50.0f
#define RECALL_PI 3.14159265f
static float recall_tan_half(int axis) {
static int fov_set = -2, aspect_set = -2;
if (fov_set == -2) fov_set = rt_patch_set("fov"), aspect_set = rt_patch_set("aspect");
float fovy = RECALL_FOVY, aspect = 16.0f / 9.0f, v;
const uint8_t *page = rt_patch_page();
if (page && fov_set >= 0 && rt_patch_enabled(fov_set)) {
memcpy(&v, page + 60, sizeof v);
if (v > 0.5f && v < 2) fovy *= v;
}
if (page && aspect_set >= 0 && rt_patch_enabled(aspect_set)) {
memcpy(&v, page + 56, sizeof v);
if (v > 0.5f && v < 5) aspect = v;
}
float t = tanf(fovy * 0.5f * RECALL_PI / 180);
return axis ? t : t * aspect;
}
// The change of Recall's cursor offset o that moves its aim by `degrees`. The aim is the camera's follow, o * range
// degrees, plus the cursor's angle from the centre, atan(o * t); it grows with o, so Newton's method converges from o.
static float recall_offset(float o, float range, float t, float degrees) {
const float k = 180 / RECALL_PI;
float target = o * range + atanf(o * t) * k + degrees, n = o;
for (int i = 0; i < 5; i++) {
float d = range + k * t / (1 + n * n * t * t);
n -= (n * range + atanf(n * t) * k - target) / d;
}
return isfinite(n) ? n - o : 0;
}
// The stick load: the gamepad's part of the axis, and the degrees for the base load. f is what the game stored.
static float load_stick(const LoadSite *s, Cpu *c, float f) {
uint64_t object = c->x[19], input = c->x[s->input_reg];
int pair = PAIR(s->mode, s->axis);
load_pending[pair].object = 0;
MouseSample m = sample_read();
if (m.object != input || !m.camera_enabled || plat_now_ns() - m.when >= 150000000ull) return f;
float scale = 1;
if (s->flags & (SKIP_OFFSET | ONLY_OFFSET)) {
uint32_t state;
memcpy(&state, (void *)(uintptr_t)(input + 0x15c), sizeof state);
int offset = (state & 0x100) != 0;
if ((s->flags & SKIP_OFFSET) && offset) return f;
if (s->flags & ONLY_OFFSET) {
if (!offset) return f;
plat_mutex_lock(&sample_lock);
scale = offset_scale.object == object ? offset_scale.scale[s->axis] : 0;
plat_mutex_unlock(&sample_lock);
if (!isfinite(scale) || fabsf(scale) < 1e-4f) { // not read yet: the stick, as the game turns it
static int logged;
if (!logged) logged = 1, rt_log("mouse: ability offset mode before its scale was read, stick input");
return f;
}
}
}
float mouse = f - m.pad[s->axis], degrees = m.angle[s->axis];
float out = m.pad[s->axis];
if (s->flags & KEEP_STICK) {
out = f;
load_share[pair].object = object, load_share[pair].pad = m.pad[s->axis], load_share[pair].total = f;
} else if ((s->flags & EPSILON) && out == 0 && mouse != 0) {
out = copysignf(1e-12f, mouse);
}
if (!degrees || !load_take_once(object, s->axis, m.generation)) degrees = 0;
// The offset turns the camera by -offset * P: an aim that turns as the sample's angle is -degrees of o's growth
if (s->flags & ONLY_OFFSET && degrees)
degrees = recall_offset(guest_float(object + (s->axis ? 0x1dc : 0x1d8)), scale, recall_tan_half(s->axis),
-degrees);
if ((s->flags & LIMIT_160) && degrees) {
float pitch = guest_float(object + 0x160), lo = guest_float(object + 0x1ac), hi = guest_float(object + 0x1b0);
if (isfinite(pitch) && isfinite(lo) && isfinite(hi)) {
if (lo > hi) { float t = lo; lo = hi; hi = t; }
float want = pitch + degrees, kept = fminf(hi, fmaxf(lo, want));
if (kept != want) { // past the limit: that share of the mouse's stick moves the camera's other parameter
out += mouse * ((want - kept) / degrees);
degrees = kept - pitch;
}
}
}
load_pending[pair].object = object;
load_pending[pair].generation = m.generation;
load_pending[pair].degrees = degrees;
return out;
}
// The base load: the angle plus the degrees its stick load left (ROLE_BASE_KEEP leaves them for the next base load)
static float load_base(const LoadSite *s, Cpu *c, float f) {
uint64_t object = c->x[19];
int pair = PAIR(s->mode, s->axis);
if (load_pending[pair].object != object) return f;
if (s->role != ROLE_BASE_KEEP) load_pending[pair].object = 0;
float degrees = load_pending[pair].degrees;
if (degrees) {
sample_mark_taken(load_pending[pair].generation);
static int logged[MODE_COUNT];
if (!logged[s->mode]) {
logged[s->mode] = 1;
rt_log("mouse: %s camera angles directly, %.4f degrees per count at sensitivity 1",
mode_names[s->mode], DEG_PER_COUNT);
}
}
return f + degrees;
}
// A turn the game made from a KEEP_STICK load's total t, as K * -t|t| (shield surfing, wakeboard: their pitch squares
// the stick), cut to the gamepad's p: K * -p|p|, the mouse's part being degrees at the base load
static float load_share_cut(const LoadSite *s, Cpu *c, float f) {
int pair = PAIR(s->mode, s->axis);
if (load_share[pair].object != c->x[19]) return f;
load_share[pair].object = 0;
float p = load_share[pair].pad, t = load_share[pair].total;
return t != 0 && isfinite(f) ? f * (p * fabsf(p)) / (t * fabsf(t)) : f;
}
// A host call in place of one float load (load_sites): performs the load, then gives the register the value above
static void camera_load(Cpu *c) {
uint64_t offset = c->x[30] - 4 - MOD_MAIN_BASE;
const LoadSite *s = NULL;
for (size_t i = 0; i < LOAD_SITES; i++)
if (load_sites[i].offset == offset) { s = &load_sites[i]; break; }
if (!s) {
rt_log("mouse: load hook called from main+%#llx, no such site", (unsigned long long)offset);
abort();
}
uint64_t at = load_address(c, s->insn);
float f = guest_float(at);
if (s->role == ROLE_SCALE) { // the ability camera's offset scale: P.y is this load, P.x the float before it
plat_mutex_lock(&sample_lock);
offset_scale.object = c->x[19];
offset_scale.scale[0] = guest_float(at - 4);
offset_scale.scale[1] = f;
plat_mutex_unlock(&sample_lock);
} else if (s->role == ROLE_WATCH) { // the value stays the game's; a load with the mouse in it names its mode
if (direct_mode()) {
plat_mutex_lock(&sample_lock);
if (sample.object == c->x[(s->insn >> 5) & 31] && sample.camera_enabled &&
(sample.angle[0] || sample.angle[1]))
sample_watched = sample.generation, sample_watched_mode = s->mode;
plat_mutex_unlock(&sample_lock);
}
} else if (direct_mode()) {
f = s->role == ROLE_STICK ? load_stick(s, c, f) : s->role == ROLE_SHARE ? load_share_cut(s, c, f)
: load_base(s, c, f);
}
uint32_t bits;
memcpy(&bits, &f, sizeof bits);
if (load_to_general(s->insn)) c->x[s->insn & 31] = bits; // a W load clears the upper half
else c->v[s->insn & 31] = bits; // a 32-bit load clears the rest of the vector register
}
// 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,
@@ -336,5 +916,6 @@ __attribute__((constructor)) static void mouse_direct_register(void) {
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);
rt_register_host(HOST_MOUSE_LOAD, camera_load);
}
#endif
+239 -23
View File
@@ -16,11 +16,11 @@ HARNESS = r'''
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;
static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook, load_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; }
int rt_patch_set(const char *s) { (void)s; return 0; }
int rt_patch_set(const char *s) { return strcmp(s,"mouse_direct") ? -1 : 0; } // no fov / aspect set: 16:9, 50 deg
int rt_patch_enabled(int s) { (void)s; return patched; }
int ikb_captured(void) { return captured; }
int ikb_foreground(void) { return foreground; }
@@ -38,11 +38,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 abort();
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; }
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; }
int main(void) {
assert(hook && activity_hook);
@@ -53,19 +56,29 @@ int main(void) {
expected=c;
raw_x=100; raw_y=-50;
hook(&c);
assert(fabsf(scalar(c.v[9])-2)<1e-6f);
assert(fabsf(scalar(c.v[10])-1)<1e-6f);
// x is STICK_PER_COUNT a count over the frame ratio; y PITCH_GAIN times that, for the cameras' linear pitch
assert(fabsf(scalar(c.v[9])-100*STICK_PER_COUNT/ratio)<1e-5f);
assert(fabsf(scalar(c.v[10])-PITCH_GAIN*50*STICK_PER_COUNT/ratio)<1e-5f);
float stored; memcpy(&stored,target+0x420,4); assert(stored==scalar(c.v[9]));
expected.v[9]=c.v[9]; expected.v[10]=c.v[10]; assert(!memcmp(&expected,&c,sizeof c));
assert(input_direct_mouse_active());
// The next read sees no repeated movement. Gamepad/keyboard input is preserved.
expected=c; hook(&c); assert(!memcmp(&expected,&c,sizeof c));
// Equal mouse distance produces equal integrated input at 30, 60 and 144 Hz; there is no unit-circle clamp.
// Equal mouse distance turns the camera equally at 30, 60 and 144 Hz, yaw by x and pitch linearly by y, and as
// many degrees a count on both axes; there is no unit-circle clamp.
float rates[]={30,60,144};
for(int i=0;i<3;i++) {
ratio=30/rates[i]; raw_x=10000; raw_y=10000; c.v[9]=c.v[10]=0; hook(&c);
assert(fabsf(scalar(c.v[9])*ratio-100)<0.001f);
assert(fabsf(scalar(c.v[10])*ratio+100)<0.001f);
float y=scalar(c.v[10]);
assert(fabsf(scalar(c.v[9])*ratio-10000*STICK_PER_COUNT)<0.001f);
assert(fabsf(y*ratio/PITCH_GAIN+10000*STICK_PER_COUNT)<0.001f);
}
// The ability camera's offset mode turns the stored stick at its own scales: they are undone.
{
float x=0, y=0; mouse_stick_add(100,-50,0.04f,1,&x,&y);
assert(fabsf(x-100*0.04f*ABILITY_GAIN_X)<1e-5f && fabsf(y-50*0.04f*ABILITY_GAIN_Y)<1e-5f);
x=y=0; mouse_stick_add(100,-50,0.04f,0,&x,&y);
assert(fabsf(x-4)<1e-5f && fabsf(y-PITCH_GAIN*2)<1e-5f);
}
// One count is not discarded by a dead zone.
ratio=0.5f; raw_x=1; raw_y=0; c.v[9]=c.v[10]=0; hook(&c); assert(scalar(c.v[9])>0);
@@ -100,18 +113,18 @@ int main(void) {
c.x[0]=(uintptr_t)camera; begin_hook(&c);
assert(guest_float((uintptr_t)target+0x420)==0 && guest_float((uintptr_t)target+0x424)==0);
c.x[20]=(uintptr_t)camera; c.sp=(uintptr_t)frame; angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-26)<0.0001f);
assert(fabsf(guest_float((uintptr_t)frame+0x48)-32)<0.0001f);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-26)<0.0001f);
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);
c.x[0]=(uintptr_t)camera; begin_hook(&c); angles_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x48)-32)<0.0001f); // one sample cannot repeat next update
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.
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-26)<0.0001f && fabsf((current+goal)*0.5f-26)<0.0001f);
assert(fabsf(goal-(30-50*DEG_PER_COUNT))<0.0001f && fabsf((current+goal)*0.5f-(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);
@@ -120,18 +133,18 @@ int main(void) {
guest_float_store((uintptr_t)camera+0x88,0.3f);
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)-26)<0.0001f);
assert(fabsf(guest_float((uintptr_t)camera+0x15c)-7)<0.0001f);
assert(fabsf(guest_float((uintptr_t)camera+0x108)-12)<0.0001f);
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(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);
assert(fabsf(guest_float((uintptr_t)camera+0x108)-12)<0.0001f);
assert(fabsf(guest_float((uintptr_t)camera+0x108)-((30-50*DEG_PER_COUNT)-14))<0.0001f);
}
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; raw_y=1; hook(&c);
c.x[0]=(uintptr_t)camera; begin_hook(&c);
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)-26.08f)<0.0001f);
assert(fabsf(guest_float((uintptr_t)frame+0x44)-((30-50*DEG_PER_COUNT)+DEG_PER_COUNT))<0.0001f);
// An explicit controller recenter still hands pitch back to the game.
uint64_t recenter=0x20; memcpy(target+0x148,&recenter,8);
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; hook(&c);
@@ -140,6 +153,19 @@ int main(void) {
c.x[20]=(uintptr_t)camera; angles_hook(&c);
assert(guest_float((uintptr_t)frame+0x44)==5);
memset(target+0x148,0,8);
// When the game lets go (the recenter or an aim ends), the mouse holds the pitch the camera has then: neither its
// target from before nor the game's default target pulls the camera back.
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);
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);
memset(camera+0x88,0,4); memset(camera+0x10c,0,4); memset(camera+0x11c,0,4);
}
// A gamepad-only step (no mouse motion, no pitch hold) leaves the camera input and the angles as the game made them.
@@ -163,14 +189,15 @@ int main(void) {
guest_float_store((uintptr_t)frame+0x48,guest_float((uintptr_t)camera+0x120));
angles_hook(&c);
}
assert(fabsf(guest_float((uintptr_t)camera+0x120)-32)<0.001f);
// A held object receives mouse deltas after its own curves, once per sample, including with camera input off.
assert(fabsf(guest_float((uintptr_t)camera+0x120)-(40-100*DEG_PER_COUNT))<0.001f);
// A held object receives mouse deltas after its own curves at the camera's stick rate, at most HAND_MAX full
// tilts a step, once per sample, including with camera input off.
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; raw_x=100; raw_y=50; ratio=0.5f; hook(&c);
c.x[19]=(uintptr_t)hand; c.x[8]=0x12345678; c.v[15]=0;
guest_float_store((uintptr_t)frame+0x60,0); hand_hook(&c);
assert(fabsf(guest_float((uintptr_t)frame+0x60)-2)<0.00001f);
assert(fabsf(scalar(c.v[15])+1)<0.00001f);
hand_hook(&c); assert(fabsf(scalar(c.v[15])+1)<0.00001f);
assert(100*HAND_PER_COUNT/ratio>HAND_MAX && fabsf(guest_float((uintptr_t)frame+0x60)-HAND_MAX)<0.00001f);
assert(fabsf(scalar(c.v[15])+50*HAND_PER_COUNT/ratio)<0.00001f);
hand_hook(&c); assert(fabsf(scalar(c.v[15])+50*HAND_PER_COUNT/ratio)<0.00001f);
c.x[21]=1; raw_x=5; raw_y=6; hook(&c); assert(raw_x==0 && raw_y==0 && input_direct_mouse_active());
// A radial selector retains its virtual cursor between mouse packets, including repeated HID polls.
float cursor[2]={0}; int32_t selector[2], held_selector[2];
@@ -205,6 +232,194 @@ int main(void) {
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: 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
{
MouseShake s={0}; uint64_t t=0;
assert(mouse_shake_step(&s,12,t+=100000000ull)==0);
assert(mouse_shake_step(&s,-12,t+=100000000ull)==0); // one change: not yet a shake
assert(mouse_shake_step(&s,12,t+=100000000ull)==1); // the second: the stick takes the mouse's side
assert(mouse_shake_step(&s,-12,t+=100000000ull)==-1); // and follows it
assert(mouse_shake_step(&s,0,t+=30000000ull)==-1); // a side is held two frames, so it is read
assert(mouse_shake_step(&s,0,t+=40000000ull)==0); // the mouse rests: neutral, no swing of its own
for(int i=0;i<10;i++) assert(mouse_shake_step(&s,0,t+=50000000ull)==0);
assert(!s.turns); // and the shake is over
// Fast reversals: a side never lasts less than two frames
s=(MouseShake){0}; t=0; int side=0, flips=0; uint64_t last=0, shortest=~0ull;
for(int i=0;i<20;i++) {
int now_side=mouse_shake_step(&s,(i&1)?-12.f:12.f,t+=30000000ull);
if(now_side!=side) { if(side&&now_side&&last) shortest=t-last<shortest?t-last:shortest; side=now_side; last=t; flips++; }
}
assert(flips>3 && shortest>=SHAKE_SIDE_NS);
// A swing made of several small steps counts as one
s=(MouseShake){0}; t=0; int got=0;
for(int i=0;i<8;i++) got|=mouse_shake_step(&s,(i/2&1)?-6.f:6.f,t+=50000000ull)!=0;
assert(got);
s=(MouseShake){0}; t=0; got=0; // the same swings 0.4 s apart: a held object turned back and forth
for(int i=0;i<6;i++) got|=mouse_shake_step(&s,(i&1)?-12.f:12.f,t+=400000000ull)!=0;
assert(!got);
s=(MouseShake){0}; t=0; got=0; // swings under 10 degrees
for(int i=0;i<8;i++) got|=mouse_shake_step(&s,(i&1)?-4.f:4.f,t+=50000000ull)!=0;
assert(!got);
}
// The aiming, scope, ability and horse cameras: the stick loads give the gamepad's part, the angle loads the
// mouse's degrees, once per sample
{
assert(load_hook);
unsigned char input[0x500]={0}, cam[0x200]={0}; float frame[0x20]={0};
float pad_x=0.25f, pad_y=-0.5f;
#define LOAD(site,reg) (c.x[30]=MOD_MAIN_BASE+(site)+4, load_hook(&c), scalar(c.v[reg]))
#define SAMPLE(dx,dy) do { c.x[20]=(uintptr_t)input; c.x[21]=0; c.v[9]=0; c.v[10]=0; \
memcpy(&c.v[9],&pad_x,4); memcpy(&c.v[10],&pad_y,4); raw_x=(dx); raw_y=(dy); hook(&c); \
guest_float_store((uintptr_t)input+0x424,scalar(c.v[10])); } while(0)
ratio=0.5f;
SAMPLE(100,-50);
float stick_x=guest_float((uintptr_t)input+0x420), stick_y=guest_float((uintptr_t)input+0x424);
assert(stick_x!=pad_x && stick_y!=pad_y); // the stick the game stored has the mouse in it
c.x[19]=(uintptr_t)cam; c.x[22]=(uintptr_t)input; c.x[29]=(uintptr_t)(frame+0x18);
guest_float_store((uintptr_t)cam+0x15c,10); frame[0x18-0x12]=40; // [x29-0x48]
// Aiming pitch: stick y is the pad's, the pitch target base gets dy counts of degrees
assert(LOAD(0x23beb94,1)==pad_y);
assert(fabsf(LOAD(0x23bebc8,1)-(10-50*DEG_PER_COUNT))<1e-5f);
assert(LOAD(0x23bebc8,1)==10); // a base load with no stick load before it is the game's own
assert(LOAD(0x23beb94,1)==pad_y && LOAD(0x23bebc8,1)==10); // the same sample turns the camera once
// Aiming yaw: the base yaw on the frame gets dx counts
assert(LOAD(0x23bee2c,1)==pad_x);
assert(fabsf(LOAD(0x23beeec,1)-(40-100*DEG_PER_COUNT))<1e-5f);
// Scope pitch near its lower limit: degrees up to the limit, the rest of the mouse's stick stays
SAMPLE(0,-50);
float mouse_y=guest_float((uintptr_t)input+0x424)-pad_y, want=-39-50*DEG_PER_COUNT;
guest_float_store((uintptr_t)cam+0x160,-39); guest_float_store((uintptr_t)cam+0x1ac,-40);
guest_float_store((uintptr_t)cam+0x1b0,40); c.x[23]=(uintptr_t)input;
float y=LOAD(0x23c0a1c,11);
assert(fabsf(y-(pad_y+mouse_y*((want+40)/(want+39))))<1e-5f);
assert(fabsf(LOAD(0x23c0b00,0)-(-40))<1e-5f);
// Inside the limits all of it is degrees
SAMPLE(0,-50); guest_float_store((uintptr_t)cam+0x160,0);
assert(LOAD(0x23c0a1c,11)==pad_y && fabsf(LOAD(0x23c0b00,0)+50*DEG_PER_COUNT)<1e-5f);
// The ability camera's offset mode (input +0x15c bit 8) keeps the mouse in the stick and adds no degrees
SAMPLE(100,0); uint32_t offset_mode=0x100; memcpy(input+0x15c,&offset_mode,4);
assert(LOAD(0x23d2eec,8)==guest_float((uintptr_t)input+0x420));
frame[0x18-0x14]=7; assert(LOAD(0x23d34cc,1)==7); // [x29-0x50]
offset_mode=0; memcpy(input+0x15c,&offset_mode,4);
// Horse: the input is x20 / x21; another camera's input or a stale sample is left alone
SAMPLE(100,-50); c.x[20]=(uintptr_t)input; c.x[21]=(uintptr_t)input;
guest_float_store((uintptr_t)cam+0x68,5); guest_float_store((uintptr_t)cam+0x64,6);
assert(LOAD(0x23cb274,8)==pad_x && fabsf(LOAD(0x23cb2a0,1)-(5-100*DEG_PER_COUNT))<1e-5f);
assert(LOAD(0x23cb3f8,8)==pad_y && fabsf(LOAD(0x23cb424,1)-(6-50*DEG_PER_COUNT))<1e-5f);
SAMPLE(100,-50); unsigned char other[0x500]; memcpy(other,input,sizeof other); c.x[20]=(uintptr_t)other;
assert(LOAD(0x23cb274,8)==guest_float((uintptr_t)other+0x420) && LOAD(0x23cb2a0,1)==5);
c.x[20]=(uintptr_t)input; now+=150000000ull;
assert(LOAD(0x23cb274,8)==guest_float((uintptr_t)input+0x420) && LOAD(0x23cb2a0,1)==5);
// The ability camera's offset mode (Recall's cursor): before its scale was read the mouse stays in the stick;
// then the offset moves so that the aim, o * P + atan(o * tan(half view angle)), moves by the mouse's degrees
now+=1; SAMPLE(100,-50); c.x[23]=(uintptr_t)input; offset_mode=0x100; memcpy(input+0x15c,&offset_mode,4);
guest_float_store((uintptr_t)cam+0x1d8,1); guest_float_store((uintptr_t)cam+0x1dc,0);
assert(LOAD(0x23d2f30,8)==guest_float((uintptr_t)input+0x420) && LOAD(0x23d2f5c,2)==1);
float scale_p[2]={2,4}; c.x[0]=(uintptr_t)scale_p;
assert(LOAD(0x23d3210,0)==4);
SAMPLE(100,-50); c.x[23]=(uintptr_t)input;
{
float ty=tanf(25*RECALL_PI/180), tx=ty*16/9, k=180/RECALL_PI;
#define AIM(o,r,t) ((o)*(r)+atanf((o)*(t))*k)
assert(LOAD(0x23d2f30,8)==pad_x);
float ox=LOAD(0x23d2f5c,2);
assert(fabsf(AIM(ox,2,tx)-AIM(1,2,tx)-100*DEG_PER_COUNT)<1e-3f);
assert(LOAD(0x23d2f6c,8)==pad_y);
float oy=LOAD(0x23d2f98,2);
assert(fabsf(AIM(oy,4,ty)-50*DEG_PER_COUNT)<1e-3f);
assert(ox>1 && ox-1<100*DEG_PER_COUNT/2 && oy>0 && oy<50*DEG_PER_COUNT/4); // slower than the camera alone
#undef AIM
}
// outside the offset mode these sites are the game's
offset_mode=0; memcpy(input+0x15c,&offset_mode,4); SAMPLE(100,-50); c.x[23]=(uintptr_t)input;
assert(LOAD(0x23d2f30,8)==guest_float((uintptr_t)input+0x420) && LOAD(0x23d2f5c,2)==1);
// The Purah Pad camera: pitch +0x50, yaw [sp+0x48]
uint64_t spbuf[0x10]={0}; uint64_t old_sp=c.sp; c.sp=(uintptr_t)spbuf;
SAMPLE(100,-50); c.x[21]=(uintptr_t)input;
guest_float_store((uintptr_t)cam+0x50,3); guest_float_store((uintptr_t)spbuf+0x48,9);
assert(LOAD(0x23c2890,13)==pad_y && fabsf(LOAD(0x23c28b4,1)-(3-50*DEG_PER_COUNT))<1e-5f);
assert(LOAD(0x23c2934,13)==pad_x && fabsf(LOAD(0x23c2964,1)-(9-100*DEG_PER_COUNT))<1e-5f);
c.sp=old_sp;
// Lock-on: the stick gives the pad's part, both reads of the step's turn ([sp+0x20]) get the mouse's degrees
{
uint64_t lock_sp[0x10]={0}, keep_sp=c.sp; c.sp=(uintptr_t)lock_sp;
SAMPLE(100,-50); c.x[24]=(uintptr_t)input; guest_float_store((uintptr_t)lock_sp+0x20,0.5f);
assert(LOAD(0x23d756c,0)==guest_float((uintptr_t)input+0x420)); // the input test keeps the mouse
assert(LOAD(0x23d7594,1)==pad_x);
assert(fabsf(LOAD(0x23d75b4,0)-(0.5f-100*DEG_PER_COUNT))<1e-5f);
assert(fabsf(LOAD(0x23d75c4,9)-(0.5f-100*DEG_PER_COUNT))<1e-5f);
assert(LOAD(0x23d75c4,9)==0.5f); // once both reads took them, the degrees are spent
assert(LOAD(0x23d7998,1)==pad_y);
assert(fabsf(LOAD(0x23d79b8,0)-(0.5f-50*DEG_PER_COUNT))<1e-5f);
assert(fabsf(LOAD(0x23d79c8,10)-(0.5f-50*DEG_PER_COUNT))<1e-5f);
c.sp=keep_sp;
}
// The bow drawn while locked on: the pad's stick, the offsets +0x198 (pitch) and +0x19c (yaw) get the degrees
SAMPLE(100,-50); c.x[21]=(uintptr_t)input;
guest_float_store((uintptr_t)cam+0x198,2); guest_float_store((uintptr_t)cam+0x19c,3);
assert(LOAD(0x23cecb4,9)==pad_y && fabsf(LOAD(0x23cecf8,1)-(2-50*DEG_PER_COUNT))<1e-5f);
assert(LOAD(0x23ced0c,9)==pad_x && fabsf(LOAD(0x23ced4c,1)-(3-100*DEG_PER_COUNT))<1e-5f);
assert(LOAD(0x23ced4c,1)==3); // the gyro path's join reads the offset with no stick load before it
// Climbing (Tail): the pad's stick on both loads, yaw +0xc0 and pitch +0xb4 get the degrees
SAMPLE(100,-50); c.x[20]=(uintptr_t)input;
guest_float_store((uintptr_t)cam+0xc0,7); guest_float_store((uintptr_t)cam+0xb4,8);
assert(LOAD(0x23d4b14,12)==pad_x && LOAD(0x23d4b1c,13)==pad_y);
assert(fabsf(LOAD(0x23d4f34,0)-(7-100*DEG_PER_COUNT))<1e-5f && fabsf(LOAD(0x23d5028,1)-(8-50*DEG_PER_COUNT))<1e-5f);
// Climbing an object: the patched mov has set x22 = x19 + 0x88; the pitch load writes no register but s1
SAMPLE(100,-50); c.x[24]=(uintptr_t)input; c.x[22]=(uintptr_t)cam+0x88;
guest_float_store((uintptr_t)cam+0x88,4); guest_float_store((uintptr_t)cam+0x90,5);
assert(LOAD(0x23c3d64,1)==pad_y && fabsf(LOAD(0x23c3d70,1)-(4-50*DEG_PER_COUNT))<1e-5f);
assert(c.x[22]==(uintptr_t)cam+0x88);
assert(LOAD(0x23c3dd0,1)==pad_x && fabsf(LOAD(0x23c3dd8,1)-(5-100*DEG_PER_COUNT))<1e-5f);
// Waterfall climbing: the yaw is loaded into w8 as bits; the height (stick y) stays the game's
{
float wf[0x20]={0}; uint64_t keep29=c.x[29]; c.x[29]=(uintptr_t)(wf+0x10);
SAMPLE(100,-50); c.x[22]=(uintptr_t)input; wf[0x10-0x0d]=11; // [x29-0x34]
assert(LOAD(0x23dced8,13)==pad_x);
c.x[30]=MOD_MAIN_BASE+0x23dd070+4; c.x[8]=~0ull; load_hook(&c);
float w; uint32_t wb=(uint32_t)c.x[8]; memcpy(&w,&wb,4);
assert(c.x[8]>>32==0 && fabsf(w-(11-100*DEG_PER_COUNT))<1e-5f);
assert(LOAD(0x23dd008,9)==guest_float((uintptr_t)input+0x424));
assert(sample_watched==sample.generation && sample_watched_mode==MODE_WATERFALL);
c.x[29]=keep29;
}
// Dialogue: the yaw stick load is also the input test, so with the pad's x at rest it gives a tiny value in the
// mouse's direction (input, no turn) and the yaw gets the degrees
{
float keep_x=pad_x; pad_x=0;
SAMPLE(100,-50); c.x[23]=(uintptr_t)input;
guest_float_store((uintptr_t)cam+0x5c,1); guest_float_store((uintptr_t)cam+0x64,2);
float tx=LOAD(0x23c54d4,11);
assert(tx>0 && tx<1e-9f && fabsf(LOAD(0x23c5554,1)-(2-100*DEG_PER_COUNT))<1e-5f);
assert(LOAD(0x23c542c,11)==pad_y && fabsf(LOAD(0x23c546c,1)-(1-50*DEG_PER_COUNT))<1e-5f);
SAMPLE(0,-50); c.x[23]=(uintptr_t)input; assert(LOAD(0x23c54d4,11)==0); // no mouse x: the pad's 0
pad_x=keep_x;
}
// Shield surfing: stick y keeps the mouse (other parameters follow it), the pitch +0xdc gets the degrees and the
// pitch turn the game made from y is cut to the pad's share (the turn squares y)
{
uint64_t ssp[0x10]={0}, keep_sp=c.sp; c.sp=(uintptr_t)ssp;
SAMPLE(100,-50); c.x[22]=(uintptr_t)input;
float total=guest_float((uintptr_t)input+0x424);
guest_float_store((uintptr_t)cam+0xdc,12); guest_float_store((uintptr_t)ssp+0x10,0.3f);
guest_float_store((uintptr_t)ssp+0x3c,20);
assert(LOAD(0x23d0100,1)==pad_x && LOAD(0x23d0104,0)==total);
assert(fabsf(LOAD(0x23d02ac,0)-(12-50*DEG_PER_COUNT))<1e-5f);
float cut=LOAD(0x23d0a34,0);
assert(fabsf(cut-0.3f*(pad_y*fabsf(pad_y))/(total*fabsf(total)))<1e-6f);
assert(LOAD(0x23d0a34,0)==0.3f); // once
assert(fabsf(LOAD(0x23d087c,9)-(20-100*DEG_PER_COUNT))<1e-5f);
c.sp=keep_sp;
}
// The stick mode leaves every load to the game
mode=0; c.x[20]=(uintptr_t)input; assert(LOAD(0x23cb274,8)==guest_float((uintptr_t)input+0x420)); mode=1;
#undef LOAD
#undef SAMPLE
}
puts("direct mouse: aiming, scope, ability, Recall cursor, horse, Purah Pad, lock-on, bow lock-on, climbing, waterfall, dialogue and shield cameras in degrees passed");
puts("direct mouse: one turn rate, linear pitch, ability gains and the hold after a handoff passed");
puts("direct mouse: a mouse shake followed side by side and released at rest, not a slow turn nor small swings passed");
}
'''
@@ -291,6 +506,7 @@ def main():
with tempfile.TemporaryDirectory(prefix='totk-mouse-') as tmp:
folder = Path(tmp)
(folder / 'patches.h').write_text('#define RT_PATCH_PAGE 0x7003adf000ull\n')
(folder / 'layout.h').write_text('#define MOD_MAIN_BASE 0x7000000000ull\n')
source = folder / 'check.c'
source.write_text(HARNESS)
exe = folder / ('check.exe' if os.name == 'nt' else 'check')
+5 -1
View File
@@ -309,8 +309,12 @@
# right click is B. On the map the mouse moves the map's cursor and the scroll wheel zooms. 0 leaves the menus and
# the map to the keys and the pad. Changes while the game runs.
# menu_cursor = 1
# 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). With mouse_mode = direct, 1 turns the
# camera 0.0467 degrees per count, the same in every camera mode (about 24.5 cm a turn at 800 DPI).
# mouse_sens = 1
# The mouse's camera motion times this while the bow is drawn (0.1 to 3; mouse_mode = direct, game 1.4.3), on top of
# the 0.0467 degrees per count every camera turns, the bow's included. Changes while the game runs.
# mouse_aim_scale = 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,
# none turns it off. 0.2 to 0.9, the default holds for anything else. Only the keyboard is scaled: the mouse camera and