mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
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:
commit
184221f032
9 files changed
+1010
-53
No files matched your search
+63
-12
@@ -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.
|
||||
|
||||
@@ -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
@@ -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},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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 ∶ }
|
||||
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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user