mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Compare commits
No files matched your search
+2
-1
@@ -230,7 +230,8 @@ Before a commit:
|
||||
|
||||
A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/button_icons.py`) passes
|
||||
`.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game
|
||||
files); a change to the settings passes `totk.exe --probe-cfg`.
|
||||
files); a change to the settings passes `totk.exe --probe-cfg` and `python tests\settings_doc_check.py`, which fails
|
||||
when a setting is described neither in `totk.ini.example` nor in [settings.md](settings.md).
|
||||
|
||||
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
|
||||
change passes these, all run with `.venv`:
|
||||
|
||||
+68
-16
@@ -29,7 +29,7 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
|
||||
| Minus | Back | M | map |
|
||||
| Right stick shaken left and right | shake the right stick | G | detach a fused part from what Ultrahand holds |
|
||||
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel is unbound. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
|
||||
|
||||
## Xbox button prompts
|
||||
|
||||
@@ -109,31 +109,83 @@ 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.
|
||||
`mouse_mode = direct` (the default) turns the camera by the raw mouse motion on games 1.4.3 and 1.2.1, through hooks in the game's camera code (`runtime/mouse_direct.c`, the `mouse_direct` sites of `recomp/patches-v143.txt` and `recomp/patches-v121.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 (about
|
||||
14.3 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.0343 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 0.01 stick units a count over the frame ratio, 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, with the same meaning as in 0.3.10 (the free camera turns 0.08 degrees a count at 1, and now every other camera
|
||||
does too); its scale differs from the stick-emulation mode.
|
||||
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, 125 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.
|
||||
Games 1.1.0 and 1.2.1 have no hooks and keep stick emulation whatever `mouse_mode` says.
|
||||
Game 1.1.0 has no hooks and keeps stick emulation whatever `mouse_mode` says. On 1.2.1 `mouse_aim_scale` has no effect (the bow-in-hand state is not read there).
|
||||
|
||||
In stick-emulation mode, mouse motion (raw input) becomes right-stick deflection. Each presented frame takes the counts moved since the last frame, divides by the frame time and sets the deflection to `counts per second * TOTK_MOUSE_SENS / 3000` of full, held for that frame, so the turn rate does not depend on the frame rate. This is not mouselook: the game's stick curve and dead zone still apply, so very slow movements do nothing and a fast flick saturates. The mouse deflection competes with the gamepad and arrow keys by the larger-deflection rule.
|
||||
|
||||
@@ -145,7 +197,7 @@ The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sti
|
||||
|
||||
## Mouse in the game's menus
|
||||
|
||||
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild, the Purah Pad's camera and the map stay gameplay: the mouse keeps the camera there. When the menu closes the cursor is captured again.
|
||||
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
|
||||
|
||||
`runtime/menu_cursor.c` reads eui's screens at each pad read and moves the focus with the writes `eui::BoxCursorMgr::moveBoxCursor` makes, so no game code is called. `menu_cursor = 0` (`TOTK_MENU_CURSOR=0`, live; the *Mouse in the game's menus* row of the Controls tab) turns it off; `TOTK_MENU_CURSOR_LOG=1` logs every screen it reads (by default the first six), with the layout size, the node count and the hit panes it uses.
|
||||
|
||||
|
||||
+14
-5
@@ -38,7 +38,10 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
|
||||
|
||||
The player runs a newer installer on the same folder. It reads `totk-install.json`, keeps `romfs/`, `exefs/`,
|
||||
`shared-fonts/`, `profile/` and `totk.ini` after checking their hashes, rebuilds `totk.exe` and re-prepares the
|
||||
shaders. A newer release that changes nothing game-derived skips the rebuild.
|
||||
shaders. A newer release that changes nothing game-derived skips the rebuild. The window starts with the last install folder
|
||||
and the base, update, keys and firmware paths it ran with (paths only, never the keys file's contents), kept in
|
||||
`%LOCALAPPDATA%\TotK Recomp\setup.json` (`$XDG_STATE_HOME/totk-recomp/setup.json` elsewhere). Entries that no longer exist
|
||||
stay empty. A reinstall or a switch to another game version starts filled in.
|
||||
|
||||
Given an update file other than the recorded one (1.1.0, then 1.2.1, then 1.4.3), the installer extracts and builds again from it
|
||||
and keeps `profile/`. Saves carry over both ways between 1.2.1 and 1.4.3: 1.4.3 converts a 1.2.1 save on load, and 1.2.1
|
||||
@@ -129,9 +132,12 @@ python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR
|
||||
English as the game language, because Chinese game text needs the Switch shared fonts, which the setup extracts only
|
||||
from a firmware folder. A folder with
|
||||
`totk-install.json` needs only the folder. `--selftest [--shots DIR]` builds every screen in every language and
|
||||
closes itself.
|
||||
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the self-closing window and one end-to-end run
|
||||
of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its docstring).
|
||||
closes itself. The window is not stretched on a display whose scale is the system DPI: `installer/dpi.py` makes the
|
||||
process system DPI aware before Tk starts, Tk then draws the fonts at the display's scale and `dpi.px` sizes the
|
||||
paddings and wrap widths with it; the launcher does the same for its error box.
|
||||
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the display scale, the self-closing window and
|
||||
one end-to-end run of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its
|
||||
docstring).
|
||||
- `tests/installer_test.py`: bookkeeping with fake steps. `tests/installer_e2e.py`: a dev-mode run with the files of
|
||||
`config.local.toml`.
|
||||
|
||||
@@ -212,7 +218,8 @@ bin/ totk_xlate, hactool, ninja, libSDL3.so.0
|
||||
|
||||
## Constraints
|
||||
|
||||
- The installer steps contain no Win32 calls: Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The installer steps contain no Win32 calls; only the window's DPI awareness (`installer/dpi.py`) is Windows-only, and
|
||||
it does nothing elsewhere. Linux runs the same steps with its own compiler ("Linux release").
|
||||
- The keys file path is passed to the tools; key values never appear in the UI, logs or files.
|
||||
- The window is a front end over the CLI (`python -m installer --base ... --update ... --keys ... --lang ... --dest
|
||||
...`), which prints one JSON progress event per line. Tests and CI use the CLI.
|
||||
@@ -236,3 +243,5 @@ bin/ totk_xlate, hactool, ninja, libSDL3.so.0
|
||||
## Later
|
||||
|
||||
- Re-warm the driver cache on start after a move, GPU or driver change.
|
||||
- Per-monitor DPI in the setup window: Tk 9 has none, so a display whose scale differs from the primary one still gets
|
||||
the window stretched.
|
||||
+4
-1
@@ -48,7 +48,10 @@ Two mods that edit the same game file do not combine: the higher one wins the wh
|
||||
(`.tkcl` files) go the same way.
|
||||
|
||||
The port merges the resource size table on its own: at every start with mods on, it combines the tables the mods ship
|
||||
and raises the entries for files a mod made larger or added. A separate RESTBL mod is not needed.
|
||||
and raises the entries for files a mod made larger or added, the files inside a mod's packs (`Pack\**\*.pack.zs`)
|
||||
included. A separate RESTBL mod is not needed. The table tells the game how much memory each file needs: a file that
|
||||
outgrew its entry crashes the game when it loads, which can be long after the start, when the game first needs it. The
|
||||
log line `RomFS: resource size table: ...` says how many entries were raised and names the first files inside packs.
|
||||
|
||||
## Code patches (ExeFS)
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# Settings reference
|
||||
|
||||
`totk.ini.example` describes the settings for players. This page lists the rest: diagnostics, test hooks, paths and experiments. Set one as `name = value` in `totk.ini`, or as the environment variable `TOTK_NAME`, which wins over the file. Remove the line to go back to the default.
|
||||
|
||||
## Safety
|
||||
|
||||
- **Safe**: changes only what the row says, with no effect on stability or saves.
|
||||
- **Caution**: can slow the game, grow files, or change the picture, and undoes cleanly when removed.
|
||||
- **Risky**: can crash or hang the game, or break the picture or game logic, while it is set.
|
||||
- **Save risk**: can damage or lose saves, or point the game at other game or save files.
|
||||
- **Unknown**: the code did not settle it.
|
||||
|
||||
Many rows below are developer tools that only act together with another setting or a command-line mode (for example a trace mode or the DLSS path). The row says so where the code does. The rows come from reading the code, and most effects have not been checked in a run.
|
||||
|
||||
`tests/settings_doc_check.py` fails when a name in `known[]` (`runtime/cfg.c`) is described neither here nor in `totk.ini.example`, or when a default here differs from the code.
|
||||
|
||||
## Diagnostics and logging
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `astc_time` | `1` times the GPU dispatches of each ASTC texture decode and logs the time per block footprint and the largest call. | Adds GPU timestamp queries around every decode; if the queue has no timestamps or no `hostQueryReset`, the log says the decodes are not timed and nothing is measured. | Caution |
|
||||
| `bind_stats` | `1` counts bindings and descriptors per draw of each stage and how much of the bound state changes between draws. | Adds per-draw bookkeeping to the render thread and extra log output; frames get slower. | Caution |
|
||||
| `contact_debug` | `1` draws the contact-shadow factor in grey in place of the picture. | The frame shows a grey debug image while it is set; has no effect when contact shadows are off. | Caution |
|
||||
| `core_diag` | `1` records why ready guest threads wait for a virtual core and for how long, with a histogram every 60 presents. | Turns on host-call and SVC depth tracking and writes many log lines (one per present over 25 ms); frames get slower. | Caution |
|
||||
| `menu_cursor_log` | `1` logs what the menu cursor code reads at every screen change. | Only adds log lines. | Safe |
|
||||
| `perf_slow_ms` (default `50`) | Frames at least this many milliseconds long get a `perf: slow frame` log line with the time blocked per cause. | Changes how many frames are logged; a small value fills the log. | Safe |
|
||||
| `replay_check` | `1` reads every texture read-back a second time after a full drain of the replay queue and compares the two. | Stalls the replay queue at every read-back, so frames get much slower; mismatches are only counted and logged. | Caution |
|
||||
| `resolve_check` | `1` compares every cached pool lookup with the locked lookup and stops on a difference. | Slows every pool lookup; the game stops if the two ever differ. | Caution |
|
||||
| `speed_probe` | `1` adds the player's position and ground speed to the frame-rate line that `TOTK_TRACE=perf` prints every 120 presents. | Only adds log text; has no effect without `TOTK_TRACE=perf`. | Safe |
|
||||
|
||||
## Profiling and measurement
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `ab` | Splits game time into blocks of this many seconds in the order A B B A and lets settings written as `ab` take effect only in the B blocks. | Settings that accept `ab` flip on and off every few seconds, so frame times and the picture change with the block; without such a setting it does nothing. | Caution |
|
||||
| `crit` | `<first>-<last>` frame range (with `TOTK_TRACE=sched`) that records every state change and wake of the guest threads, then writes a file at the last frame. | Recording adds overhead to every thread switch in the range and writes one file; `tools/crit_path.py` reads it. | Caution |
|
||||
| `crit_out` (default `crit.bin`) | Path of the file that `crit` writes. | Only moves the file; nothing is written without `crit`. | Safe |
|
||||
| `profile` | Path of a sampling profile file; the profiler suspends threads to unwind their stacks. | Frames get slower while it samples and the profile file grows; `tools/prof_report.py` reads it. | Caution |
|
||||
| `profile_chains` | `1` keeps call chains of every thread in the profile. | Larger profile file and more work per sample; has no effect without `profile`. | Caution |
|
||||
| `profile_frames` | `<lo>-<hi>` samples only while presents lo to hi-1 are pending; `w<lo>-<hi>` uses world-clock seconds. | Limits the profile to a window; has no effect without `profile`. | Safe |
|
||||
| `profile_main` | `1` samples only the guest main thread. | Narrows the profile; has no effect without `profile`. | Safe |
|
||||
| `profile_render` | `1` samples only the render thread and keeps its call chains. | Narrows the profile and makes it larger; has no effect without `profile`. | Caution |
|
||||
|
||||
## Capture and frame dumps
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `dump` | Frame number to dump: every render pass target and storage image of that frame is copied out as PNG, with `passes.txt` and `images.txt`. | Writes many PNG files and shader hashes to disk and slows the dumped frame; size is capped by the dump code. | Caution |
|
||||
| `dump_buf` | `<addr>:<size>[:<off>:<stride>:<w>:<h>]` snapshots one GPU buffer range after every logged pass of a dump. | Writes `buf_<pass>.bin` files and `bufs.txt`; has no effect without `dump`. | Caution |
|
||||
| `dump_dir` (default `dump`) | Folder that receives the frame dump files. | Only moves the output. | Safe |
|
||||
| `dump_log` | Frame number; logs the frames from there up to the dumped frame into `passes.txt`, with PNGs only of targets up to 64K texels. | Writes a longer `passes.txt` and more PNGs; has no effect without `dump`. | Caution |
|
||||
| `flight_at` | Comma-separated frame numbers that trigger a flight-recorder dump. | Writes a dump folder when each frame is reached; has no effect unless the flight recorder (`TOTK_FLIGHT=1`) is on. | Caution |
|
||||
| `flight_dir` | Folder for flight-recorder dumps; the default is `flight` beside the folder of `totk.exe`. | Only moves the output. | Safe |
|
||||
| `flight_frames` (default `600`) | Number of presents the flight recorder keeps. | A larger ring keeps more history and more thumbnails in memory; has no effect unless `TOTK_FLIGHT=1`. | Caution |
|
||||
| `flight_thumbs` (default `1`) | `0` turns off the 320x180 thumbnail the flight recorder saves for each present. | Dumps hold only `frames.tsv`; thumbnails cost a GPU blit per present when on. | Safe |
|
||||
| `rdoc` | Comma-separated frame numbers to capture with RenderDoc, which is loaded before Vulkan starts. | The game runs under RenderDoc, with its capture options set to keep `VK_EXT_external_memory_host` available and to reference all resources, and captures are written to disk. | Caution |
|
||||
| `rdoc_dir` | Output folder for RenderDoc captures (the default is `build/captures`). | Only moves the output. | Safe |
|
||||
| `rdoc_dll` | Path of the RenderDoc library, for when it is not found by default. | A wrong path logs that the library cannot be loaded and no capture is taken. | Safe |
|
||||
| `shots` | `<lo>-<hi>:<step>` saves every step-th present in the range as `f<n>.png`, scaled to 480x270; `w<lo>-<hi>:<step>` takes them by world-clock seconds. | Writes one PNG per step and adds a readback of the window texture; the pictures clip highlights in HDR. | Caution |
|
||||
| `shots_dir` (default `.`) | Folder for the `shots` pictures; it is created when needed. | Only moves the output. | Safe |
|
||||
| `shots_full` | Set to any value to save `shots` at full size instead of 480x270. | Files get far larger and writing them takes longer; has no effect without `shots`. | Caution |
|
||||
|
||||
## Automated tests and test hooks
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `background_window` | `1` keeps the window behind the user's windows, never fullscreen at start, and F11 keeps its place; SDL windows ignore it. | The window does not come to the front or flash, so it is easy to miss; meant for unattended runs. | Caution |
|
||||
| `gpu` | `vulkan` or `null`: the graphics back end; `null` has no GPU, completes every submit at once and returns zeros for texture reads. | `null` shows no picture; any other value stops the start with an error. | Caution |
|
||||
| `input` | `none` reports no input from the keyboard, mouse or controllers. | The game gets no controller input and the menu cursor stops following the mouse; `input_script` still applies. | Caution |
|
||||
| `input_script` | Path of a file with lines `<frame> <button> [hold]` or `<frame> LSTICK or RSTICK <x> <y> [hold]` that are ORed into the real input. | The game presses those buttons by itself; a file that cannot be opened is logged and ignored. | Caution |
|
||||
| `menu_test` | `<frame>[:<frames per page>]` opens the menu at that frame, shows each tab for 30 frames (or the given count), saves one screenshot per tab and closes the menu. | The menu opens by itself and screenshots are written. | Caution |
|
||||
| `menu_test_dir` | Folder for the `menu_test` screenshots; the default is the working directory. | Only moves the output. | Safe |
|
||||
| `menu_test_set` | `<frame>:<key>=<value>[,...][;...]` changes settings live at that frame, with or without the menu open; `SAVE=1` also writes `totk.ini`. | Settings change during play without input, and `SAVE=1` rewrites the file. | Caution |
|
||||
| `probe_movie` | Text that picks the movie for `totk.exe --probe-movie`: the first file whose path contains it. | Only used by the movie decoder probe; ignored in normal play. | Safe |
|
||||
| `probe_movie_shot` | Path of a `.ppm` file that the movie probe writes the middle frame to. | Only used by the movie decoder probe. | Safe |
|
||||
| `swkbd_answer` | `<text>`, `<cancel>` or `<initial>`: answers the on-screen keyboard (for example a horse name) without opening a window. | The game gets that answer every time it asks for text. | Caution |
|
||||
| `swkbd_test` | `ok` or `cancel` opens the keyboard window and presses OK or Cancel on the first timer tick. | The keyboard window closes by itself. | Caution |
|
||||
| `test_pad` | `<lx>,<ly>,<rx>,<ry>` attaches a virtual gamepad whose sticks rest at these raw SDL axis values (-32768 to 32767). | The game sees a controller with that drift or deflection; a malformed value is logged and ignored. | Caution |
|
||||
| `test_prepare_shaders` | `1` translates every recorded shader on all usable CPUs during start. | Start takes longer and fills the shader cache folder; the CPU is fully used while it runs. | Caution |
|
||||
|
||||
## Input
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `bind_<button>` | Remaps one Switch button or stick direction to `pad:<name>` or `key:<name>` entries; covers `bind_a`, `bind_b`, `bind_x`, `bind_y`, `bind_l`, `bind_r`, `bind_zl`, `bind_zr`, `bind_plus`, `bind_minus`, `bind_left`, `bind_up`, `bind_right`, `bind_down`, `bind_lclick`, `bind_rclick`, `bind_lstick_left`, `bind_lstick_up`, `bind_lstick_right`, `bind_lstick_down`, `bind_rstick_left`, `bind_rstick_up`, `bind_rstick_right`, `bind_rstick_down` and `bind_shake`. | A line replaces every default of that button; button prompts in the game still show the default layout. The Controls tab of the menu writes these lines; `docs/controls.md` lists the entry names. | Safe |
|
||||
|
||||
## Paths and files
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `amiibo_dir` | Folder the Amiibo tab starts in; the tab saves the folder you last browsed here. | Only changes which folder the tab lists first. | Safe |
|
||||
| `exefs` | Folder of the game's ExeFS (with `main.npdm`); a relative path is relative to `totk.exe`'s folder. | Without it, an `exefs` folder next to `totk.exe` is used, else the path set at build time. The game code is compiled into `totk.exe`, and the loader checks the files here by SHA-256, so files from another build or game version stop the start with a message. | Caution |
|
||||
| `mods_dir` (default `mods`) | Folder that holds one subfolder per player mod, beside `totk.exe`. | Mods found there replace game files; see `docs/mods.md`. | Caution |
|
||||
| `mods_disabled` | Names of mods that are switched off, separated by `\|`. | Listed mods are not applied; the Mods tab writes this line. | Safe |
|
||||
| `mods_order` | Mod names, top first, separated by `\|`; the top mod wins a file that two mods replace. | Changes which mod's file the game gets; unlisted mods go below the listed ones, by name. | Safe |
|
||||
| `pipeline_cache` | Path of the Vulkan pipeline cache file (default `pipeline-cache.bin` beside `totk.exe`). | Moves the cache; a new path starts empty and pipelines are built again. | Caution |
|
||||
| `profile_dir` | Folder of the game profile, which holds the user account id that save data is named after; the default is a `profile` folder beside `totk.exe`, else the build-time path. | Another folder means a different set of saves. | Save risk |
|
||||
| `romfs` | Folder of the game's RomFS (with `romfs.bin`); a relative path is relative to `totk.exe`'s folder. | Without it, a `romfs` folder next to `totk.exe` is used, else the build-time path; a missing image stops the start with a message. | Save risk |
|
||||
| `romfs_index` | Path of `romfs-index.bin`, which `tools/romfs_index.py` wrote; the default is beside `totk.exe`. | The index is checked against the image by SHA-256 and size; a mismatch stops the start. | Caution |
|
||||
| `romfs_mmap` | `1` maps the RomFS image into memory instead of reading it with positional reads. | Changes how the image is read; the log reports when the mapped path is on. | Caution |
|
||||
| `romfs_mods` | Folder of an overlay with a `manifest.tsv` of sizes and SHA-256 hashes; its files are served in place of the originals. | A missing file, size or hash mismatch, or a changed original stops the start; player mods win over this overlay. | Save risk |
|
||||
| `romfs_patches` | Folder of an overlay with a `manifest.tsv`, served in place of the original files; the default is `romfs-patches` beside the index. | Same checks as `romfs_mods`; while it is set the Xbox and PC button prompts are off, and the menu says so. | Save risk |
|
||||
| `romfs_proof_dir` | Folder for the `.romfs-verified-v1` proof file of a full verification; the default is beside the index. | Only moves one small file. | Safe |
|
||||
| `romfs_verify` | `full` hashes the whole RomFS image at start; `force` hashes it again even when a proof file exists. | Start takes many minutes for a 17 GB image; a proof file is written so later starts skip the hash. | Caution |
|
||||
| `shader_cache` | Folder of the translated shader cache (default `shader-cache` beside `totk.exe`). | A new folder starts empty, so shaders are translated again during play. | Caution |
|
||||
| `shared_fonts` | Folder of the extracted system fonts; the default is `shared-fonts` beside `totk.exe`, else beside the RomFS. | Simplified Chinese, Traditional Chinese and Korean need these fonts: without them the game exits before its first frame in those languages. Other languages do not use them. | Caution |
|
||||
|
||||
## Shader translation
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `xlate` | Path of the shader translator program; the default is `totk_xlate` beside `totk.exe`, else under `build/xlate`. | Shaders are translated by that program; if it is missing the log says so and graphics shaders cannot be translated. | Risky |
|
||||
| `xlate_id` | Text or 64 hex digits that identify the translator build in shader cache keys; the default comes from an `.id` file beside the translator, else a hash of the program. | A value that two different builds share lets the cache serve shaders translated by the other build. | Caution |
|
||||
| `xlate_keep` (default `0`) | `1` keeps the translation job of a last-frame variant as `<key>.job` with its code. | Writes extra files to the shader cache folder; meant for running the translator by hand. | Caution |
|
||||
| `xlate_procs` (default `2`) | Number of translator processes allowed during play. | More processes translate new shaders sooner and use more CPU and memory; values below 1 count as 1. | Caution |
|
||||
|
||||
## Graphics experiments
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `block_views_3d` (default `0`) | `1` gives 3D block-compressed images block views too. | On some AMD cards a GPU write past the end of the image's memory loses the Vulkan device, and the game stops. | Risky |
|
||||
| `feedback` | `0` samples a target that an earlier draw of the same pass wrote with no barrier; `restart` ends and restarts the pass behind a full barrier; otherwise the feedback-loop layout is used when the GPU supports it. | `0` brings back the old behaviour that the barrier fixed, so the picture can break in passes that read a target they draw into; `restart` costs extra passes. | Risky |
|
||||
| `flush_span` (default `0`) | `1` copies one span from the lowest to the highest flushed address of a pool, as an older version did. | Copies far more data to the GPU in crowded towns: an estimated 30-50 ms of GPU time per large copy over PCIe 3.0 x8. | Caution |
|
||||
| `freeze_idle` (default `60`) | Number of submissions a 2 MiB region stays unwritten before it is made read-only; `0` turns the freeze off. | A larger value keeps more regions writable and costs more write tracking; a write to a frozen region costs about 60 microseconds. | Caution |
|
||||
| `gpl_draw_build` (default `1`) | `0` leaves the missing shader libraries of a waiting draw to its background job instead of building them in parallel at once. | Draws whose pipeline is not ready are skipped meanwhile, until the job finishes. | Caution |
|
||||
| `lib_prebuild_keep` (default `0`) | Number of recorded graphics shader library variants that stay in memory after the boot prebuild. | Each kept library costs 140-460 KB of memory; the driver cache already makes a rebuild cheap. | Caution |
|
||||
| `minimal_layout` (default `1`) | `0` skips the compare against one-block tiles when a texture's memory layout is chosen. | The code calls it a negative control for fresh sessions; the comment does not say what the picture does. | Unknown |
|
||||
| `mirror_gap` (default `2`) | Hot pools at most this many 2 MiB regions apart share one write-watch query. | Changes the number of queries per submission; negative values count as 0. | Caution |
|
||||
| `movie_host` (default `1`) | `0` leaves every movie decoder to the game's own library. | The code comment says that library aborts at the first movie, so the game stops at the first cutscene. | Risky |
|
||||
| `movie_pace` (default `1`) | `0` hands every decoded movie frame over at the next present instead of when it is due. | Movies play at the game's frame rate rather than their own, so their speed no longer matches the sound. | Caution |
|
||||
| `pcss_k` (default `0.5`) | Depth-to-penumbra factor of soft shadows at 100 percent, for tuning. | Changes how wide soft shadows blur; has no effect while `pcss` is `0`. | Caution |
|
||||
| `pipe_sync` | Any value other than `0` or empty stops graphics pipelines from being built in the background. | Pipelines are built on the render path, which causes stutter when new effects appear. | Caution |
|
||||
| `push_stage` | `fs` pushes the descriptor set from the fragment stage instead of the vertex stage. | The log names the stage in use; the code does not say what changes in the picture. | Unknown |
|
||||
| `rec_end` (default `1`) | `0` lets the submitting thread end a recorded part again, as an older version did. | On some AMD cards the GPU hangs or the scene turns green and then white when a submission is split. | Risky |
|
||||
| `tex_vram_pressure` (default `90`) | Percent of the device-local memory budget above which unused released textures are dropped, 0 to 100; `0` is off. | A low value drops textures sooner and re-uploads them; `0` keeps them and lets a 4 GiB card spill into system memory. Also a slider in the menu. | Caution |
|
||||
| `txd_transpose` (default `1`) | `0` turns off the swap of gradient components for gradient texture samples. | Terrain textures turned across the screen show crawling patches again. | Caution |
|
||||
| `ubo_snapshot` (default `1`) | `0` binds live memory for uniform blocks; `ab` alternates with the `ab` blocks. | Without snapshots late draws read another object's matrix, so objects can be drawn in the wrong place. | Risky |
|
||||
| `vk_no_float64` (default `0`) | `1` clears the 64-bit float feature, so the translator lowers FP64 to FP32. | Shaders change, so the shader cache entries for it are made again; precision of FP64 shaders drops. | Caution |
|
||||
| `vk_ssbo_align` (default unset) | Power of two that storage buffer offsets are treated as needing, to test the unaligned path. | The translator and the draw code round storage offsets down to it; shaders are translated again. | Caution |
|
||||
| `vk_subgroup` (default `32`) | Subgroup size to pin where the driver allows it. | A value other than 32 changes what the translator is told about the GPU, and shaders are translated again. | Caution |
|
||||
| `vram_headroom` (default `1`) | `0` turns off the spare half-block of video memory that a background thread keeps per memory type. | Texture allocations on the render thread can wait for a new block again. | Caution |
|
||||
| `vs_outs` | `all` declares all 32 vertex shader output locations; otherwise only the union of the paired fragment inputs. | Larger vertex shaders and new pipelines; unwritten outputs read the same as before. | Caution |
|
||||
| `wb_exact` (default `1`) | `0` decides write-back of GPU-written textures by page only. | A target that shares a page with data the CPU writes can go unwritten, so a name label next to the reticle can blink. | Caution |
|
||||
| `wb_merge` (default `1`) | `0` copies GPU-written ranges back as plain copies instead of writing word by word the words that changed. | In mirror memory mode the GPU's copy can overwrite words the CPU wrote, which breaks effects such as falling rain and waterfall splashes. | Caution |
|
||||
|
||||
## Motion vectors and DLSS
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `dlaa_jitter` (default `1`) | `0` turns off the camera jitter fed to DLSS. | DLSS gets no jitter between frames; a test setting. | Caution |
|
||||
| `dlaa_maskdebug` (default `0`) | `1` shows the reactive mask in place of the picture. | The frame shows a debug image while it is set. | Caution |
|
||||
| `dlaa_mv` (default `1`) | `0` sends zero motion vectors to DLSS. | DLSS sees no motion, so moving content can smear; a test setting. | Caution |
|
||||
| `dlaa_mvdebug` (default `0`) | `1` shows the motion vectors in place of the picture, x in red and y in green. | The frame shows a debug image while it is set. | Caution |
|
||||
| `dlaa_survey` (default `0`) | `1` logs what is drawn into the light target that DLSS reads. | Adds log output and per-draw work. | Caution |
|
||||
| `dlaa_times` (default `0`) | `1` times the DLSS passes on the GPU. | Adds GPU timestamps and log lines. | Caution |
|
||||
| `dlss_reactive` (default `0`) | `1` builds a reactive mask for DLSS where it ran. | Changes how DLSS blends moving areas; a test setting. | Caution |
|
||||
| `mv_check` | Frame interval; every that many DLSS frames copies the light target and motion images to host memory and logs how well the vectors explain the picture. | Costs a GPU-to-host copy and a comparison on the completion thread at each interval. | Caution |
|
||||
| `mv_survey` (default `0`) | `1` notes the uniform blocks of each scene draw and compares them with those of earlier frames. | Adds per-draw work and log output. | Caution |
|
||||
| `mvobj_all` (default `0`) | `1` also lists draws for object motion that read no object blocks (terrain and grass). | More draws are checked and listed, which costs time. | Caution |
|
||||
| `mvobj_banks` (default `0`) | `1` logs which words of the other uniform banks changed for each scene draw. | Adds log output and per-draw work. | Caution |
|
||||
| `mvobj_equal` (default `0`) | `1` uses an EQUAL depth compare for the object motion pass, for comparison. | Changes which pixels get object vectors; the code comment names the default as the one that hides objects behind nearer geometry. | Caution |
|
||||
| `mvobj_exact` | Hex bank mask; only draws whose position uses exactly these banks get object motion. | Fewer draws get object vectors, so their motion falls back to the camera's. | Caution |
|
||||
| `mvobj_exec` (default `1`) | `0` lists object motion draws but never records them. | Objects get camera motion only. | Caution |
|
||||
| `mvobj_noc4` (default `0`) | `1` draws again positions that have no scene camera bank. | Those draws get this frame's view, so they can show wrong motion. | Caution |
|
||||
| `mvobj_only` | Hex bank mask; only draws whose position reads no bank outside the mask get object motion. | Fewer draws get object vectors. | Caution |
|
||||
| `mvobj_times` (default `0`) | `1` adds timings to the object-motion summary. | The clock reads themselves cost frame time. | Caution |
|
||||
| `mvprev_check` (default `0`) | `1` verifies the stored first bytes of each previous-frame table entry. | The code comment measures 3.6 ms per frame. | Caution |
|
||||
| `mvprev_grow` (default `1`) | `0` keeps only three banks in the previous-frame tables. | Object motion that depends on the other banks is no longer tracked. | Caution |
|
||||
| `mvprev_skip` | Hex bank mask of banks left out of the previous-frame copy, replacing the default. | Different banks are tracked, which changes object motion. | Caution |
|
||||
| `mvprev_trust` | Hex bank mask of banks whose found previous-frame copy is taken without the check. | A copy that does not match is used anyway, which can give wrong motion. | Caution |
|
||||
|
||||
## Scheduling and host
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `app_pool` | Size in bytes of the guest application memory pool; accepts decimal or `0x` hex. | The default depends on the resolution settings; `0` stops the start with a message, and too small a pool leaves the game without memory. | Risky |
|
||||
| `audio_off` | Any value mixes audio without opening an output device. | The game runs in silence and reports two channels; same as `audio = none`. | Caution |
|
||||
| `one_per_core` (default `0`) | `1` keeps the process on the first logical processor of each physical core; `ab` switches it with the `ab` blocks. | Busy threads stop sharing a core with a sibling thread, which can help or hurt on the CPU; it only applies when the CPU has hyperthreads. | Caution |
|
||||
| `reexec` | Counter of address-space retries that the runtime sets itself when it starts the process again. | The counter is cleared when the window is recreated; a value of 20 or more stops the start with a fatal error. | Risky |
|
||||
| `restart_wait` | Set by the runtime when it restarts itself; names the old process by id and creation time so the new process waits for it to exit. | Meant for the runtime's own use; the code does not describe a manual value. | Unknown |
|
||||
| `spin` (default `1`) | `0` makes a guest thread that polls memory keep its virtual core. | Polling threads can starve the thread whose item they wait for, so frames can stall. | Caution |
|
||||
| `spin_handoff_us` (default `50`) | Microseconds a polling wait lasts before it gives its virtual core to a waiting thread. | A shorter value hands cores over sooner; a longer one keeps them longer. | Caution |
|
||||
|
||||
## Generated-code patches
|
||||
|
||||
These names are code patch sets built into the generated game code (`recomp/patches-*.txt`). `rt_patches_init` reads the environment name at start, then the resolution and mouse code switches some sets itself.
|
||||
|
||||
| Setting | What it does | Effect of changing it | Safety |
|
||||
| --- | --- | --- | --- |
|
||||
| `aspect_patch` (default `0`) | Patch set that feeds the camera the output's aspect ratio when it is not 16:9. | The resolution code switches this set on or off itself at start, so a value here is replaced; the code does not settle what a manual value does. | Unknown |
|
||||
| `fov_patch` (default `0`) | Patch set that scales the camera's vertical view angle by the field-of-view setting. | The resolution code switches this set itself whenever the field of view is not 100 percent; a manual value is replaced. | Unknown |
|
||||
| `mouse_direct_patch` (default `1`) | Patch set of the hooks in the game's camera code behind `mouse_mode = direct` on game 1.4.3. | `0` stops `mouse_mode = direct` from taking effect; other mouse modes are unaffected. | Safe |
|
||||
| `patch_output` (default `0`) | Patch set that makes the game use the configured output size for its swapchain and display configuration. | The resolution code switches this set whenever the output differs from the base size; a manual value is replaced. | Unknown |
|
||||
| `patch_scene` (default `0`) | Patch set that makes the game use the configured scene render size. | The resolution code switches this set from the render size setting; a manual value is replaced. | Unknown |
|
||||
@@ -0,0 +1,109 @@
|
||||
"""The display scale of the installer window: process DPI awareness and the pixel sizes that follow it.
|
||||
|
||||
Windows draws a DPI-unaware process at 96 DPI and stretches its windows to the display's scale, which is what made the
|
||||
setup window blurry at 125% and above. gui.py calls enable() before it imports tkinter, so the process is system DPI
|
||||
aware before its first window exists; Tk then reads the display's DPI when it starts (`tk scaling` is 2.667 at 200%)
|
||||
and every font size in points comes out at the right size. The pixel sizes of the layout have no such scaling of their
|
||||
own, so gui.py passes them through px().
|
||||
|
||||
System DPI awareness, not per-monitor: Tk 9.0.4, the one the release bundles, has no per-monitor support (its DLL
|
||||
imports no DPI-awareness function), so it draws everything at the system DPI. Per-monitor awareness would leave the
|
||||
window crisp on a display at another scale but with the primary display's font sizes; system awareness draws the
|
||||
primary display exactly and lets Windows stretch the window only on a display whose scale differs from it, as before.
|
||||
|
||||
On Linux the window stays 1:1 (px returns n as it is): Tk and the desktop scale what they draw.
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
|
||||
DPI_96 = 96 # the scale Windows calls 100%
|
||||
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2
|
||||
PROCESS_SYSTEM_DPI_AWARE = 1 # SetProcessDpiAwareness' value for the same thing (Windows 8.1)
|
||||
LOGPIXELSX = 88 # GetDeviceCaps' horizontal DPI, the only source before Windows 10 1607
|
||||
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 # ctypes.WinDLL's winmode: System32 only, never the installer's folder
|
||||
|
||||
_scale = None
|
||||
|
||||
|
||||
def enable():
|
||||
"""Make the process system DPI aware, then return the display scale (1.0 at 96 DPI). Must run before the first
|
||||
window; a later call only returns the scale."""
|
||||
global _scale
|
||||
if _scale is None:
|
||||
dpi = _system_dpi() if os.name == 'nt' else DPI_96
|
||||
_scale = dpi / DPI_96
|
||||
return _scale
|
||||
|
||||
|
||||
def scale():
|
||||
"""The display scale: 1.0 at 100%, 2.0 at 200%."""
|
||||
return enable()
|
||||
|
||||
|
||||
def px(n):
|
||||
"""n pixels at 100% as this display's pixels: 14 stays 14 at 100% and becomes 28 at 200%."""
|
||||
return int(round(n * enable()))
|
||||
|
||||
|
||||
def awareness():
|
||||
"""What Windows says the process is: 0 unaware, 1 system aware, 2 per-monitor aware."""
|
||||
if os.name != 'nt':
|
||||
return 0
|
||||
try: # Windows 10 1607
|
||||
user32 = ctypes.WinDLL('user32')
|
||||
user32.GetThreadDpiAwarenessContext.restype = ctypes.c_void_p
|
||||
user32.GetAwarenessFromDpiAwarenessContext.argtypes = [ctypes.c_void_p]
|
||||
return int(user32.GetAwarenessFromDpiAwarenessContext(user32.GetThreadDpiAwarenessContext()))
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows 8.1
|
||||
value = ctypes.c_int()
|
||||
shcore = ctypes.WinDLL('shcore', winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)
|
||||
shcore.GetProcessDpiAwareness.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)]
|
||||
if shcore.GetProcessDpiAwareness(None, ctypes.byref(value)) == 0:
|
||||
return value.value
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows Vista
|
||||
return 1 if ctypes.WinDLL('user32').IsProcessDPIAware() else 0
|
||||
except (AttributeError, OSError):
|
||||
return 0
|
||||
|
||||
|
||||
def _system_dpi():
|
||||
"""Make the process system DPI aware and return the display DPI. Each call that this Windows version has is tried;
|
||||
one failing means the process already is aware (a manifest, or the call before it) and changes nothing."""
|
||||
try:
|
||||
user32 = ctypes.WinDLL('user32', use_last_error=True)
|
||||
except OSError:
|
||||
return DPI_96
|
||||
try: # Windows 10 1607, and the only way to ask for the system context explicitly
|
||||
user32.SetProcessDpiAwarenessContext.argtypes = [ctypes.c_void_p]
|
||||
user32.SetProcessDpiAwarenessContext.restype = ctypes.c_bool
|
||||
user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE))
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows 8.1
|
||||
ctypes.WinDLL('shcore', winmode=LOAD_LIBRARY_SEARCH_SYSTEM32).SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE)
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows Vista
|
||||
user32.SetProcessDPIAware()
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # Windows 10 1607
|
||||
return int(user32.GetDpiForSystem()) or DPI_96
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
try: # before that
|
||||
gdi32 = ctypes.WinDLL('gdi32')
|
||||
user32.GetDC.restype = ctypes.c_void_p
|
||||
user32.GetDC.argtypes = [ctypes.c_void_p]
|
||||
user32.ReleaseDC.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
gdi32.GetDeviceCaps.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||
dc = user32.GetDC(None)
|
||||
dpi = gdi32.GetDeviceCaps(ctypes.c_void_p(dc), LOGPIXELSX)
|
||||
user32.ReleaseDC(None, ctypes.c_void_p(dc))
|
||||
return dpi or DPI_96
|
||||
except (AttributeError, OSError):
|
||||
return DPI_96
|
||||
+124
-48
@@ -3,7 +3,8 @@
|
||||
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, the optional firmware
|
||||
folder, the game language and the install folder, runs `python -m installer ...` as a subprocess and shows its JSON
|
||||
events. Decisions live in guilogic.py; this file only draws. The window text is in
|
||||
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids.
|
||||
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids. On Windows the window follows the
|
||||
display's DPI (installer/dpi.py): fonts and paddings grow with the scale, so Windows does not stretch it.
|
||||
|
||||
--selftest [--shots DIR] build every screen in every language and close by itself (PNG per screen with --shots)
|
||||
--lang en|ru|pt-BR|zh-Hans window language (default: the Windows display language)
|
||||
@@ -15,9 +16,18 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
import tempfile
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from . import dpi
|
||||
|
||||
# Windows fixes the DPI awareness of a process when its first window is created, and Tk reads the display's DPI when it
|
||||
# starts: both happen inside the first tkinter.Tk(), so this call comes before tkinter is imported at all.
|
||||
dpi.enable()
|
||||
|
||||
import tkinter as tk # noqa: E402 (after dpi.enable, see above)
|
||||
from tkinter import filedialog, messagebox, ttk # noqa: E402
|
||||
|
||||
try:
|
||||
from PIL import ImageGrab # only the screenshots of --selftest --shots need it; the release has no PIL
|
||||
@@ -26,13 +36,28 @@ except ImportError:
|
||||
|
||||
from . import ini
|
||||
from . import layout as layouts
|
||||
from . import recent
|
||||
from .guilogic import (DONE, FAILED, PENDING, RUNNING, SKIPPED, UI_LANGUAGE_LABELS, UI_LANGUAGES, Catalog, Form, Model,
|
||||
Process, amount, default_game_language, describe_event, parse_line, windows_ui_language)
|
||||
|
||||
POLL_MS = 100
|
||||
GLYPH = {PENDING: '\u25cb', RUNNING: '\u25b6', DONE: '\u2714', SKIPPED: '\u2013', FAILED: '\u2716'}
|
||||
COLOR = {PENDING: '#808080', RUNNING: '#1a5fb4', DONE: '#26a269', SKIPPED: '#808080', FAILED: '#c01c28'}
|
||||
PAD = 14
|
||||
# The window's pixels at 100%: Tk scales the fonts by itself (tk scaling), these through installer/dpi.py.
|
||||
PAD = dpi.px(14) # padding of the window and its frames
|
||||
WRAP = dpi.px(620) # wrap width of the long labels
|
||||
|
||||
RECENT_WAIT = 1.5 # seconds the window waits for the recorded paths
|
||||
|
||||
|
||||
def load_recent(wait=RECENT_WAIT):
|
||||
"""recent.load() in a daemon thread, waited for at most `wait` seconds: a recorded path on a disconnected network
|
||||
share can block its stat call for a long time. Nothing is prefilled when it has not returned by then."""
|
||||
out = {}
|
||||
thread = threading.Thread(target=lambda: out.update(recent.load()), daemon=True)
|
||||
thread.start()
|
||||
thread.join(wait)
|
||||
return {} if thread.is_alive() else out
|
||||
|
||||
|
||||
def fix_tcl_paths():
|
||||
@@ -57,6 +82,11 @@ class App:
|
||||
self.bound = [] # (widget, option, id, params) retranslated on a language switch
|
||||
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'firmware', 'dest')}
|
||||
self.vars['lang'] = tk.StringVar()
|
||||
self.recorded = {} # base/update paths prefilled from the recorded ones, by field
|
||||
self.touched = set() # fields the user picked or typed
|
||||
self.programmatic = False
|
||||
for name in ('base', 'update'):
|
||||
self.vars[name].trace_add('write', lambda *_, n=name: self.touched.add(n) if not self.programmatic else None)
|
||||
self.lang_code = default_game_language(ui_lang)
|
||||
self.screen = None
|
||||
self.fitted = None # the (width, height) fit last gave the window
|
||||
@@ -74,7 +104,7 @@ class App:
|
||||
self.mono_font = 'Consolas' if os.name == 'nt' else 'DejaVu Sans Mono'
|
||||
style = ttk.Style(root)
|
||||
style.configure('Title.TLabel', font=(self.ui_font, 15, 'bold'))
|
||||
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=8)
|
||||
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=dpi.px(8))
|
||||
style.configure('Err.TLabel', foreground='#c01c28')
|
||||
style.configure('Version.TLabel', foreground='#808080')
|
||||
|
||||
@@ -84,7 +114,7 @@ class App:
|
||||
self.title.pack(side='left')
|
||||
version = ttk.Label(top, style='Version.TLabel')
|
||||
self.bind_text(version, 'text', 'ui.version', version=layout.release)
|
||||
version.pack(side='left', padx=(10, 0), pady=(6, 0))
|
||||
version.pack(side='left', padx=(dpi.px(10), 0), pady=(dpi.px(6), 0))
|
||||
sw = ttk.Frame(top)
|
||||
sw.pack(side='right')
|
||||
self.ui_var = tk.StringVar(value=self.cat.lang) # the window language code; the box shows its name
|
||||
@@ -103,6 +133,15 @@ class App:
|
||||
self.bind_text(self.title, 'text', 'ui.rebuild_title')
|
||||
self.vars['dest'].set(str(rebuild))
|
||||
root.after(POLL_MS, self.start)
|
||||
else:
|
||||
# recorded sources fill only empty fields; dest goes last, its trace runs refresh_form (update hint and
|
||||
# recorded language). In update mode filled files are harmless: Extract.current hashes one only when its
|
||||
# size or mtime differ from the recorded ones.
|
||||
last = load_recent()
|
||||
self.recorded = {n: last[n] for n in ('base', 'update') if n in last and not self.vars[n].get().strip()}
|
||||
for name in ('base', 'update', 'keys', 'firmware', 'dest'):
|
||||
if name in last and not self.vars[name].get().strip():
|
||||
self.set_prefill(name, last[name])
|
||||
|
||||
# ---- text -----------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -185,19 +224,20 @@ class App:
|
||||
self.root.geometry(f'{max(w, self.root.winfo_width())}x{max(h, self.root.winfo_height())}')
|
||||
|
||||
def notice(self, parent):
|
||||
w = ttk.Label(parent, style='Notice.TLabel', wraplength=620, justify='left')
|
||||
w = ttk.Label(parent, style='Notice.TLabel', wraplength=WRAP, justify='left')
|
||||
self.bind_text(w, 'text', 'ui.notice')
|
||||
return w
|
||||
|
||||
def build_form(self):
|
||||
f = ttk.Frame(self.body)
|
||||
gap = dpi.px(4) # between the form's rows
|
||||
bar = ttk.Frame(f) # packed first so pack shrinks the text above it, never the buttons
|
||||
bar.pack(side='bottom', fill='x', pady=(12, 0))
|
||||
bar.pack(side='bottom', fill='x', pady=(dpi.px(12), 0))
|
||||
self.start_button = self.button(bar, 'ui.start', self.start)
|
||||
self.start_button.pack(side='right')
|
||||
self.form_intro = self.label(f, 'ui.intro', wraplength=620, justify='left')
|
||||
self.form_intro.pack(anchor='w', pady=(0, 8))
|
||||
self.notice(f).pack(fill='x', pady=(0, 10))
|
||||
self.form_intro = self.label(f, 'ui.intro', wraplength=WRAP, justify='left')
|
||||
self.form_intro.pack(anchor='w', pady=(0, dpi.px(8)))
|
||||
self.notice(f).pack(fill='x', pady=(0, dpi.px(10)))
|
||||
grid = ttk.Frame(f)
|
||||
grid.pack(fill='x')
|
||||
grid.columnconfigure(1, weight=1)
|
||||
@@ -206,44 +246,44 @@ class App:
|
||||
self.form_rows = {}
|
||||
for i, (name, label_id, pick_id) in enumerate(rows):
|
||||
lab = self.label(grid, label_id)
|
||||
lab.grid(row=i, column=0, sticky='w', pady=4, padx=(0, 10))
|
||||
ttk.Entry(grid, textvariable=self.vars[name]).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
lab.grid(row=i, column=0, sticky='w', pady=gap, padx=(0, dpi.px(10)))
|
||||
ttk.Entry(grid, textvariable=self.vars[name]).grid(row=i, column=1, sticky='ew', pady=gap)
|
||||
b = self.button(grid, 'ui.browse', lambda n=name, p=pick_id: self.pick_file(n, p))
|
||||
b.grid(row=i, column=2, padx=(8, 0))
|
||||
b.grid(row=i, column=2, padx=(dpi.px(8), 0))
|
||||
self.form_rows[name] = lab
|
||||
i = len(rows)
|
||||
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(8, 0))
|
||||
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=gap)
|
||||
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=gap)
|
||||
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(dpi.px(8), 0))
|
||||
i += 1
|
||||
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=4)
|
||||
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=gap)
|
||||
self.lang_box = ttk.Combobox(grid, textvariable=self.vars['lang'], state='readonly', width=30)
|
||||
self.lang_box.grid(row=i, column=1, sticky='w', pady=4)
|
||||
self.lang_box.grid(row=i, column=1, sticky='w', pady=gap)
|
||||
self.lang_touched = False
|
||||
self.lang_box.bind('<<ComboboxSelected>>', lambda _e: setattr(self, 'lang_touched', True))
|
||||
self.fill_languages()
|
||||
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=4)
|
||||
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=4)
|
||||
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(8, 0))
|
||||
self.label(f, 'ui.compressed_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.firmware_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=gap)
|
||||
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=gap)
|
||||
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.compressed_help', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.firmware_help', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
# On by default and named with its source and licence: the player sees what is downloaded and can untick it
|
||||
self.dlss_var = tk.BooleanVar(value=True)
|
||||
dlss = ttk.Checkbutton(f, variable=self.dlss_var)
|
||||
self.bind_text(dlss, 'text', 'ui.dlss')
|
||||
dlss.pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.dlss_help', wraplength=620, justify='left').pack(anchor='w', padx=(22, 0))
|
||||
self.update_hint = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.update_hint.pack(anchor='w', pady=(8, 0))
|
||||
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=620, justify='left')
|
||||
self.form_error.pack(anchor='w', pady=(8, 0))
|
||||
self.label(f, 'ui.keys_private', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
|
||||
dlss.pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.dlss_help', wraplength=WRAP, justify='left').pack(anchor='w', padx=(dpi.px(22), 0))
|
||||
self.update_hint = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.update_hint.pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=WRAP, justify='left')
|
||||
self.form_error.pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
self.label(f, 'ui.keys_private', wraplength=WRAP, justify='left').pack(anchor='w', pady=(dpi.px(8), 0))
|
||||
return f
|
||||
|
||||
def build_run(self):
|
||||
f = ttk.Frame(self.body)
|
||||
bar = ttk.Frame(f)
|
||||
bar.pack(side='bottom', fill='x', pady=(8, 0))
|
||||
bar.pack(side='bottom', fill='x', pady=(dpi.px(8), 0))
|
||||
self.cancel_button = self.button(bar, 'ui.cancel', self.cancel)
|
||||
self.cancel_button.pack(side='right')
|
||||
self.log_toggle = self.button(bar, 'ui.show_log', self.toggle_log)
|
||||
@@ -252,14 +292,22 @@ class App:
|
||||
self.run_header.pack(anchor='w')
|
||||
self.step_rows = {}
|
||||
self.steps_box = ttk.Frame(f)
|
||||
self.steps_box.pack(fill='x', pady=(8, 8))
|
||||
self.phase_label = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.steps_box.pack(fill='x', pady=dpi.px(8))
|
||||
self.phase_label = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.phase_label.pack(anchor='w')
|
||||
self.bar = ttk.Progressbar(f, mode='determinate', maximum=1000)
|
||||
self.bar.pack(fill='x', pady=(4, 6))
|
||||
self.space_label = ttk.Label(f, wraplength=620, justify='left')
|
||||
bar_holder = ttk.Frame(f)
|
||||
bar_holder.pack(fill='x', pady=(dpi.px(4), dpi.px(6)))
|
||||
self.bar = ttk.Progressbar(bar_holder, mode='determinate', maximum=1000)
|
||||
self.bar.pack(fill='both', expand=True)
|
||||
if os.name == 'nt' and ttk.Style(f).theme_use() == 'vista':
|
||||
# The vista theme draws the bar 22 px high at every DPI and has no thickness option: the holder gives it the
|
||||
# height it would have at 100%, scaled with the display. Every other theme draws its own height, and the
|
||||
# holder follows it.
|
||||
bar_holder.pack_propagate(False)
|
||||
bar_holder.configure(height=dpi.px(22))
|
||||
self.space_label = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.space_label.pack(anchor='w')
|
||||
self.warn_label = ttk.Label(f, wraplength=620, justify='left', foreground='#9a6700')
|
||||
self.warn_label = ttk.Label(f, wraplength=WRAP, justify='left', foreground='#9a6700')
|
||||
self.warn_label.pack(anchor='w')
|
||||
self.log_frame = ttk.Frame(f)
|
||||
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=(self.mono_font, 9))
|
||||
@@ -273,22 +321,22 @@ class App:
|
||||
def build_finish(self):
|
||||
f = ttk.Frame(self.body)
|
||||
bar = ttk.Frame(f)
|
||||
bar.pack(side='bottom', fill='x', pady=(12, 0))
|
||||
bar.pack(side='bottom', fill='x', pady=(dpi.px(12), 0))
|
||||
self.button(bar, 'ui.close', self.on_close).pack(side='right')
|
||||
self.open_button = self.button(bar, 'ui.open_folder', self.open_folder)
|
||||
self.open_button.pack(side='right', padx=(0, 8))
|
||||
self.open_button.pack(side='right', padx=(0, dpi.px(8)))
|
||||
self.back_button = self.button(bar, 'ui.back', lambda: self.show('form'))
|
||||
self.back_button.pack(side='left')
|
||||
if self.rebuild: # a failed or cancelled rebuild: the game starts with the code it has
|
||||
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, 8))
|
||||
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=620, justify='left')
|
||||
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, dpi.px(8)))
|
||||
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=WRAP, justify='left')
|
||||
self.finish_title.pack(anchor='w')
|
||||
self.finish_text = ttk.Label(f, wraplength=620, justify='left')
|
||||
self.finish_text.pack(anchor='w', pady=(8, 8))
|
||||
self.finish_text = ttk.Label(f, wraplength=WRAP, justify='left')
|
||||
self.finish_text.pack(anchor='w', pady=dpi.px(8))
|
||||
self.finish_steps = ttk.Frame(f)
|
||||
self.finish_steps.pack(fill='x', pady=(0, 8))
|
||||
self.finish_steps.pack(fill='x', pady=(0, dpi.px(8)))
|
||||
self.finish_notice = self.notice(f)
|
||||
self.finish_notice.pack(fill='x', pady=(8, 0))
|
||||
self.finish_notice.pack(fill='x', pady=(dpi.px(8), 0))
|
||||
return f
|
||||
|
||||
# ---- form -----------------------------------------------------------------------------------------------------
|
||||
@@ -298,9 +346,35 @@ class App:
|
||||
firmware=self.vars['firmware'].get(), lang=self.selected_lang(), dest=self.vars['dest'].get(),
|
||||
dlss=self.dlss_var.get())
|
||||
|
||||
def set_prefill(self, name, value):
|
||||
self.programmatic = True
|
||||
try:
|
||||
self.vars[name].set(value)
|
||||
finally:
|
||||
self.programmatic = False
|
||||
|
||||
def match_prefill(self, state):
|
||||
"""Prefilled base/update stay only while the chosen install recorded the same file (name and size, one stat);
|
||||
another install would otherwise extract again from them. Fields the user touched are left alone."""
|
||||
for name, value in self.recorded.items():
|
||||
if name in self.touched:
|
||||
continue
|
||||
keep = True
|
||||
if state is not None:
|
||||
known = (state.get('inputs') or {}).get(name)
|
||||
try:
|
||||
keep = (isinstance(known, dict) and Path(value).name == known.get('name')
|
||||
and Path(value).stat().st_size == known.get('size'))
|
||||
except (OSError, ValueError):
|
||||
keep = False
|
||||
want = value if keep else ''
|
||||
if self.vars[name].get() != want:
|
||||
self.set_prefill(name, want)
|
||||
|
||||
def refresh_form(self):
|
||||
form = self.form()
|
||||
state = form.installed_state()
|
||||
self.match_prefill(state)
|
||||
if state is not None:
|
||||
self.update_hint.configure(text=self.cat('ui.update_mode'))
|
||||
recorded = ini.canonical_language(state.get('lang'))
|
||||
@@ -339,6 +413,7 @@ class App:
|
||||
self.fit()
|
||||
return
|
||||
self.form_error.configure(text='')
|
||||
recent.save(form.dest, {name: getattr(form, name) for name in recent.SOURCES})
|
||||
# in a repository the Python running this window: the tools' embeddable Python there has no ._pth entry for
|
||||
# the repository, so `-m installer` would not find the package
|
||||
python = Path(sys.executable) if self.layout.dev else self.layout.python
|
||||
@@ -365,7 +440,7 @@ class App:
|
||||
self.step_rows = {}
|
||||
for s in steps:
|
||||
row = ttk.Frame(self.steps_box)
|
||||
row.pack(fill='x', pady=1)
|
||||
row.pack(fill='x', pady=dpi.px(1))
|
||||
glyph = ttk.Label(row, width=3)
|
||||
glyph.pack(side='left')
|
||||
name = ttk.Label(row)
|
||||
@@ -450,7 +525,7 @@ class App:
|
||||
def toggle_log(self):
|
||||
self.log_open = not self.log_open
|
||||
if self.log_open:
|
||||
self.log_frame.pack(fill='both', expand=True, pady=(8, 0))
|
||||
self.log_frame.pack(fill='both', expand=True, pady=(dpi.px(8), 0))
|
||||
else:
|
||||
self.log_frame.pack_forget()
|
||||
self.bound = [b for b in self.bound if b[0] is not self.log_toggle]
|
||||
@@ -587,6 +662,7 @@ def grab(root, path):
|
||||
def selftest(layout, shots=None):
|
||||
"""Every screen in every language; the window closes itself. Returns 0 when nothing raised."""
|
||||
fix_tcl_paths()
|
||||
os.environ[recent.ENV] = os.path.join(tempfile.mkdtemp(prefix='totk-selftest-'), 'setup.json')
|
||||
root = tk.Tk()
|
||||
root.geometry('+60+40')
|
||||
app = App(root, 'en', layout)
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
No console window. The two message strings come from the catalog (installer/text/<lang>.json, ids launcher.title and
|
||||
launcher.python_missing); tools/make_release.py writes them into launcher_text.h before it compiles this file.
|
||||
Build: llvm/bin/llvm-windres -I runtime -O coff launcher.rc -o launcher_rc.o, then
|
||||
llvm/bin/clang -O2 -mwindows launcher.c launcher_rc.o -o TotK-Setup.exe */
|
||||
llvm/bin/clang -O2 -mwindows launcher.c launcher_rc.o -o TotK-Setup.exe
|
||||
|
||||
System DPI aware, like the window's Python process (installer/dpi.py): Windows draws the error box of fail() at the
|
||||
display's scale instead of stretching it, which it would at 125% and above. */
|
||||
#include <windows.h>
|
||||
#include <wchar.h>
|
||||
|
||||
@@ -38,6 +41,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command_line, i
|
||||
STARTUPINFOW si = { sizeof si };
|
||||
PROCESS_INFORMATION pi;
|
||||
(void)instance; (void)previous; (void)command_line; (void)show;
|
||||
SetProcessDPIAware(); /* before the message box exists; the call fails harmlessly when a manifest set it */
|
||||
if (n == 0 || n >= PATH_CHARS) { fail(); return 1; }
|
||||
while (n > 0 && folder[n - 1] != L'\\') n--;
|
||||
folder[n > 0 ? n - 1 : 0] = 0;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""The last install folder and source paths, in a per-user file, so an update, a reinstall or a switch to another game
|
||||
version starts with the form filled in.
|
||||
|
||||
{"format": 1, "dest": ..., "base": ..., "update": ..., "keys": ..., "firmware": ...}
|
||||
|
||||
Absolute paths only. The keys entry is the path of the keys file, never its contents.
|
||||
|
||||
Windows: %LOCALAPPDATA%\TotK Recomp\setup.json; elsewhere $XDG_STATE_HOME/totk-recomp/setup.json (default
|
||||
~/.local/state/totk-recomp/setup.json). TOTK_SETUP_RECENT names another file (tests, --selftest). Nothing else is
|
||||
stored: the language comes from the install's own totk-install.json. A file of format 0 (only "dest") still loads.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from . import state
|
||||
|
||||
FORMAT = 1
|
||||
ENV = 'TOTK_SETUP_RECENT'
|
||||
FILES = ('base', 'update', 'keys') # recorded when they are files
|
||||
SOURCES = FILES + ('firmware',)
|
||||
FIELDS = ('dest',) + SOURCES
|
||||
|
||||
|
||||
def path():
|
||||
override = os.environ.get(ENV)
|
||||
if override:
|
||||
return Path(override)
|
||||
if sys.platform == 'win32':
|
||||
base = os.environ.get('LOCALAPPDATA') or str(Path.home() / 'AppData' / 'Local')
|
||||
return Path(base) / 'TotK Recomp' / 'setup.json'
|
||||
base = os.environ.get('XDG_STATE_HOME') or str(Path.home() / '.local' / 'state')
|
||||
return Path(base) / 'totk-recomp' / 'setup.json'
|
||||
|
||||
|
||||
def usable(name, value):
|
||||
"""value when it is a non-empty absolute path of the right kind for the field, else None. One stat call per
|
||||
path, no file is opened or hashed; a path the OS rejects (embedded NUL, unreachable share) counts as missing."""
|
||||
if not isinstance(value, str) or not value or not os.path.isabs(value):
|
||||
return None
|
||||
try:
|
||||
p = Path(value)
|
||||
if name == 'dest':
|
||||
ok = (p / state.NAME).is_file()
|
||||
elif name == 'firmware':
|
||||
ok = p.is_dir()
|
||||
else:
|
||||
ok = p.is_file()
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return value if ok else None
|
||||
|
||||
|
||||
def read():
|
||||
try:
|
||||
data = json.loads(path().read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def load():
|
||||
"""The recorded entries that still exist, by field name. Never raises."""
|
||||
data = read()
|
||||
return {name: data[name] for name in FIELDS if usable(name, data.get(name))}
|
||||
|
||||
|
||||
def save(dest, sources=None):
|
||||
"""Records dest and the non-empty, existing entries of sources (base, update, keys, firmware); an entry left out
|
||||
or empty keeps its saved value. Atomic; a failure to write is ignored, the window works without it."""
|
||||
target = path()
|
||||
tmp = target.with_name(target.name + '.tmp')
|
||||
data = {name: v for name, v in read().items() if name in FIELDS and isinstance(v, str)}
|
||||
data['dest'] = os.path.abspath(dest)
|
||||
for name, value in (sources or {}).items():
|
||||
if name in SOURCES and isinstance(value, str) and value.strip():
|
||||
value = os.path.abspath(value.strip())
|
||||
if usable(name, value):
|
||||
data[name] = value
|
||||
try:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp.write_text(json.dumps({'format': FORMAT, **data}), encoding='utf-8')
|
||||
os.replace(tmp, target)
|
||||
except OSError:
|
||||
pass
|
||||
@@ -103,6 +103,104 @@ set pooloff TOTK_POOL_OFFSET_PATCH 1
|
||||
site pooloff main ad43f0 93407f22 2a1903e2 # sxtw x2, w25 -> mov w2, w25 (SetStorage offset)
|
||||
site pooloff main ad440c 8b39c101 8b394101 # add x1, x8, w25, sxtw -> add x1, x8, w25, uxtw (packaged data source)
|
||||
|
||||
# Direct mouse camera (runtime/mouse_direct.c; the same hooks as patches-v143.txt, mapped by enclosing function to the
|
||||
# 1.2.1 main, each original word checked). The camera input update (main+0xd18088, 1.4.3 0x874c1c) after its dead zone,
|
||||
# clamp and StickExponent keeps x in s11 (1.4.3 s9); the host callback performs the displaced store and adds raw mouse
|
||||
# counts. The free orbit (main+0xae28b8) keeps its final angles at [sp+0x3c] / [sp+0x40], Ultrahand's curve results
|
||||
# (main+0x1768ebc) are s9 and s15. The host functions are at page +0xfd0 .. +0xf80; the page is main+0x472b000.
|
||||
set mouse_direct TOTK_MOUSE_DIRECT_PATCH 1
|
||||
site mouse_direct main d18140 bd04228b 94e84fa4 # camera input: str s11, [x20, #0x420]
|
||||
site mouse_direct main d181fc 6d4123e9 94e84f71 # camera input activity: ldp d9, d8, [sp, #0x10]
|
||||
site mouse_direct main ae28e8 f9400408 94f125b2 # free orbit begin: ldr x8, [x0, #8]
|
||||
site mouse_direct main ae355c bd403fed 94f12291 # free orbit angles: ldr s13, [sp, #0x3c]
|
||||
site mouse_direct main 176949c b9001be9 94bf0abd # Ultrahand input: str w9, [sp, #0x18]
|
||||
# The aiming, scope, ability, horse and Purah Pad cameras: per axis, the stick load and the angle the turn is added to,
|
||||
# each a bl to page +0xf80 told apart by the return address (runtime/mouse_direct.c load_sites_121). Where 1.2.1 adds
|
||||
# the turn in a shared helper (main+0x1bb70e8, 0x1bd0d00: *p = wrap(*p + s0)), the hook is on the turn the helper takes
|
||||
# (Horse) or on the store of the helper's result (Aiming's pitch target). Every enclosing function saves x30 in its
|
||||
# prologue and reads it only to return.
|
||||
site mouse_direct main 1d34338 bd4426c1 94a7df12 # Aiming: ldr s1, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 1d34374 bd015660 94a7df03 # Aiming: str s0, [x19, #0x154] (pitch target: +0x15c + turn from a helper)
|
||||
site mouse_direct main 1d34584 bd4422c1 94a7de7f # Aiming: ldr s1, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 1d345d0 bc5b83a1 94a7de6c # Aiming: ldur s1, [x29, #-0x48] (yaw)
|
||||
site mouse_direct main 1d35fd4 bd4422e0 94a7d7eb # AimingTelescope: ldr s0, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 1d35fe0 bd4426e9 94a7d7e8 # AimingTelescope: ldr s9, [x23, #0x424] (stick y)
|
||||
site mouse_direct main 1d36280 bd416260 94a7d740 # AimingTelescope: ldr s0, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 1d36224 bd405be1 94a7d757 # AimingTelescope: ldr s1, [sp, #0x58] (yaw)
|
||||
site mouse_direct main 1d48478 bd4422cf 94a78ec2 # SpecialPower: ldr s15, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 1d48480 bd4426cc 94a78ec0 # SpecialPower: ldr s12, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 1d48bb4 bd416262 94a78cf3 # SpecialPower: ldr s2, [x19, #0x160] (pitch)
|
||||
site mouse_direct main 1d48b38 bc5a83a1 94a78d12 # SpecialPower: ldur s1, [x29, #-0x58] (yaw)
|
||||
site mouse_direct main 1d4070c bd442288 94a7ae1d # Horse: ldr s8, [x20, #0x420] (stick x)
|
||||
site mouse_direct main 1d40740 bd401be0 94a7ae10 # Horse: ldr s0, [sp, #0x18] (yaw turn, which a helper adds to +0x68)
|
||||
site mouse_direct main 1d408f8 bd4426a8 94a7ada2 # Horse: ldr s8, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 1d409ac bd406661 94a7ad75 # Horse: ldr s1, [x19, #0x64] (pitch)
|
||||
site mouse_direct main 1d48ccc bd4422c9 94a78cad # SpecialPower offset mode: ldr s9, [x22, #0x420] (stick x)
|
||||
site mouse_direct main 1d48cf0 bd41da61 94a78ca4 # SpecialPower offset mode: ldr s1, [x19, #0x1d8] (offset x)
|
||||
site mouse_direct main 1d48cfc bd4426c9 94a78ca1 # SpecialPower offset mode: ldr s9, [x22, #0x424] (stick y)
|
||||
site mouse_direct main 1d48d18 bd41de61 94a78c9a # SpecialPower offset mode: ldr s1, [x19, #0x1dc] (offset y)
|
||||
site mouse_direct main 1d485f8 bd400400 94a78e62 # SpecialPower: ldr s0, [x0, #0x4] (offset scale, read only)
|
||||
site mouse_direct main 1d37b44 bd4426ad 94a7d10f # CameraFinder: ldr s13, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 1d37b68 bd405261 94a7d106 # CameraFinder: ldr s1, [x19, #0x50] (pitch)
|
||||
site mouse_direct main 1d37bcc bd4422ad 94a7d0ed # CameraFinder: ldr s13, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 1d37bf4 bd405be1 94a7d0e3 # CameraFinder: ldr s1, [sp, #0x58] (yaw)
|
||||
# The other player cameras. ClimbObj, EventMultiTalk and EventTalkManual load an angle with a pre-indexed load whose
|
||||
# write-back the code after it needs; the mov before it adds the offset instead and the hook loads [base] (no register
|
||||
# between the two words, no branch into them). Wakeboard, ShieldSurfing and Tail read the stick twice, once for its
|
||||
# length and once for its angle: the first pair gives the same values without degrees of its own (REPEAT).
|
||||
site mouse_direct main 1d38e08 bd442301 94a7cc5e # ClimbObj: ldr s1, [x24, #0x420]
|
||||
site mouse_direct main 1d38e10 bd409261 94a7cc5c # ClimbObj: ldr s1, [x19, #0x90] (yaw)
|
||||
site mouse_direct main 1d38dac bd442701 94a7cc75 # ClimbObj: ldr s1, [x24, #0x424]
|
||||
site mouse_direct main 1d38da4 aa1303f6 91022276 # ClimbObj: mov x22, x19 -> add x22, x19, #0x88
|
||||
site mouse_direct main 1d38db8 bc488ec1 94a7cc72 # ClimbObj: ldr s1, [x22, #0x88]! (pitch), as ldr s1, [x22]
|
||||
site mouse_direct main 1d50738 bd4422c0 94a76e12 # Wakeboard: ldr s0, [x22, #0x420] (x for the length, REPEAT)
|
||||
site mouse_direct main 1d50740 bd4426c1 94a76e10 # Wakeboard: ldr s1, [x22, #0x424] (y for the length, REPEAT)
|
||||
site mouse_direct main 1d50768 bd4426c0 94a76e06 # Wakeboard: ldr s0, [x22, #0x424] (stick y, kept)
|
||||
site mouse_direct main 1d5076c bd4422c1 94a76e05 # Wakeboard: ldr s1, [x22, #0x420]
|
||||
site mouse_direct main 1d50e5c bd40da60 94a76c49 # Wakeboard: ldr s0, [x19, #0xd8] (pitch)
|
||||
site mouse_direct main 1d508bc bd401fa0 94a76db1 # Wakeboard: ldr s0, [x29, #0x1c] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 1d50f20 bd403bec 94a76c18 # Wakeboard: ldr s12, [sp, #0x38] (yaw)
|
||||
site mouse_direct main 1d51e10 bd4422cd 94a7685c # WaterfallClimb: ldr s13, [x22, #0x420]
|
||||
site mouse_direct main 1d51fb8 b9403be8 94a767f2 # WaterfallClimb: ldr w8, [sp, #0x38] (yaw)
|
||||
site mouse_direct main 1d51f48 bd4426c9 94a7680e # WaterfallClimb: ldr s9, [x22, #0x424]
|
||||
site mouse_direct main 1d4a11c bd442280 94a78799 # Tail: ldr s0, [x20, #0x420] (x for the length, REPEAT)
|
||||
site mouse_direct main 1d4a124 bd442681 94a78797 # Tail: ldr s1, [x20, #0x424] (y for the length, REPEAT)
|
||||
site mouse_direct main 1d4a14c bd442680 94a7878d # Tail (climbing): ldr s0, [x20, #0x424] (stick y)
|
||||
site mouse_direct main 1d4a150 bd442281 94a7878c # Tail: ldr s1, [x20, #0x420]
|
||||
site mouse_direct main 1d4a4a0 bd40c260 94a786b8 # Tail (climbing): ldr s0, [x19, #0xc0] (yaw)
|
||||
site mouse_direct main 1d4a514 bd40b661 94a7869b # Tail (climbing): ldr s1, [x19, #0xb4] (pitch)
|
||||
site mouse_direct main 1d4c930 bd442320 94a77d94 # LockOn / Target: ldr s0, [x25, #0x420]
|
||||
site mouse_direct main 1d4c958 bd442321 94a77d8a # LockOn / Target: ldr s1, [x25, #0x420] (stick x)
|
||||
site mouse_direct main 1d4c97c bd402be0 94a77d81 # LockOn / Target: ldr s0, [sp, #0x28] (yaw turn)
|
||||
site mouse_direct main 1d4c98c bd402be9 94a77d7d # LockOn / Target: ldr s9, [sp, #0x28] (yaw turn)
|
||||
site mouse_direct main 1d4cd5c bd442721 94a77c89 # LockOn / Target: ldr s1, [x25, #0x424] (stick y)
|
||||
site mouse_direct main 1d4cd80 bd402be0 94a77c80 # LockOn / Target: ldr s0, [sp, #0x28] (pitch turn)
|
||||
site mouse_direct main 1d4cd90 bd402beb 94a77c7c # LockOn / Target: ldr s11, [sp, #0x28] (pitch turn)
|
||||
site mouse_direct main 1d443e8 bd4426a9 94a79ee6 # LockOnAimingAt: ldr s9, [x21, #0x424] (stick y)
|
||||
site mouse_direct main 1d44430 bd419a61 94a79ed4 # LockOnAimingAt: ldr s1, [x19, #0x198] (pitch offset)
|
||||
site mouse_direct main 1d44444 bd4422a9 94a79ecf # LockOnAimingAt: ldr s9, [x21, #0x420] (stick x)
|
||||
site mouse_direct main 1d44490 bd419e61 94a79ebc # LockOnAimingAt: ldr s1, [x19, #0x19c] (yaw offset)
|
||||
site mouse_direct main 1d418dc bd44234b 94a7a9a9 # HorseLockOnEmpty: ldr s11, [x26, #0x420]
|
||||
site mouse_direct main 1d3a484 bd44230b 94a7c6bf # EventMultiTalk: ldr s11, [x24, #0x420]
|
||||
site mouse_direct main 1d3a3f8 bd44270b 94a7c6e2 # EventMultiTalk: ldr s11, [x24, #0x424] (stick y)
|
||||
site mouse_direct main 1d3a418 aa1303f7 91017277 # EventMultiTalk: mov x23, x19 -> add x23, x19, #0x5c
|
||||
site mouse_direct main 1d3a420 bc45cee1 94a7c6d8 # EventMultiTalk: ldr s1, [x23, #0x5c]! (pitch), as ldr s1, [x23]
|
||||
site mouse_direct main 1d3a4dc aa1303f6 91019276 # EventMultiTalk: mov x22, x19 -> add x22, x19, #0x64
|
||||
site mouse_direct main 1d3a4e4 bc464ec1 94a7c6a7 # EventMultiTalk: ldr s1, [x22, #0x64]! (yaw), as ldr s1, [x22]
|
||||
site mouse_direct main 1d3e2b0 bd442729 94a7b734 # EventTalkManual: ldr s9, [x25, #0x424] (stick y)
|
||||
site mouse_direct main 1d3e2d4 aa1303f8 9101d278 # EventTalkManual: mov x24, x19 -> add x24, x19, #0x74
|
||||
site mouse_direct main 1d3e2d8 bc474f01 94a7b72a # EventTalkManual: ldr s1, [x24, #0x74]! (pitch), as ldr s1, [x24]
|
||||
site mouse_direct main 1d3e33c bd442329 94a7b711 # EventTalkManual: ldr s9, [x25, #0x420] (stick x)
|
||||
site mouse_direct main 1d3e360 aa1303f6 9101f276 # EventTalkManual: mov x22, x19 -> add x22, x19, #0x7c
|
||||
site mouse_direct main 1d3e364 bc47cec1 94a7b707 # EventTalkManual: ldr s1, [x22, #0x7c]! (yaw), as ldr s1, [x22]
|
||||
site mouse_direct main 1d46108 bd4422e0 94a7979e # ShieldSurfing: ldr s0, [x23, #0x420] (x for the length, REPEAT)
|
||||
site mouse_direct main 1d46110 bd4426e1 94a7979c # ShieldSurfing: ldr s1, [x23, #0x424] (y for the length, REPEAT)
|
||||
site mouse_direct main 1d46138 bd4426e0 94a79792 # ShieldSurfing: ldr s0, [x23, #0x424] (stick y, kept)
|
||||
site mouse_direct main 1d4613c bd4422e1 94a79791 # ShieldSurfing: ldr s1, [x23, #0x420] (stick x)
|
||||
site mouse_direct main 1d46788 bd40de60 94a795fe # ShieldSurfing: ldr s0, [x19, #0xdc] (pitch)
|
||||
site mouse_direct main 1d46910 bd401fa0 94a7959c # ShieldSurfing: ldr s0, [x29, #0x1c] (pitch turn, cut to the pad)
|
||||
site mouse_direct main 1d467b4 bd403bec 94a795f3 # ShieldSurfing: ldr s12, [sp, #0x38] (yaw)
|
||||
|
||||
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.txt: loops that only
|
||||
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
|
||||
spin main ac600 ac620 ac630 # 13 instructions; loads
|
||||
|
||||
@@ -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
|
||||
|
||||
+57
-11
@@ -7,6 +7,7 @@
|
||||
// cfg_get handed out stays valid) and cfg_save writes only the changed keys back, keeping the rest of the file.
|
||||
// Readers take no lock: an entry is complete before n_entries counts it, and value pointers are swapped atomically.
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -28,7 +29,8 @@ static PlatMutex set_lock = PLAT_MUTEX_INIT; // cfg_set, cfg_reset, cfg_save
|
||||
static int save_ok = 1; // the last cfg_save_to wrote (or had nothing to write)
|
||||
|
||||
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line, and gets a
|
||||
// description in totk.ini.example or docs/settings.md (tests/settings_doc_check.py checks both).
|
||||
static const char *const known[] = {
|
||||
"AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_OFF", "AUDIO_PREFILL_MS",
|
||||
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
|
||||
@@ -40,15 +42,16 @@ static const char *const known[] = {
|
||||
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
|
||||
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FRAME_QUEUE",
|
||||
"FREEZE_IDLE", "FREE_CORES",
|
||||
"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_ASPECT", "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",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_BENCH", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
|
||||
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
|
||||
"RENDER_SCALE", "REPLAY_ASYNC",
|
||||
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
|
||||
@@ -64,11 +67,11 @@ static const char *const known[] = {
|
||||
|
||||
// The totk.ini section a key cfg_set may write goes to when the file lacks it (totk.ini.example's sections).
|
||||
static const struct { const char *key, *section; } sections[] = {
|
||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"},
|
||||
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"FRAME_QUEUE", "frame rate"},
|
||||
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
|
||||
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
|
||||
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"MOVIE_ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
{"COLOR_PRESET", "display"},
|
||||
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
|
||||
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
|
||||
@@ -84,7 +87,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"},
|
||||
@@ -119,7 +122,7 @@ static const CfgMeta metas[] = {
|
||||
{"DLAA", "0", "0|1", RESTART}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance|ultra_performance", LIVE},
|
||||
{"DLSS_PRESET", "default", "default|k|j", LIVE},
|
||||
{"DLSS_CHAR_MASK", "65", "0|35|65|100", LIVE}, {"FXAA", "1", "0|1", LIVE},
|
||||
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
|
||||
{"ASPECT", "fit", "fit|stretch", LIVE}, {"MOVIE_ASPECT", "keep", "keep|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
|
||||
{"ANISO", "game", "game|2|4|8|16", RESTART}, // vk_texture.c sampler_features
|
||||
// colour adjustment (vk_color.c); the preset is the menu's, the four values are what the present uses
|
||||
{"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE},
|
||||
@@ -134,7 +137,7 @@ static const CfgMeta metas[] = {
|
||||
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
|
||||
{"PCSS", "0", "0..200", RESTART},
|
||||
// Game
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
|
||||
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE}, {"FRAME_QUEUE", "0", "0|1", LIVE},
|
||||
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
|
||||
{"MODS_REBUILD", "auto", "auto|off", RESTART},
|
||||
{"HIGHFPS_FIX", "1", "0|1", RESTART},
|
||||
@@ -149,7 +152,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},
|
||||
@@ -327,9 +330,51 @@ static const char *raw_get(const char *name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ---- the screen's shape (desktop): a primary display of another shape than 16:9 gives output_res a default of that
|
||||
// shape while neither the environment nor totk.ini sets it, so the first start fills the screen instead of a 16:9
|
||||
// picture between black bars. The sizes are the ones the installer lays the HUD out for (installer/steps.py
|
||||
// ASPECT_SIZES, ui/menu.cpp row_output_res): of the display's shape within 1%, the smallest at least 1080 high that fits
|
||||
// the display, else the largest that fits. A 16:9 display, an unknown one or one of another shape keeps 1920x1080.
|
||||
// Xbox or keyboard prompts keep 1920x1080 too: the setup builds their folders for 16:9 only (romfs.c overlay_init), so
|
||||
// a default of another shape would trade the prompts the player chose for the Switch ones.
|
||||
static const unsigned SHAPED[][2] = {
|
||||
{2560, 1080}, {3440, 1440}, {3840, 1600}, {5120, 2160}, // 21:9
|
||||
{3840, 1080}, {5120, 1440}, // 32:9
|
||||
{1280, 800}, {1680, 1050}, {1920, 1200}, {2560, 1600}, // 16:10
|
||||
{1920, 1280}, {2160, 1440}, {2256, 1504}, {2880, 1920}, {3000, 2000}, // 3:2
|
||||
};
|
||||
static char screen_res[16]; // set once by cfg_init, "" for none
|
||||
|
||||
static int switch_prompts(void) {
|
||||
const char *p = raw_get(PREFIX "PROMPTS"), *l = raw_get(PREFIX "PAD_LAYOUT");
|
||||
return (!p || !strcasecmp(p, "nintendo")) && !(l && !strcasecmp(l, "xbox_prompts"));
|
||||
}
|
||||
|
||||
static void screen_default(void) {
|
||||
int sw, sh;
|
||||
if (!plat_screen_size(&sw, &sh)) return;
|
||||
double r = (double)sw / sh;
|
||||
if (fabs(r / (16.0 / 9.0) - 1) <= 0.01) return;
|
||||
int best = -1, fit = -1;
|
||||
for (int i = 0; i < (int)(sizeof SHAPED / sizeof *SHAPED); i++) {
|
||||
unsigned w = SHAPED[i][0], h = SHAPED[i][1];
|
||||
if (fabs((double)w / h / r - 1) > 0.01 || w > (unsigned)sw || h > (unsigned)sh) continue;
|
||||
if (fit < 0 || w > SHAPED[fit][0]) fit = i; // the largest that fits
|
||||
if (h >= 1080 && (best < 0 || w < SHAPED[best][0])) best = i;
|
||||
}
|
||||
if (best < 0) best = fit;
|
||||
if (best < 0) return;
|
||||
snprintf(screen_res, sizeof screen_res, "%ux%u", SHAPED[best][0], SHAPED[best][1]);
|
||||
if (!raw_get(PREFIX "OUTPUT_RES") && switch_prompts())
|
||||
rt_log("cfg: the display is %dx%d: output_res defaults to %s, its shape (totk.ini output_res sets another)", sw,
|
||||
sh, screen_res);
|
||||
}
|
||||
|
||||
const char *cfg_get(const char *name) {
|
||||
const char *v = raw_get(name);
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN) || !profile_of()) return v;
|
||||
if (v || strncmp(name, PREFIX, PREFIX_LEN)) return v;
|
||||
if (!profile_of())
|
||||
return screen_res[0] && !strcasecmp(name + PREFIX_LEN, "OUTPUT_RES") && switch_prompts() ? screen_res : NULL;
|
||||
for (size_t i = 0; i < sizeof DECK / sizeof *DECK; i++)
|
||||
if (!strcasecmp(DECK[i].key, name + PREFIX_LEN)) return DECK[i].value;
|
||||
return NULL;
|
||||
@@ -704,6 +749,7 @@ static void load_ini(void) {
|
||||
|
||||
void cfg_init(void) {
|
||||
load_ini();
|
||||
if (!profile_of()) screen_default();
|
||||
if (profile_of())
|
||||
rt_log("cfg: device profile Steam Deck: defaults for the keys totk.ini does not set (TOTK_DEVICE_PROFILE=desktop "
|
||||
"turns it off)");
|
||||
|
||||
+11
-3
@@ -295,8 +295,7 @@ static void free_capture(void) {
|
||||
g_cap_valid = 0;
|
||||
}
|
||||
|
||||
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held.
|
||||
static int create_swapchain(void) {
|
||||
static int build_swapchain(void) {
|
||||
flip_locked(); // the acquired image goes out before its swapchain is replaced
|
||||
VkCtx *c = vk_ctx();
|
||||
VkSurfaceCapabilitiesKHR caps;
|
||||
@@ -364,10 +363,19 @@ static int create_swapchain(void) {
|
||||
}
|
||||
free_capture();
|
||||
vk_queue_unlock();
|
||||
plat_xchg32(&g_dirty, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Returns 1 with a usable swapchain, 0 when the window has no area (minimised). Called with g_pl held. The stale mark
|
||||
// goes before the surface is read: a resize during the build keeps it. The switch to fullscreen at start lands there
|
||||
// on the window thread, and the driver does not report the swapchain as out of date.
|
||||
static int create_swapchain(void) {
|
||||
plat_xchg32(&g_dirty, 0);
|
||||
if (build_swapchain()) return 1;
|
||||
plat_xchg32(&g_dirty, 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sync_barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags2 s0,
|
||||
VkAccessFlags2 a0, VkPipelineStageFlags2 s1, VkAccessFlags2 a1) {
|
||||
VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = s0, .srcAccessMask = a0,
|
||||
|
||||
@@ -333,10 +333,12 @@ static void window_main(void *arg) {
|
||||
outer_size(g_hwnd, cw, ch, &ow, &oh);
|
||||
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
if (!background) {
|
||||
// Pending before the raise: the activation it gets is the first WM_ACTIVATEAPP, which takes the fullscreen
|
||||
// setting; set after it, fullscreen waited for the next activation and the game started in a window
|
||||
g_fs_pending = cfg_fullscreen();
|
||||
raise_window(g_hwnd); // a frontend's player may have no way to reach a window left behind
|
||||
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
|
||||
FlashWindowEx(&fw);
|
||||
g_fs_pending = cfg_fullscreen();
|
||||
}
|
||||
note_applied();
|
||||
refresh_rate_update(g_hwnd, 1);
|
||||
|
||||
@@ -167,6 +167,9 @@ void mem_thaw(uint64_t a, uint64_t n); // makes the frozen r
|
||||
void mem_freeze_stats(uint64_t *frozen, uint64_t *freezes, uint64_t *fault_thaws, uint64_t *other_thaws);
|
||||
// regions frozen now; freezes and thaws since the last call
|
||||
void mem_kernel_free(void *p, uint64_t size); // TLR/TLS reuse after thread exit
|
||||
int mem_guest_readable(uint64_t a, uint64_t n); // [a, a+n) can be read without a fault: inside a loaded
|
||||
// module or the committed heap answered from the layout,
|
||||
// anything else by plat_mem_readable (an OS query)
|
||||
|
||||
// ---- modules (loader.c)
|
||||
typedef struct {
|
||||
|
||||
+47
-1
@@ -541,6 +541,35 @@ static int32_t shake_stick(int held) {
|
||||
return (now - from) / SHAKE_SIDE_NS % 2 ? STICK_MAX : -STICK_MAX;
|
||||
}
|
||||
|
||||
// The scroll wheel on the Purah Pad's map, which zooms with the right stick's y (up: in): each notch is a full-tilt
|
||||
// pulse of MAP_ZOOM_PULSE_NS on that stick with MAP_ZOOM_GAP_NS of neutral after it, so a step of the zoom and a
|
||||
// held zoom both see one notch as one push. A fast scroll's notches wait their turn (at most MAP_ZOOM_QUEUE), a turn the
|
||||
// other way drops them. Returns the stick y for this poll, 0 between pulses and off the map. Under lock.
|
||||
#define MAP_ZOOM_PULSE_NS 100000000ull
|
||||
#define MAP_ZOOM_GAP_NS 50000000ull
|
||||
#define MAP_ZOOM_QUEUE 4
|
||||
static int32_t map_zoom_stick(int32_t notches, int on_map) {
|
||||
static int32_t queued, dir;
|
||||
static uint64_t until; // the end of the pulse (dir != 0) or of the neutral after it
|
||||
uint64_t now = plat_now_ns();
|
||||
if (!on_map) { queued = dir = 0, until = 0; return 0; }
|
||||
if (notches) {
|
||||
if (queued && (notches > 0) != (queued > 0)) queued = 0;
|
||||
queued += notches;
|
||||
queued = queued > MAP_ZOOM_QUEUE ? MAP_ZOOM_QUEUE : queued < -MAP_ZOOM_QUEUE ? -MAP_ZOOM_QUEUE : queued;
|
||||
}
|
||||
if (dir) {
|
||||
if (now < until) return dir * STICK_MAX;
|
||||
dir = 0, until = now + MAP_ZOOM_GAP_NS;
|
||||
return 0;
|
||||
}
|
||||
if (now < until || !queued) return 0;
|
||||
dir = queued > 0 ? 1 : -1;
|
||||
queued -= dir;
|
||||
until = now + MAP_ZOOM_PULSE_NS;
|
||||
return dir * STICK_MAX;
|
||||
}
|
||||
|
||||
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
|
||||
static int pad_physical(uint8_t pd[PAD_COUNT]) {
|
||||
if ((!pad || !SDL_GamepadConnected(pad)) && plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
|
||||
@@ -579,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};
|
||||
@@ -617,10 +648,22 @@ 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);
|
||||
pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
// 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.
|
||||
int map = menu_cursor_map();
|
||||
if (map) pick(&sl[0], &sl[1], k[0], k[1]);
|
||||
else pick(&sr[0], &sr[1], k[0], k[1]);
|
||||
int32_t zoom = map_zoom_stick(ikb_wheel_take(), map);
|
||||
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;
|
||||
@@ -644,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,10 +23,18 @@ 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);
|
||||
int menu_cursor_pointer(void);
|
||||
int menu_cursor_map(void); // the Purah Pad's map is open: the mouse's stick is the left one (its cursor)
|
||||
// The same for host UI (the text entry): never blocked. input_block(1) while a text entry is open: input_poll then
|
||||
// reports nothing; input_block(0) when it closes: nothing until all buttons and stick directions are released.
|
||||
void input_poll_ui(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry);
|
||||
|
||||
+113
-7
@@ -1,11 +1,13 @@
|
||||
// The SDL3 back end of the keyboard and the mouse (input_wsys.h; input.c merges them with the gamepad): SDL scancodes to
|
||||
// the key ids of input_map.h, the mouse camera's relative motion and the cursor capture (SDL's relative mouse mode).
|
||||
// Key and button state comes from the window's events (window thread) and is read by any thread. Keys and their map:
|
||||
// docs/controls.md.
|
||||
// the key ids of input_map.h, the mouse camera's relative motion, the cursor capture (SDL's relative mouse mode), the
|
||||
// pointer's place in the window (the game's menus, menu_cursor.c) and the mouse travel (the last-device switch,
|
||||
// input_kbm_last). Key and button state comes from the window's events (window thread) and is read by any thread. Keys
|
||||
// and their map: docs/controls.md.
|
||||
//
|
||||
// The capture follows the Win32 back end's rules (input_win.c): captured while the window has the focus, the mouse
|
||||
// camera is on and no text entry or menu is open; Esc releases it until a click in the window (and closing the menu
|
||||
// opened by that Esc gives it back); losing the focus releases it until the window has the focus again.
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
@@ -21,8 +23,18 @@ static SDL_Window *win;
|
||||
static volatile uint8_t key_held[KEY_COUNT];
|
||||
static volatile int32_t esc_held, f8_held, focused;
|
||||
static volatile int32_t mouse_dx, mouse_dy; // relative motion since the last ikb_mouse_take, whole counts
|
||||
static volatile int32_t mouse_wheel; // wheel turned while captured, in 1/120 notches (ikb_wheel_take)
|
||||
static float rem_x, rem_y; // window thread: the fractions SDL reported beyond them
|
||||
static volatile int32_t mouse_travel; // raw counts moved in total, captured or not (ikb_mouse_travel)
|
||||
static float travel_rem; // window thread: the fraction of a count beyond mouse_travel
|
||||
static volatile int32_t captured; // relative mouse mode is on
|
||||
// The pointer's place in the window, as fractions of its size packed 32.32 fixed point (x in the high word), written
|
||||
// by the window thread from the mouse events and read by the game thread (menu_cursor.c through ikb_pointer).
|
||||
// ptr_have is 0 whenever the pointer is not over the window, as the Win32 back end's client-rect test.
|
||||
static volatile uint64_t ptr_pos;
|
||||
static volatile int32_t ptr_have;
|
||||
static volatile int32_t warp_pending; // ikb_pointer_set asked the window thread to move the pointer
|
||||
static float warp_nx, warp_ny; // its target, read by the window thread once warp_pending is seen
|
||||
enum { CUR_REL = 1, CUR_ESC = 2 }; // as input_win.c: released by Esc or a click elsewhere; by Esc
|
||||
static volatile int32_t cur_rel, cur_suspend, cur_pending;
|
||||
static uint32_t wake_event; // SDL_RegisterEvents: wakes the window thread for a cursor change
|
||||
@@ -133,6 +145,39 @@ static void cursor_apply(void) {
|
||||
plat_xchg32(&captured, want);
|
||||
}
|
||||
|
||||
// The pointer's place in the window (the window thread): x and y are SDL window units, the same space the mouse
|
||||
// events report, so dividing by the window's size makes fractions of it whatever the pixel density is. At and past
|
||||
// the edges the fraction is clamped to 0..1, where a letterboxed picture covers no item anyway.
|
||||
static void pointer_store(float x, float y) {
|
||||
if (!win) return;
|
||||
int w = 0, h = 0;
|
||||
SDL_GetWindowSize(win, &w, &h);
|
||||
if (w < 1 || h < 1) return;
|
||||
double fx = x / w, fy = y / h;
|
||||
if (!(fx >= 0)) fx = 0; // also NaN
|
||||
else if (fx > 1) fx = 1;
|
||||
if (!(fy >= 0)) fy = 0;
|
||||
else if (fy > 1) fy = 1;
|
||||
__atomic_store_n(&ptr_pos, (uint64_t)(uint32_t)(fx * 4294967295.0) << 32 | (uint32_t)(fy * 4294967295.0),
|
||||
__ATOMIC_RELEASE);
|
||||
__atomic_store_n(&ptr_have, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
// A warp asked for by ikb_pointer_set, done here as every window call: on the window thread, and after the cursor
|
||||
// change of the same menu opening has been applied. SDL's warp makes a motion event where it works (X11); where it
|
||||
// does not (some Wayland compositors) the stored place stands, so the item under the pointer is the place asked for
|
||||
// even if the visible cursor stayed behind.
|
||||
static void pointer_apply(void) {
|
||||
if (!plat_xchg32(&warp_pending, 0)) return;
|
||||
if (!win || !focused) return;
|
||||
int w = 0, h = 0;
|
||||
SDL_GetWindowSize(win, &w, &h);
|
||||
if (w < 1 || h < 1) return;
|
||||
float x = warp_nx * w, y = warp_ny * h;
|
||||
SDL_WarpMouseInWindow(win, x, y);
|
||||
pointer_store(x, y);
|
||||
}
|
||||
|
||||
// Any thread: the window thread applies the change at its next event.
|
||||
void ikb_cursor_update(void) {
|
||||
plat_xchg32(&cur_pending, 1);
|
||||
@@ -158,11 +203,13 @@ void input_sdl_window(SDL_Window *w) {
|
||||
win = w;
|
||||
wake_event = SDL_RegisterEvents(1);
|
||||
plat_xchg32(&focused, (SDL_GetWindowFlags(w) & SDL_WINDOW_INPUT_FOCUS) != 0);
|
||||
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE); // the pointer's place is unknown until it enters or moves
|
||||
cursor_apply();
|
||||
}
|
||||
|
||||
void input_sdl_idle(void) {
|
||||
if (cur_pending) cursor_apply();
|
||||
pointer_apply();
|
||||
}
|
||||
|
||||
void input_sdl_event(const SDL_Event *e) {
|
||||
@@ -186,9 +233,19 @@ void input_sdl_event(const SDL_Event *e) {
|
||||
int k = mouse_key(e->button.button);
|
||||
if (k) key_held[k] = (uint8_t)down;
|
||||
if (down && (cur_rel & CUR_REL)) plat_xchg32(&cur_rel, 0); // a click in the window captures again
|
||||
if (!captured) pointer_store(e->button.x, e->button.y); // a click can come without a motion first
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
case SDL_EVENT_MOUSE_MOTION: {
|
||||
// The travel counts any motion, captured or not, as the Win32 back end's raw input does: it is what makes a
|
||||
// moved mouse the device used last, so the menu pointer appears without a click or a key.
|
||||
travel_rem += fabsf(e->motion.xrel) + fabsf(e->motion.yrel);
|
||||
int32_t t = (int32_t)travel_rem;
|
||||
if (t) {
|
||||
travel_rem -= t;
|
||||
plat_add32(&mouse_travel, t);
|
||||
}
|
||||
if (!captured) pointer_store(e->motion.x, e->motion.y); // in relative mode x and y are not the pointer's
|
||||
if (captured) {
|
||||
rem_x += e->motion.xrel;
|
||||
rem_y += e->motion.yrel;
|
||||
@@ -198,6 +255,22 @@ void input_sdl_event(const SDL_Event *e) {
|
||||
if (dy) plat_add32(&mouse_dy, dy);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_WINDOW_MOUSE_ENTER: { // entering without moving yet: the place is where the enter found the pointer
|
||||
float x, y;
|
||||
SDL_GetMouseState(&x, &y); // the window with mouse focus is this one during its own enter event
|
||||
pointer_store(x, y);
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_WHEEL: // notches, + away from the player (a flipped wheel reports the other sign)
|
||||
if (captured) {
|
||||
float y = e->wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -e->wheel.y : e->wheel.y;
|
||||
plat_add32(&mouse_wheel, (int32_t)(y * 120));
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
plat_xchg32(&focused, 1);
|
||||
break;
|
||||
@@ -207,9 +280,11 @@ void input_sdl_event(const SDL_Event *e) {
|
||||
memset((void *)key_held, 0, sizeof key_held); // the key-ups go elsewhere
|
||||
plat_xchg32(&esc_held, 0);
|
||||
plat_xchg32(&f8_held, 0);
|
||||
__atomic_store_n(&ptr_have, 0, __ATOMIC_RELEASE); // the pointer left with the focus, leave or not
|
||||
break;
|
||||
}
|
||||
cursor_apply();
|
||||
pointer_apply(); // after cursor_apply: a menu opening uncaptures first, then the pointer moves to its middle
|
||||
}
|
||||
|
||||
// ---- input_wsys.h
|
||||
@@ -217,11 +292,42 @@ int ikb_foreground(void) { return focused != 0; }
|
||||
int ikb_down(int key) { return key > 0 && key < KEY_COUNT && key_held[key]; }
|
||||
int ikb_escape(void) { return esc_held != 0; }
|
||||
int ikb_captured(void) { return captured != 0; }
|
||||
int ikb_pointer(float *nx, float *ny) { (void)nx, (void)ny; return 0; } // (menu_cursor.c: Windows only for now)
|
||||
void ikb_pointer_set(float nx, float ny) { (void)nx, (void)ny; }
|
||||
uint32_t ikb_mouse_travel(void) { return 0; } // (the last-device switch: keys and buttons only for now)
|
||||
|
||||
// The pointer's place in the window as fractions of its size (menu_cursor.c maps them onto the picture the present
|
||||
// drew). 0 whenever the pointer is not over this window or the window has not the focus, as the Win32 back end's
|
||||
// GetCursorPos test.
|
||||
int ikb_pointer(float *nx, float *ny) {
|
||||
if (!focused || !__atomic_load_n(&ptr_have, __ATOMIC_ACQUIRE)) return 0;
|
||||
uint64_t p = __atomic_load_n(&ptr_pos, __ATOMIC_ACQUIRE);
|
||||
*nx = (float)(uint32_t)(p >> 32) / 4294967295.0f;
|
||||
*ny = (float)(uint32_t)p / 4294967295.0f;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Moves the pointer there (menu_cursor.c: to the middle when a menu opens). The window thread warps at its next
|
||||
// event, as SDL asks for its video thread; until then the warp target stands as the pointer's place.
|
||||
void ikb_pointer_set(float nx, float ny) {
|
||||
if (!(nx >= 0 && nx <= 1) || !(ny >= 0 && ny <= 1)) return;
|
||||
warp_nx = nx, warp_ny = ny;
|
||||
__atomic_store_n(&warp_pending, 1, __ATOMIC_RELEASE);
|
||||
if (wake_event) {
|
||||
SDL_Event e = {.user = {.type = wake_event}};
|
||||
SDL_PushEvent(&e);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ikb_mouse_travel(void) { return (uint32_t)__atomic_load_n(&mouse_travel, __ATOMIC_RELAXED); }
|
||||
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
|
||||
*dx = plat_xchg32(&mouse_dx, 0);
|
||||
*dy = plat_xchg32(&mouse_dy, 0);
|
||||
}
|
||||
|
||||
int32_t ikb_wheel_take(void) {
|
||||
int32_t o, n;
|
||||
do {
|
||||
o = mouse_wheel;
|
||||
n = o % 120; // the part of a notch stays for the next call
|
||||
} while (!__atomic_compare_exchange_n(&mouse_wheel, &o, n, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST));
|
||||
return o / 120;
|
||||
}
|
||||
@@ -84,6 +84,7 @@ uint32_t input_key_char(int key) {
|
||||
// ---- mouse camera: raw motion accumulated by the window thread, one stick sample per presented frame
|
||||
static volatile LONG mouse_dx, mouse_dy; // raw counts since the last frame sample
|
||||
static volatile LONG mouse_travel; // raw counts moved in total, captured or not (ikb_mouse_travel)
|
||||
static volatile LONG mouse_wheel; // wheel turned while captured, in WHEEL_DELTA units (ikb_wheel_take)
|
||||
static volatile LONG cur_captured; // cursor hidden and confined to the client area
|
||||
static volatile LONG cur_active; // WM_ACTIVATEAPP: the window's application is active
|
||||
// Bits: CUR_REL, Esc or activation by a click on the title bar or border: until a client click; CUR_ESC, set with CUR_REL
|
||||
@@ -147,6 +148,9 @@ int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
|
||||
InterlockedAdd(&mouse_dy, y);
|
||||
}
|
||||
}
|
||||
// The wheel comes in the same message (usButtonData, signed, + away from the player)
|
||||
if (cur_captured && ri.header.dwType == RIM_TYPEMOUSE && (ri.data.mouse.usButtonFlags & RI_MOUSE_WHEEL))
|
||||
InterlockedAdd(&mouse_wheel, (SHORT)ri.data.mouse.usButtonData);
|
||||
break; // DefWindowProc must see WM_INPUT for cleanup
|
||||
}
|
||||
case WM_SETCURSOR:
|
||||
@@ -234,6 +238,15 @@ void ikb_mouse_take(int32_t *dx, int32_t *dy) {
|
||||
*dy = InterlockedExchange(&mouse_dy, 0);
|
||||
}
|
||||
|
||||
int32_t ikb_wheel_take(void) {
|
||||
LONG o, n;
|
||||
do {
|
||||
o = mouse_wheel;
|
||||
n = o % WHEEL_DELTA; // the part of a notch stays for the next call
|
||||
} while (InterlockedCompareExchange(&mouse_wheel, n, o) != o);
|
||||
return (int32_t)(o / WHEEL_DELTA);
|
||||
}
|
||||
|
||||
void ikb_cursor_update(void) { cursor_update(gfx_window_handle()); }
|
||||
|
||||
void ikb_menu_closed(void) { cur_rel_update(rel_menu_closed); }
|
||||
@@ -13,6 +13,8 @@ void ikb_pointer_set(float nx, float ny); // moves the pointer there (the wind
|
||||
int ikb_captured(void); // the cursor is captured for the mouse camera
|
||||
void ikb_mouse_take(int32_t *dx, int32_t *dy); // raw mouse motion since the last call, in counts
|
||||
uint32_t ikb_mouse_travel(void); // raw counts the mouse moved in total (|dx| + |dy|), captured or not
|
||||
int32_t ikb_wheel_take(void); // whole wheel notches turned while captured since the last call (+ away
|
||||
// from the player); a high-resolution wheel's part of a notch is kept
|
||||
void ikb_cursor_update(void); // what input_cursor_allowed says may have changed
|
||||
void ikb_menu_closed(void); // the menu closed: a capture that Esc gave up comes back
|
||||
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@
|
||||
//
|
||||
// totk.exe [--exefs DIR] [--romfs DIR] [--profile DIR] <mode>
|
||||
// --selftest layout, dispatch table and host-function registry; needs no game files
|
||||
// --probe-mods the mods folder scan, order and switches (mods.c); needs no game files
|
||||
// --probe-mods the mods folder scan, order and switches (mods.c), the size table entries of the members of
|
||||
// mods' packs (restbl_probe.c); needs no game files
|
||||
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
|
||||
// --probe-kernel, --probe-ipc, --probe-services, --probe-fs host probes
|
||||
// --probe-discord the Rich Presence IPC exchange; skips without a Discord client; no game files
|
||||
|
||||
@@ -288,6 +288,35 @@ uint32_t mem_set_heap_size(uint64_t size, uint64_t *addr) {
|
||||
return R_OK;
|
||||
}
|
||||
|
||||
// The runtime's readers of game objects (menu_cursor.c, mouse_direct.c) check every pointer before they follow it. An
|
||||
// OS query (plat_mem_readable: VirtualQuery) costs tens of microseconds in this process, and a reader that checks a
|
||||
// few dozen pointers at a time stalls the game thread for milliseconds. The game's objects live in the committed heap
|
||||
// and its globals in the modules' images, which are readable for as long as they are mapped: those answer from the
|
||||
// layout. Any other range asks the OS. The heap only shrinks when the game asks for it (mem_set_heap_size), not while
|
||||
// it runs.
|
||||
int mem_guest_readable(uint64_t a, uint64_t n) {
|
||||
static uint64_t fast, slow;
|
||||
if (a + n < a) return 0;
|
||||
uint64_t hs = __atomic_load_n(&heap_size, __ATOMIC_ACQUIRE);
|
||||
int ok = a >= L.heap.base && a + n <= L.heap.base + hs;
|
||||
for (int i = 0; !ok && i < ldr_count(); i++) {
|
||||
const RtLoaded *m = ldr_module(i);
|
||||
ok = a >= m->base && a + n <= m->base + m->size;
|
||||
}
|
||||
if (ok) {
|
||||
if (__atomic_add_fetch(&fast, 1, __ATOMIC_RELAXED) == 100000)
|
||||
rt_log("memory: 100000 guest reads checked by the layout, %llu by the OS",
|
||||
(unsigned long long)__atomic_load_n(&slow, __ATOMIC_RELAXED));
|
||||
} else {
|
||||
ok = plat_mem_readable(a, n);
|
||||
uint64_t s = __atomic_add_fetch(&slow, 1, __ATOMIC_RELAXED);
|
||||
if (!(s & (s - 1)) && s >= 1024) // 1024, 2048, 4096...: how often the layout could not answer
|
||||
rt_log("memory: guest reads checked by the layout %llu, by the OS %llu",
|
||||
(unsigned long long)__atomic_load_n(&fast, __ATOMIC_RELAXED), (unsigned long long)s);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---- module data permissions (0x02, 0x03) and module pages for QueryMemory
|
||||
static int module_index(uint64_t a) {
|
||||
for (int i = 0; i < ldr_count(); i++) {
|
||||
|
||||
+64
-6
@@ -53,7 +53,7 @@ enum { PANE_PARENT = 0x18, PANE_SIZE = 0x50, PANE_FLAG = 0x58, PANE_ALPHA = 0x59
|
||||
static uint64_t main_lo, main_hi;
|
||||
|
||||
static int ok_at(uint64_t p, uint64_t n) {
|
||||
return p >= 0x10000 && p < (1ull << 47) && (p & 7) == 0 && plat_mem_readable(p, n);
|
||||
return p >= 0x10000 && p < (1ull << 47) && (p & 7) == 0 && mem_guest_readable(p, n);
|
||||
}
|
||||
static uint64_t rd64(uint64_t a) { uint64_t v; memcpy(&v, (const void *)(uintptr_t)a, 8); return v; }
|
||||
static uint32_t rd32(uint64_t a) { uint32_t v; memcpy(&v, (const void *)(uintptr_t)a, 4); return v; }
|
||||
@@ -157,11 +157,45 @@ static Focus focus_now(void) {
|
||||
static void layout_name(uint64_t scr, char *out, size_t n) {
|
||||
snprintf(out, n, "?");
|
||||
uint64_t lyt = rd64(scr + SCR_LAYOUT), name = ok_at(lyt, LYT_NAME + 8) ? rd64(lyt + LYT_NAME) : 0;
|
||||
if (name < 0x10000 || name >= (1ull << 47) || !plat_mem_readable(name, n)) return;
|
||||
if (name < 0x10000 || name >= (1ull << 47) || !mem_guest_readable(name, n)) return;
|
||||
memcpy(out, (const void *)(uintptr_t)name, n - 1);
|
||||
out[n - 1] = 0;
|
||||
for (char *q = out; *q; q++) if (*q < 0x20 || *q > 0x7e) { *q = 0; break; }
|
||||
}
|
||||
// The open screens: eui::ScreenMgr's screen array, count at +0x8 and its pointers at +0x10 (1.0.0's layout, which the
|
||||
// 1.4.3 constructor keeps). The map has no focus box, so it is found here: the Purah Pad's frame (AppMenuOverlay_00)
|
||||
// with AppMap_00 and the menu's background AppMenuBG_00 open. The Scope opens the frame over AppMap_00 without that
|
||||
// background (Weeping of the Empire's engine/runtime/totk_menu_cursor.cpp census tells them apart the same way).
|
||||
// TOTK_MENU_CURSOR_LOG=1 logs the open screens whenever the set changes.
|
||||
enum { SM_COUNT = 0x8, SM_SCREENS = 0x10 };
|
||||
static int map_open(void) {
|
||||
uint64_t sm = screen_mgr();
|
||||
if (!sm) return 0;
|
||||
int32_t n = (int32_t)rd32(sm + SM_COUNT);
|
||||
uint64_t arr = rd64(sm + SM_SCREENS);
|
||||
if (n <= 0 || n > 1024 || !ok_at(arr, 8ull * (uint64_t)n)) return 0;
|
||||
const char *e = cfg_get("TOTK_MENU_CURSOR_LOG");
|
||||
int verbose = e && !strcmp(e, "1"), pad = 0, map = 0, bg = 0;
|
||||
char list[1024];
|
||||
size_t len = 0;
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
for (int32_t i = 0; i < n; i++) {
|
||||
uint64_t scr = rd64(arr + 8ull * (uint64_t)i);
|
||||
if (!game_object(scr, SCR_REQUEST + 1) || rd8(scr + SCR_STATE) != SCR_OPENED) continue;
|
||||
char nm[48];
|
||||
layout_name(scr, nm, sizeof nm);
|
||||
pad |= !strcmp(nm, "AppMenuOverlay_00");
|
||||
map |= !strcmp(nm, "AppMap_00");
|
||||
bg |= !strcmp(nm, "AppMenuBG_00");
|
||||
if (!verbose) continue;
|
||||
for (const char *c = nm; *c; c++) h = (h ^ (uint8_t)*c) * 1099511628211ull;
|
||||
if (len < sizeof list - 64) len += (size_t)snprintf(list + len, sizeof list - len, "%s%s", len ? " " : "", nm);
|
||||
}
|
||||
static uint64_t last;
|
||||
static int lines;
|
||||
if (verbose && h != last && lines < 200) last = h, lines++, rt_log("menu_cursor: open screens: %s", list);
|
||||
return pad && map && bg;
|
||||
}
|
||||
static void layout_size(uint64_t scr, float *lw, float *lh) {
|
||||
*lw = 1920, *lh = 1080;
|
||||
uint64_t lyt = rd64(scr + SCR_LAYOUT);
|
||||
@@ -245,8 +279,10 @@ static int gameplay_screen(const char *nm) {
|
||||
}
|
||||
|
||||
static volatile int32_t g_pointer; // a menu has the pointer: input_cursor_allowed releases the capture
|
||||
static volatile int32_t g_map; // the Purah Pad's map is open (not the Scope): the mouse moves its cursor
|
||||
|
||||
int menu_cursor_pointer(void) { return __atomic_load_n(&g_pointer, __ATOMIC_RELAXED); }
|
||||
int menu_cursor_map(void) { return __atomic_load_n(&g_map, __ATOMIC_RELAXED); }
|
||||
|
||||
static int enabled(void) {
|
||||
if (!RT_V143) return 0;
|
||||
@@ -282,29 +318,40 @@ static void log_screen(const Focus *f, const char *nm, int n, float lw, float lh
|
||||
// Once per pad read on the game thread: the pointer's hover and clicks, as pad buttons (A, B) for this read
|
||||
void menu_cursor_poll(uint64_t *buttons) {
|
||||
static int inited, pending_a, held_l, held_r;
|
||||
static uint64_t pending_node, pending_until, hold_until, hold_bit;
|
||||
static uint64_t pending_node, pending_until, hold_until, hold_bit, menu_seen;
|
||||
static float last_px = -1, last_py = -1;
|
||||
if (!inited) {
|
||||
inited = 1;
|
||||
main_lo = MOD_MAIN_BASE, main_hi = MOD_MAIN_BASE + MAIN_SIZE;
|
||||
if (RT_V143) rt_log("menu_cursor: on for 1.4.3 (TOTK_MENU_CURSOR=0 turns it off)");
|
||||
}
|
||||
if (!enabled()) { set_pointer(0); return; }
|
||||
if (!enabled()) { set_pointer(0); __atomic_store_n(&g_map, 0, __ATOMIC_RELAXED); return; }
|
||||
Focus f = focus_now();
|
||||
char nm[48] = "";
|
||||
if (f.screen) layout_name(f.screen, nm, sizeof nm);
|
||||
// The open screens are scanned every 100 ms, not at every pad read: each screen costs memory queries
|
||||
uint64_t now = plat_now_ms();
|
||||
static uint64_t map_at;
|
||||
static int map, map_logged;
|
||||
if (!map_at || now - map_at >= 100) map_at = now, map = map_open();
|
||||
if (map && !map_logged) map_logged = 1, rt_log("menu_cursor: the map is open: the mouse moves its cursor");
|
||||
__atomic_store_n(&g_map, map, __ATOMIC_RELAXED);
|
||||
// With the gamepad as the last device the menus stay as they are on the console: no pointer, the capture kept.
|
||||
// Moving the mouse (or a key) makes keyboard and mouse the last device and the pointer comes.
|
||||
int menu = f.screen && !gameplay_screen(nm) && ikb_foreground() && input_kbm_last();
|
||||
set_pointer(menu);
|
||||
if (menu) menu_seen = now;
|
||||
// A menu's focus gap (a tab switch, a popup opening: no focus box for a frame or two) keeps the pointer where it
|
||||
// is, for up to 300 ms: releasing it there captured it again and centred it when the next tab took the focus
|
||||
int gap = !menu && !f.screen && menu_seen && now - menu_seen < 300 && ikb_foreground() && input_kbm_last();
|
||||
set_pointer(menu || gap);
|
||||
int l = ikb_down(KEY_MOUSE_L), r = ikb_down(KEY_MOUSE_R);
|
||||
int click_l = l && !held_l, click_r = r && !held_r;
|
||||
held_l = l, held_r = r;
|
||||
uint64_t now = plat_now_ms();
|
||||
if (now < hold_until) { // a click's button, held for 60 ms so the game sees one press whatever its read rate
|
||||
*buttons |= hold_bit;
|
||||
return;
|
||||
}
|
||||
if (gap) return; // a click's focus request waits for the next screen
|
||||
if (!menu) { pending_a = 0; last_px = last_py = -1; return; }
|
||||
if (pending_a) { // the focus asked for by a click: A once it is there
|
||||
if (rd64(f.control + CTL_ACTIVE) == pending_node) {
|
||||
@@ -338,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"));
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// --probe-mods: the mods folder scan, its order and switches (mods.c). Builds a mods folder of its own next to totk.exe,
|
||||
// scans it and removes it. Needs no game files.
|
||||
// scans it and removes it. Also the resource size table's entries of the members of the mods' packs (restbl_probe.c).
|
||||
// Needs no game files.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -7,6 +8,7 @@
|
||||
#include "host.h"
|
||||
#include "mods.h"
|
||||
#include "platform/platform.h"
|
||||
#include "restbl.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL mods: %s\n", what); } } while (0)
|
||||
@@ -126,5 +128,6 @@ int probe_mods(void) {
|
||||
}
|
||||
remove_tree(root);
|
||||
printf("mods: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails ? 1 : 0;
|
||||
int table = restbl_probe();
|
||||
return fails || table ? 1 : 0;
|
||||
}
|
||||
+766
-37
@@ -1,12 +1,16 @@
|
||||
// Direct mouse input for TotK 1.4.3. The camera controller's input update (main+0x874c1c) turns HID stick floats
|
||||
// into a radial dead zone, clamps the radius to one and applies StickExponent. The mouse enters after all three,
|
||||
// at main+0x874cb4, before the game's X/Y inversion and input-activity flags. It never becomes a HID stick value.
|
||||
// Direct mouse input for TotK 1.4.3 and 1.2.1. The camera controller's input update (1.4.3 main+0x874c1c, 1.2.1
|
||||
// main+0xd18088) turns HID stick floats into a radial dead zone, clamps the radius to one and applies StickExponent.
|
||||
// The mouse enters after all three, at the store of x (1.4.3 main+0x874cb4), before the game's X/Y inversion and
|
||||
// input-activity flags. It never becomes a HID stick value. Where each version keeps the values the hooks read is in
|
||||
// its MouseLayout (layout_143, layout_121), chosen by the running game version.
|
||||
// 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 +19,37 @@
|
||||
#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. It is the free camera's rate
|
||||
// since the direct mode came (0.08, about 14.3 cm a turn at 800 DPI), so a mouse_sens set for it keeps its meaning. 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 0.08f
|
||||
// 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: the mouse's part is
|
||||
// HAND_PER_COUNT a count over the frame ratio, the rate the direct mode has had since it came, at most HAND_MAX full
|
||||
// tilts a step
|
||||
#define HAND_PER_COUNT 0.01f
|
||||
#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 +72,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;
|
||||
@@ -88,18 +121,49 @@ void input_direct_mouse_row(int32_t dx, float sensitivity, uint64_t now, InputMo
|
||||
#define HOST_MOUSE_HAND (RT_PATCH_PAGE + 0xf90)
|
||||
static _Thread_local uint64_t mouse_input_object;
|
||||
|
||||
// Where a game version keeps what the hooks read and write (recomp/patches-v143.txt, patches-v121.txt). The camera
|
||||
// input update keeps its flags in w21, its input object in x20 and y in s10 in both; the free orbit's object is x0 at
|
||||
// its begin and x20 at its angles; Ultrahand's object is x19 and its y s15.
|
||||
struct LoadSite;
|
||||
typedef struct {
|
||||
uint8_t input_x; // camera_input: the s register with x, which the displaced store puts at +0x420
|
||||
uint8_t activity_sp; // camera_activity: the displaced ldp d9, d8, [sp, #activity_sp]
|
||||
uint8_t orbit_pitch, orbit_yaw; // orbit_angles: the final pitch and yaw on the stack, sp + these
|
||||
uint8_t orbit_reg; // and the displaced ldr s<orbit_reg>, [sp, #orbit_pitch]
|
||||
uint8_t hand_x_sp, hand_x_reg; // hand_input: x at sp + hand_x_sp, or in s<hand_x_reg> when that is 0
|
||||
uint8_t hand_store_reg, hand_store_sp; // the displaced str w<hand_store_reg>, [sp, #hand_store_sp]
|
||||
uint64_t player_root_slot, main_size; // player_state: 0 when not known for this version
|
||||
const struct LoadSite *loads; // camera_load's sites
|
||||
size_t load_count;
|
||||
} MouseLayout;
|
||||
static const MouseLayout *mouse_layout(void);
|
||||
|
||||
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;
|
||||
int swing; // a mouse shake swings the right stick in this step or did in the last (pad[0] is 0 then)
|
||||
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); // the 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 +198,76 @@ 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".
|
||||
// 1.2.1's slot, component offset and verb numbers are not known (MouseLayout.player_root_slot 0): there the player
|
||||
// always reads as not found, so the ability camera's stick fallback keeps on-foot gains and mouse_aim_scale stays off.
|
||||
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 + mouse_layout()->main_size;
|
||||
}
|
||||
|
||||
static uint32_t player_state(void) {
|
||||
if (!mouse_layout()->player_root_slot) return 0;
|
||||
uint64_t slot = MOD_MAIN_BASE + mouse_layout()->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,19 +280,94 @@ 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
|
||||
// s9/s10, which carry the processed camera input, and perform the displaced store of s9 at [x20 + 0x420].
|
||||
// x and s10, which carry the processed camera input (x is s9 in 1.4.3, s11 in 1.2.1: MouseLayout.input_x), and
|
||||
// perform the displaced store of x at [x20 + 0x420].
|
||||
static void camera_input(Cpu *c) {
|
||||
const unsigned rx = mouse_layout()->input_x;
|
||||
mouse_input_object = 0;
|
||||
uint32_t flags = (uint32_t)c->x[21];
|
||||
int accept = !(flags & 1) && !input_direct_mouse_selector_active();
|
||||
__atomic_store_n(&accepts_mouse, accept, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&last_update, plat_now_ns(), __ATOMIC_RELEASE);
|
||||
MouseSample next = {.object = c->x[20], .when = plat_now_ns(), .camera_enabled = accept};
|
||||
memcpy(&next.pad[0], &c->v[9], sizeof(float));
|
||||
memcpy(&next.pad[0], &c->v[rx], sizeof(float));
|
||||
memcpy(&next.pad[1], &c->v[10], sizeof(float));
|
||||
if (flags & 2) next.pad[0] = -next.pad[0];
|
||||
if (flags & 4) next.pad[1] = -next.pad[1];
|
||||
@@ -167,6 +376,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, next.swing = 1;
|
||||
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;
|
||||
@@ -178,18 +409,20 @@ static void camera_input(Cpu *c) {
|
||||
if (isfinite(ratio) && ratio > 0 && sensitivity > 0 && (dx || dy)) {
|
||||
if (accept) mouse_input_object = c->x[20];
|
||||
float x, y;
|
||||
memcpy(&x, &c->v[9], sizeof x);
|
||||
memcpy(&x, &c->v[rx], 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);
|
||||
c->v[9] = xb;
|
||||
c->v[rx] = xb;
|
||||
c->v[10] = yb;
|
||||
static int logged;
|
||||
if (!logged) {
|
||||
@@ -199,10 +432,23 @@ 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);
|
||||
uint32_t original = (uint32_t)c->v[9];
|
||||
// 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[rx];
|
||||
memcpy((void *)(uintptr_t)(c->x[20] + 0x420), &original, sizeof original);
|
||||
}
|
||||
|
||||
@@ -225,8 +471,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);
|
||||
@@ -245,17 +500,21 @@ static void orbit_begin(Cpu *c) {
|
||||
c->x[8] = guest_pointer(object + 8); // displaced ldr x8, [x0, #8]
|
||||
}
|
||||
|
||||
// The final pitch and yaw are on the stack (1.4.3 sp + 0x44 / 0x48, 1.2.1 sp + 0x3c / 0x40)
|
||||
static void orbit_angles(Cpu *c) {
|
||||
const MouseLayout *ml = mouse_layout();
|
||||
const uint64_t pitch_at = c->sp + ml->orbit_pitch, yaw_at = c->sp + ml->orbit_yaw;
|
||||
uint64_t object = c->x[20];
|
||||
if (orbit.object == object) {
|
||||
float yaw_delta = orbit.mouse.angle[0], pitch_delta = orbit.mouse.angle[1];
|
||||
int fresh = plat_now_ns() - orbit.mouse.when < 150000000ull &&
|
||||
orbit_sample_once(object, orbit.mouse.generation);
|
||||
if (!fresh) yaw_delta = pitch_delta = 0;
|
||||
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) {
|
||||
float pitch = guest_float(c->sp + 0x44), yaw = guest_float(c->sp + 0x48);
|
||||
float pitch = guest_float(pitch_at), yaw = guest_float(yaw_at);
|
||||
float low = guest_float(object + 0xf8), high = guest_float(object + 0xfc);
|
||||
if (!isfinite(low) || !isfinite(high) || low >= high) low = -89.9f, high = 89.9f;
|
||||
low = fmaxf(-89.9f, low); high = fminf(89.9f, high);
|
||||
@@ -263,8 +522,8 @@ static void orbit_angles(Cpu *c) {
|
||||
// Integrate from our last requested angle, then correct the final output even on stationary frames.
|
||||
float base = track->hold_pitch ? track->pitch : pitch;
|
||||
float new_pitch = fminf(high, fmaxf(low, base + pitch_delta));
|
||||
guest_float_store(c->sp + 0x44, new_pitch);
|
||||
guest_float_store(c->sp + 0x48, wrap_degrees(yaw + yaw_delta));
|
||||
guest_float_store(pitch_at, new_pitch);
|
||||
guest_float_store(yaw_at, wrap_degrees(yaw + yaw_delta));
|
||||
guest_float_store(object + 0x108, wrap_degrees(new_pitch - orbit_pitch_offset(object)));
|
||||
guest_float_store(object + 0x120, wrap_degrees(guest_float(object + 0x120) + yaw_delta));
|
||||
if (!track->target_saved) track->original_target = guest_float(object + 0x15c), track->target_saved = 1;
|
||||
@@ -273,31 +532,37 @@ 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);
|
||||
orbit.object = 0;
|
||||
}
|
||||
uint32_t bits;
|
||||
memcpy(&bits, (const void *)(uintptr_t)(c->sp + 0x44), sizeof bits);
|
||||
c->v[12] = bits; // displaced ldr s12, [sp, #0x44]
|
||||
memcpy(&bits, (const void *)(uintptr_t)pitch_at, sizeof bits);
|
||||
c->v[ml->orbit_reg] = bits; // displaced ldr s12, [sp, #0x44] (1.2.1 ldr s13, [sp, #0x3c])
|
||||
}
|
||||
|
||||
// Ultrahand reads the player's manipulation input rather than the camera's processed stick. Insert the mouse
|
||||
// after that controller's per-axis power curves (main+0x1d59460), preserving the keyboard/controller contribution.
|
||||
// after that controller's per-axis power curves (1.4.3 main+0x1d59460, 1.2.1 main+0x176949c), preserving the
|
||||
// keyboard/controller contribution. x is at [sp + 0x60] in 1.4.3 and in s9 in 1.2.1, y in s15 in both.
|
||||
static void hand_input(Cpu *c) {
|
||||
const MouseLayout *ml = mouse_layout();
|
||||
__atomic_store_n(&hand_update, plat_now_ns(), __ATOMIC_RELEASE);
|
||||
static _Thread_local uint64_t object, generation;
|
||||
if (direct_mode()) {
|
||||
MouseSample m = sample_read();
|
||||
if (m.generation != generation || object != c->x[19]) {
|
||||
float x = guest_float(c->sp + 0x60), y;
|
||||
float x, y;
|
||||
if (ml->hand_x_sp) x = guest_float(c->sp + ml->hand_x_sp);
|
||||
else memcpy(&x, &c->v[ml->hand_x_reg], sizeof x);
|
||||
memcpy(&y, &c->v[15], sizeof y);
|
||||
x += m.motion[0];
|
||||
y += m.motion[1];
|
||||
guest_float_store(c->sp + 0x60, x);
|
||||
uint32_t bits;
|
||||
if (ml->hand_x_sp) guest_float_store(c->sp + ml->hand_x_sp, x);
|
||||
else memcpy(&bits, &x, sizeof bits), c->v[ml->hand_x_reg] = bits;
|
||||
memcpy(&bits, &y, sizeof bits);
|
||||
c->v[15] = bits;
|
||||
object = c->x[19]; generation = m.generation;
|
||||
@@ -308,15 +573,478 @@ static void hand_input(Cpu *c) {
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t original = (uint32_t)c->x[8];
|
||||
memcpy((void *)(uintptr_t)(c->sp + 0x48), &original, sizeof original);
|
||||
uint32_t original = (uint32_t)c->x[ml->hand_store_reg]; // displaced str w8, [sp, #0x48] (1.2.1 w9, #0x18)
|
||||
memcpy((void *)(uintptr_t)(c->sp + ml->hand_store_sp), &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 and patches-v121.txt, all at page +0xf80; the return address tells them apart; the main
|
||||
// addresses below are 1.4.3's, load_sites_121 has 1.2.1's):
|
||||
// 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. ROLE_BASE_STORE: a store (str s<t>, [x<n>, #imm]) of base + turn that a
|
||||
// shared helper computed, where the base load itself is in the helper: the stored value and s<t> get the degrees.
|
||||
enum { ROLE_STICK, ROLE_BASE, ROLE_SCALE, ROLE_WATCH, ROLE_BASE_KEEP, ROLE_SHARE, ROLE_BASE_STORE };
|
||||
// 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.
|
||||
// REPEAT: another read of the axis for the same turn before its stick load (1.2.1 reads the stick once for its length
|
||||
// and again for its angle): the same value, and it leaves the degrees to that stick load.
|
||||
enum { LIMIT_160 = 1, SKIP_OFFSET = 2, ONLY_OFFSET = 4, EPSILON = 8, KEEP_STICK = 16, REPEAT = 32 };
|
||||
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 LoadSite {
|
||||
uint32_t offset, insn; // main offset, the original load (ldr / ldur s<t> or w<t>, [x<n>, #imm]) or store
|
||||
uint8_t mode, axis, role, input_reg, flags;
|
||||
} LoadSite;
|
||||
#define PAIR(m, a) ((m) * 2 + (a))
|
||||
static const LoadSite load_sites_143[] = {
|
||||
{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)
|
||||
};
|
||||
// The same camera modes in 1.2.1 (recomp/patches-v121.txt), each site matched to its 1.4.3 one by enclosing function.
|
||||
// The base (the angle a turn is added to) differs where 1.2.1 adds the turn in a shared helper (*p = wrap(*p + s0)):
|
||||
// Aiming's pitch target takes the degrees at the store of the helper's result, Horse's yaw at the turn the helper is
|
||||
// given. Wakeboard, ShieldSurfing and Tail read each stick axis twice (REPEAT on the first read).
|
||||
static const LoadSite load_sites_121[] = {
|
||||
{0x1d34338, 0xbd4426c1, MODE_AIM, 1, ROLE_STICK, 22, 0}, // ldr s1, [x22, #0x424]
|
||||
{0x1d34374, 0xbd015660, MODE_AIM, 1, ROLE_BASE_STORE, 0, 0}, // str s0, [x19, #0x154] (+0x15c + turn)
|
||||
{0x1d34584, 0xbd4422c1, MODE_AIM, 0, ROLE_STICK, 22, 0}, // ldr s1, [x22, #0x420]
|
||||
{0x1d345d0, 0xbc5b83a1, MODE_AIM, 0, ROLE_BASE, 0, 0}, // ldur s1, [x29, #-0x48]
|
||||
{0x1d35fd4, 0xbd4422e0, MODE_SCOPE, 0, ROLE_STICK, 23, 0}, // ldr s0, [x23, #0x420]
|
||||
{0x1d35fe0, 0xbd4426e9, MODE_SCOPE, 1, ROLE_STICK, 23, LIMIT_160}, // ldr s9, [x23, #0x424]
|
||||
{0x1d36280, 0xbd416260, MODE_SCOPE, 1, ROLE_BASE, 0, 0}, // ldr s0, [x19, #0x160]
|
||||
{0x1d36224, 0xbd405be1, MODE_SCOPE, 0, ROLE_BASE, 0, 0}, // ldr s1, [sp, #0x58]
|
||||
{0x1d48478, 0xbd4422cf, MODE_ABILITY, 0, ROLE_STICK, 22, SKIP_OFFSET}, // ldr s15, [x22, #0x420]
|
||||
{0x1d48480, 0xbd4426cc, MODE_ABILITY, 1, ROLE_STICK, 22, SKIP_OFFSET | LIMIT_160}, // ldr s12, [x22, #0x424]
|
||||
{0x1d48bb4, 0xbd416262, MODE_ABILITY, 1, ROLE_BASE, 0, 0}, // ldr s2, [x19, #0x160]
|
||||
{0x1d48b38, 0xbc5a83a1, MODE_ABILITY, 0, ROLE_BASE, 0, 0}, // ldur s1, [x29, #-0x58]
|
||||
// Horse's x19 is the yaw's address (+0x68) here, after a pre-indexed store: a key for the pair, as an object is
|
||||
{0x1d4070c, 0xbd442288, MODE_HORSE, 0, ROLE_STICK, 20, 0}, // ldr s8, [x20, #0x420]
|
||||
{0x1d40740, 0xbd401be0, MODE_HORSE, 0, ROLE_BASE, 0, 0}, // ldr s0, [sp, #0x18] (the yaw turn)
|
||||
{0x1d408f8, 0xbd4426a8, MODE_HORSE, 1, ROLE_STICK, 21, 0}, // ldr s8, [x21, #0x424]
|
||||
{0x1d409ac, 0xbd406661, MODE_HORSE, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x64]
|
||||
{0x1d48ccc, 0xbd4422c9, MODE_ABILITY_OFFSET, 0, ROLE_STICK, 22, ONLY_OFFSET}, // ldr s9, [x22, #0x420]
|
||||
{0x1d48cf0, 0xbd41da61, MODE_ABILITY_OFFSET, 0, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x1d8]
|
||||
{0x1d48cfc, 0xbd4426c9, MODE_ABILITY_OFFSET, 1, ROLE_STICK, 22, ONLY_OFFSET}, // ldr s9, [x22, #0x424]
|
||||
{0x1d48d18, 0xbd41de61, MODE_ABILITY_OFFSET, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x1dc]
|
||||
{0x1d485f8, 0xbd400400, MODE_ABILITY_OFFSET, 1, ROLE_SCALE, 0, 0}, // ldr s0, [x0, #0x4] (P.y)
|
||||
{0x1d37b44, 0xbd4426ad, MODE_FINDER, 1, ROLE_STICK, 21, 0}, // ldr s13, [x21, #0x424]
|
||||
{0x1d37b68, 0xbd405261, MODE_FINDER, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x50]
|
||||
{0x1d37bcc, 0xbd4422ad, MODE_FINDER, 0, ROLE_STICK, 21, 0}, // ldr s13, [x21, #0x420]
|
||||
{0x1d37bf4, 0xbd405be1, MODE_FINDER, 0, ROLE_BASE, 0, 0}, // ldr s1, [sp, #0x58]
|
||||
{0x1d38e08, 0xbd442301, MODE_CLIMB, 0, ROLE_STICK, 24, 0}, // ldr s1, [x24, #0x420]
|
||||
{0x1d38e10, 0xbd409261, MODE_CLIMB, 0, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x90] (yaw)
|
||||
{0x1d38dac, 0xbd442701, MODE_CLIMB, 1, ROLE_STICK, 24, 0}, // ldr s1, [x24, #0x424]
|
||||
{0x1d38db8, 0xbd4002c1, MODE_CLIMB, 1, ROLE_BASE, 0, 0}, // ldr s1, [x22] (pitch +0x88, as 1.4.3)
|
||||
{0x1d50738, 0xbd4422c0, MODE_WAKEBOARD, 0, ROLE_STICK, 22, EPSILON | REPEAT}, // ldr s0, [x22, #0x420]
|
||||
{0x1d50740, 0xbd4426c1, MODE_WAKEBOARD, 1, ROLE_STICK, 22, KEEP_STICK | REPEAT}, // ldr s1, [x22, #0x424]
|
||||
{0x1d50768, 0xbd4426c0, MODE_WAKEBOARD, 1, ROLE_STICK, 22, KEEP_STICK}, // ldr s0, [x22, #0x424]
|
||||
{0x1d5076c, 0xbd4422c1, MODE_WAKEBOARD, 0, ROLE_STICK, 22, EPSILON}, // ldr s1, [x22, #0x420]
|
||||
{0x1d50e5c, 0xbd40da60, MODE_WAKEBOARD, 1, ROLE_BASE, 0, 0}, // ldr s0, [x19, #0xd8] (pitch)
|
||||
{0x1d508bc, 0xbd401fa0, MODE_WAKEBOARD, 1, ROLE_SHARE, 0, 0}, // ldr s0, [x29, #0x1c] (pitch turn)
|
||||
{0x1d50f20, 0xbd403bec, MODE_WAKEBOARD, 0, ROLE_BASE, 0, 0}, // ldr s12, [sp, #0x38] (yaw)
|
||||
{0x1d46108, 0xbd4422e0, MODE_SHIELD, 0, ROLE_STICK, 23, EPSILON | REPEAT}, // ldr s0, [x23, #0x420]
|
||||
{0x1d46110, 0xbd4426e1, MODE_SHIELD, 1, ROLE_STICK, 23, KEEP_STICK | REPEAT}, // ldr s1, [x23, #0x424]
|
||||
{0x1d46138, 0xbd4426e0, MODE_SHIELD, 1, ROLE_STICK, 23, KEEP_STICK}, // ldr s0, [x23, #0x424]
|
||||
{0x1d4613c, 0xbd4422e1, MODE_SHIELD, 0, ROLE_STICK, 23, EPSILON}, // ldr s1, [x23, #0x420]
|
||||
{0x1d46788, 0xbd40de60, MODE_SHIELD, 1, ROLE_BASE, 0, 0}, // ldr s0, [x19, #0xdc] (pitch)
|
||||
{0x1d46910, 0xbd401fa0, MODE_SHIELD, 1, ROLE_SHARE, 0, 0}, // ldr s0, [x29, #0x1c] (pitch turn)
|
||||
{0x1d467b4, 0xbd403bec, MODE_SHIELD, 0, ROLE_BASE, 0, 0}, // ldr s12, [sp, #0x38] (yaw)
|
||||
{0x1d51e10, 0xbd4422cd, MODE_WATERFALL, 0, ROLE_STICK, 22, 0}, // ldr s13, [x22, #0x420]
|
||||
{0x1d51fb8, 0xb9403be8, MODE_WATERFALL, 0, ROLE_BASE, 0, 0}, // ldr w8, [sp, #0x38] (yaw, as bits)
|
||||
{0x1d51f48, 0xbd4426c9, MODE_WATERFALL, 1, ROLE_WATCH, 22, 0}, // ldr s9, [x22, #0x424] (height, stick)
|
||||
{0x1d4a11c, 0xbd442280, MODE_TAIL, 0, ROLE_STICK, 20, REPEAT}, // ldr s0, [x20, #0x420]
|
||||
{0x1d4a124, 0xbd442681, MODE_TAIL, 1, ROLE_STICK, 20, REPEAT}, // ldr s1, [x20, #0x424]
|
||||
{0x1d4a14c, 0xbd442680, MODE_TAIL, 1, ROLE_STICK, 20, 0}, // ldr s0, [x20, #0x424]
|
||||
{0x1d4a150, 0xbd442281, MODE_TAIL, 0, ROLE_STICK, 20, 0}, // ldr s1, [x20, #0x420]
|
||||
{0x1d4a4a0, 0xbd40c260, MODE_TAIL, 0, ROLE_BASE, 0, 0}, // ldr s0, [x19, #0xc0] (yaw)
|
||||
{0x1d4a514, 0xbd40b661, MODE_TAIL, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0xb4] (pitch)
|
||||
{0x1d4c930, 0xbd442320, MODE_LOCKON, 0, ROLE_WATCH, 25, 0}, // ldr s0, [x25, #0x420] (the input test)
|
||||
{0x1d4c958, 0xbd442321, MODE_LOCKON, 0, ROLE_STICK, 25, 0}, // ldr s1, [x25, #0x420]
|
||||
{0x1d4c97c, 0xbd402be0, MODE_LOCKON, 0, ROLE_BASE_KEEP, 0, 0}, // ldr s0, [sp, #0x28] (the yaw turn)
|
||||
{0x1d4c98c, 0xbd402be9, MODE_LOCKON, 0, ROLE_BASE, 0, 0}, // ldr s9, [sp, #0x28]
|
||||
{0x1d4cd5c, 0xbd442721, MODE_LOCKON, 1, ROLE_STICK, 25, 0}, // ldr s1, [x25, #0x424]
|
||||
{0x1d4cd80, 0xbd402be0, MODE_LOCKON, 1, ROLE_BASE_KEEP, 0, 0}, // ldr s0, [sp, #0x28] (the pitch turn)
|
||||
{0x1d4cd90, 0xbd402beb, MODE_LOCKON, 1, ROLE_BASE, 0, 0}, // ldr s11, [sp, #0x28]
|
||||
{0x1d443e8, 0xbd4426a9, MODE_LOCKON_AIM, 1, ROLE_STICK, 21, 0}, // ldr s9, [x21, #0x424]
|
||||
{0x1d44430, 0xbd419a61, MODE_LOCKON_AIM, 1, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x198] (pitch offset)
|
||||
{0x1d44444, 0xbd4422a9, MODE_LOCKON_AIM, 0, ROLE_STICK, 21, 0}, // ldr s9, [x21, #0x420]
|
||||
{0x1d44490, 0xbd419e61, MODE_LOCKON_AIM, 0, ROLE_BASE, 0, 0}, // ldr s1, [x19, #0x19c] (yaw offset)
|
||||
{0x1d418dc, 0xbd44234b, MODE_HORSE_LOCKON, 0, ROLE_WATCH, 26, 0}, // ldr s11, [x26, #0x420]
|
||||
// EventMultiTalk and EventTalkManual: pre-indexed angle loads, made plain loads by the patched mov before each
|
||||
{0x1d3a3f8, 0xbd44270b, MODE_TALK, 1, ROLE_STICK, 24, 0}, // ldr s11, [x24, #0x424]
|
||||
{0x1d3a420, 0xbd4002e1, MODE_TALK, 1, ROLE_BASE, 0, 0}, // ldr s1, [x23] (pitch +0x5c)
|
||||
{0x1d3a484, 0xbd44230b, MODE_TALK, 0, ROLE_STICK, 24, EPSILON}, // ldr s11, [x24, #0x420] (also the test)
|
||||
{0x1d3a4e4, 0xbd4002c1, MODE_TALK, 0, ROLE_BASE, 0, 0}, // ldr s1, [x22] (yaw +0x64)
|
||||
{0x1d3e2b0, 0xbd442729, MODE_TALK_MANUAL, 1, ROLE_STICK, 25, 0}, // ldr s9, [x25, #0x424]
|
||||
{0x1d3e2d8, 0xbd400301, MODE_TALK_MANUAL, 1, ROLE_BASE, 0, 0}, // ldr s1, [x24] (pitch +0x74)
|
||||
{0x1d3e33c, 0xbd442329, MODE_TALK_MANUAL, 0, ROLE_STICK, 25, 0}, // ldr s9, [x25, #0x420]
|
||||
{0x1d3e364, 0xbd4002c1, MODE_TALK_MANUAL, 0, ROLE_BASE, 0, 0}, // ldr s1, [x22] (yaw +0x7c)
|
||||
};
|
||||
|
||||
static const MouseLayout layout_143 = {
|
||||
.input_x = 9, .activity_sp = 8, .orbit_pitch = 0x44, .orbit_yaw = 0x48, .orbit_reg = 12,
|
||||
.hand_x_sp = 0x60, .hand_store_reg = 8, .hand_store_sp = 0x48,
|
||||
.player_root_slot = 0x3969918, .main_size = 0x3adf000,
|
||||
.loads = load_sites_143, .load_count = sizeof load_sites_143 / sizeof load_sites_143[0]};
|
||||
static const MouseLayout layout_121 = {
|
||||
.input_x = 11, .activity_sp = 0x10, .orbit_pitch = 0x3c, .orbit_yaw = 0x40, .orbit_reg = 13,
|
||||
.hand_x_reg = 9, .hand_store_reg = 9, .hand_store_sp = 0x18,
|
||||
.player_root_slot = 0, .main_size = 0x472b000,
|
||||
.loads = load_sites_121, .load_count = sizeof load_sites_121 / sizeof load_sites_121[0]};
|
||||
// Only 1.4.3 and 1.2.1 have the mouse_direct set (1.1.0's table has none, so direct_mode() is off and no hook runs)
|
||||
static const MouseLayout *mouse_layout(void) { return RT_V143 ? &layout_143 : &layout_121; }
|
||||
|
||||
// The original single-register 32-bit loads: LDR S / W (unsigned offset), LDUR S and LDUR W (a float's bits in a
|
||||
// general register), and for ROLE_BASE_STORE the store STR S (unsigned offset). 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 & 0xfb800000u) == 0xb9000000u) return base + (((insn >> 10) & 0xfff) << 2); // LDR / STR, S or W
|
||||
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 || (insn & 0xffc00000u) == 0xb9400000u; // LDUR W, LDR W
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (!(s->flags & REPEAT)) load_pending[pair].object = 0;
|
||||
MouseSample m = sample_read();
|
||||
if (m.object != input || !m.camera_enabled || plat_now_ns() - m.when >= 150000000ull) return f;
|
||||
// A step without the mouse in it keeps the game's value as it is: the gamepad's stick and anything else the game
|
||||
// put there after the input update (the sample's pad is only the stick the update stored)
|
||||
if (!m.angle[0] && !m.angle[1] && !m.swing) 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 (s->flags & REPEAT) return out;
|
||||
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 degrees ? f + degrees : f; // f + 0 would turn a -0 the game loads into +0
|
||||
}
|
||||
|
||||
// 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 (MouseLayout.loads): performs the load, then gives the register the value
|
||||
static void camera_load(Cpu *c) {
|
||||
uint64_t offset = c->x[30] - 4 - MOD_MAIN_BASE;
|
||||
const MouseLayout *ml = mouse_layout();
|
||||
const LoadSite *s = NULL;
|
||||
for (size_t i = 0; i < ml->load_count; i++)
|
||||
if (ml->loads[i].offset == offset) { s = &ml->loads[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);
|
||||
if (s->role == ROLE_BASE_STORE) { // performs the store, of the value plus the degrees when there are any
|
||||
uint32_t bits = (uint32_t)c->v[s->insn & 31];
|
||||
float f, g;
|
||||
memcpy(&f, &bits, sizeof f);
|
||||
if (direct_mode() && (g = load_base(s, c, f)) != f) {
|
||||
f = wrap_degrees(g);
|
||||
memcpy(&bits, &f, sizeof bits);
|
||||
c->v[s->insn & 31] = bits; // the code after the store goes on with the value in the register
|
||||
}
|
||||
memcpy((void *)(uintptr_t)at, &bits, sizeof bits);
|
||||
return;
|
||||
}
|
||||
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,
|
||||
// bits 3 and 5 processed input below the threshold), so they are changed in w8; a write to +0x418 would be overwritten
|
||||
// by that store. No flag is changed for controller-only input.
|
||||
// (1.2.1 [sp, #0x10]) just before the update's last `str w8, [x20, #0x418]`: w8 holds the flags then (bit 0 raw
|
||||
// input, bit 4 any input, bits 3 and 5 processed input below the threshold), so they are changed in w8; a write to
|
||||
// +0x418 would be overwritten by that store. No flag is changed for controller-only input.
|
||||
static void camera_activity(Cpu *c) {
|
||||
if (mouse_input_object && mouse_input_object == c->x[20]) {
|
||||
uint32_t flags = (uint32_t)c->x[8];
|
||||
@@ -324,8 +1052,8 @@ static void camera_activity(Cpu *c) {
|
||||
mouse_input_object = 0;
|
||||
}
|
||||
uint64_t d9, d8;
|
||||
memcpy(&d9, (const void *)(uintptr_t)(c->sp + 8), sizeof d9);
|
||||
memcpy(&d8, (const void *)(uintptr_t)(c->sp + 16), sizeof d8);
|
||||
memcpy(&d9, (const void *)(uintptr_t)(c->sp + mouse_layout()->activity_sp), sizeof d9);
|
||||
memcpy(&d8, (const void *)(uintptr_t)(c->sp + mouse_layout()->activity_sp + 8), sizeof d8);
|
||||
c->v[9] = d9;
|
||||
c->v[8] = d8;
|
||||
}
|
||||
@@ -336,5 +1064,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
|
||||
+9
-1
@@ -29,10 +29,18 @@ typedef struct {
|
||||
enum { MOVIE_EV_INPUT, MOVIE_EV_OUTPUT, MOVIE_EV_FORMAT, MOVIE_EV_ERROR }; // DecoderEvents: EventType * per event
|
||||
enum { MOVIE_TYPE_VP9 = 3, MOVIE_MODE_BUFFER = 1, MOVIE_FORMAT_NV12 = 4, MOVIE_FLAG_EOS = 4 };
|
||||
|
||||
typedef struct { uint64_t frames, decode_ns, max_ns, errors, dropped, handed; } MovieStats;
|
||||
typedef struct { uint64_t frames, decode_ns, max_ns, errors, dropped, handed, fit_ns; } MovieStats;
|
||||
// fit_ns: the part of decode_ns spent writing the NV12 frame
|
||||
|
||||
void movie_set_guest(const MovieGuest *g); // probe: stand-ins instead of the modules
|
||||
void movie_set_clock(uint64_t (*clock)(void)); // probe: a fake clock (ns) for the pacing; NULL = real
|
||||
void movie_set_pace(int on); // probe: 0 hands every decoded frame over at the next tick
|
||||
int movie_stats(uint64_t obj, MovieStats *out); // counters of the decoder at guest object obj; 0 if none
|
||||
int movie_quiet(uint64_t obj); // 1 when its worker is idle and cannot take more input now
|
||||
|
||||
// Keeping a movie's shape on an output of another shape (ov_movie.c): the picture is shrunk inside the frame, with
|
||||
// black (Y 16, U = V 128) around it. movie_fit_new returns NULL when the shapes agree within 1%: copy the frame.
|
||||
typedef struct MovieFit MovieFit;
|
||||
MovieFit *movie_fit_new(int w, int h, unsigned out_w, unsigned out_h);
|
||||
void movie_fit_apply(const MovieFit *f, const uint8_t *const plane[3], const int stride[3], uint8_t *dst);
|
||||
void movie_fit_free(MovieFit *f);
|
||||
@@ -9,6 +9,7 @@
|
||||
// Without the RomFS (no game data) it prints why and exits 0. TOTK_PROBE_MOVIE_SHOT=<file.ppm> saves the middle frame.
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -267,7 +268,104 @@ static int paced(uint64_t obj, const uint8_t *data, const Track *t, int limit, u
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The shape-keeping resampler (ov_movie.c movie_fit_new / movie_fit_apply) on a synthetic 1920x1080 I420 picture, which
|
||||
// needs no game data. The planes are gradients, which a box filter keeps linear, so every output sample has an expected
|
||||
// value: luma rises left to right and top to bottom, U rises left to right, V falls top to bottom. A 16:9 output must give
|
||||
// no plan; the others must give bars (Y 16, U = V 128) that are symmetric, a picture of the movie's shape once the game
|
||||
// stretches the frame, and samples of the picture where the gradients say. TOTK_PROBE_MOVIE_BENCH=1 also times the frame.
|
||||
#define FIT_CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("probe-movie: FAILED: "); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
rc = 1; \
|
||||
goto done; \
|
||||
} \
|
||||
} while (0)
|
||||
static int check_fit(void) {
|
||||
enum { W = 1920, H = 1080, CW = W / 2, CH = H / 2 };
|
||||
int rc = 0;
|
||||
MovieFit *f = NULL;
|
||||
uint8_t *y = malloc((size_t)W * H), *u = malloc((size_t)CW * CH), *v = malloc((size_t)CW * CH);
|
||||
uint8_t *out = malloc((size_t)W * H * 3 / 2);
|
||||
FIT_CHECK(y && u && v && out, "fit test buffers");
|
||||
for (int j = 0; j < H; j++)
|
||||
for (int i = 0; i < W; i++) y[j * W + i] = (uint8_t)(32 + 150.0 * (i + 0.5) / W + 60.0 * (j + 0.5) / H + 0.5);
|
||||
for (int j = 0; j < CH; j++)
|
||||
for (int i = 0; i < CW; i++) {
|
||||
u[j * CW + i] = (uint8_t)(40 + 150.0 * (i + 0.5) / CW + 0.5);
|
||||
v[j * CW + i] = (uint8_t)(220 - 150.0 * (j + 0.5) / CH + 0.5);
|
||||
}
|
||||
const uint8_t *plane[3] = {y, u, v};
|
||||
const int stride[3] = {W, CW, CW};
|
||||
FIT_CHECK(!movie_fit_new(W, H, 1920, 1080) && !movie_fit_new(W, H, 2560, 1440), "16:9 outputs need no plan");
|
||||
static const unsigned outs[][2] = {{5120, 1440}, {3440, 1440}, {1280, 800}, {1920, 1200}};
|
||||
for (int k = 0; k < 4; k++) {
|
||||
unsigned ow = outs[k][0], oh = outs[k][1];
|
||||
f = movie_fit_new(W, H, ow, oh);
|
||||
FIT_CHECK(f, "%ux%u: no plan", ow, oh);
|
||||
memset(out, 0xAA, (size_t)W * H * 3 / 2); // a byte nothing else produces: every pixel must be written
|
||||
movie_fit_apply(f, plane, stride, out);
|
||||
const uint8_t *uv = out + (size_t)W * H;
|
||||
int l = 0, r = W - 1, t = 0, b = H - 1; // the picture's extent: where luma leaves 16, on the middle row and column
|
||||
while (l < W && out[(H / 2) * W + l] == 16) l++;
|
||||
while (r >= 0 && out[(H / 2) * W + r] == 16) r--;
|
||||
while (t < H && out[t * W + W / 2] == 16) t++;
|
||||
while (b >= 0 && out[b * W + W / 2] == 16) b--;
|
||||
int pw = r - l + 1, ph = b - t + 1;
|
||||
FIT_CHECK(pw > 2 && ph > 2, "%ux%u: no picture found", ow, oh);
|
||||
FIT_CHECK(abs(l - (W - 1 - r)) <= 2 && abs(t - (H - 1 - b)) <= 2,
|
||||
"%ux%u: bars not symmetric: left %d right %d, top %d bottom %d", ow, oh, l, W - 1 - r, t, H - 1 - b);
|
||||
// On screen the picture covers pw/W of the width and ph/H of the height of an ow x oh canvas; 1 = the movie's shape.
|
||||
double shape = ((double)pw / W * ow) / ((double)ph / H * oh) / ((double)W / H);
|
||||
FIT_CHECK(fabs(shape - 1) < 0.01, "%ux%u: picture %dx%d shows at %.3f of the movie's shape", ow, oh, pw, ph, shape);
|
||||
FIT_CHECK(l == 0 || ph == H, "%ux%u: sideways bars with a squeezed height", ow, oh);
|
||||
for (int j = 0; j < H; j++) // everything outside the picture is black in luma and chroma, left and right alike
|
||||
for (int i = 0; i < W; i++)
|
||||
if (j < t || j > b || i < l || i > r)
|
||||
FIT_CHECK(out[j * W + i] == 16, "%ux%u: luma (%d,%d) is %d, not black", ow, oh, i, j, out[j * W + i]);
|
||||
for (int j = 0; j < CH; j++)
|
||||
for (int i = 0; i < CW; i++) {
|
||||
int outside = 2 * j + 1 < t || 2 * j > b || 2 * i + 1 < l || 2 * i > r;
|
||||
const uint8_t *c = uv + (size_t)j * W + 2 * i;
|
||||
if (outside) FIT_CHECK(c[0] == 128 && c[1] == 128, "%ux%u: chroma (%d,%d) is %d/%d, not black", ow, oh, i, j,
|
||||
c[0], c[1]);
|
||||
}
|
||||
static const double fr[] = {0.1, 0.35, 0.5, 0.8, 0.95}; // samples of the picture, as fractions of it
|
||||
for (int a = 0; a < 5; a++)
|
||||
for (int c = 0; c < 5; c++) {
|
||||
int px = (int)(pw * fr[a]), py = (int)(ph * fr[c]); // picture pixel
|
||||
double sx = (px + 0.5) * W / pw, sy = (py + 0.5) * H / ph; // where it comes from in the source
|
||||
double ey = 32 + 150.0 * sx / W + 60.0 * sy / H;
|
||||
int got = out[(t + py) * W + l + px];
|
||||
FIT_CHECK(fabs(got - ey) <= 2, "%ux%u: luma at picture (%d,%d) is %d, want %.1f", ow, oh, px, py, got, ey);
|
||||
int cx = px / 2, cy = py / 2, cpw = pw / 2, cph = ph / 2;
|
||||
double sux = (cx + 0.5) * CW / cpw, svy = (cy + 0.5) * CH / cph;
|
||||
const uint8_t *q = uv + (size_t)(t / 2 + cy) * W + 2 * (l / 2 + cx);
|
||||
double eu = 40 + 150.0 * sux / CW, ev = 220 - 150.0 * svy / CH;
|
||||
FIT_CHECK(fabs(q[0] - eu) <= 3 && fabs(q[1] - ev) <= 3,
|
||||
"%ux%u: chroma at picture (%d,%d) is U %d V %d, want U %.1f V %.1f", ow, oh, cx, cy, q[0], q[1], eu,
|
||||
ev);
|
||||
}
|
||||
if (cfg_get("TOTK_PROBE_MOVIE_BENCH")) {
|
||||
uint64_t t0 = plat_now_ns();
|
||||
for (int i = 0; i < 100; i++) movie_fit_apply(f, plane, stride, out);
|
||||
printf("probe-movie: fit %ux%u: %.2f ms per frame\n", ow, oh, (plat_now_ns() - t0) / 1e8);
|
||||
}
|
||||
printf("probe-movie: fit %ux%u: picture %dx%d at (%d,%d) of the frame, bars Y 16 / UV 128, gradients as expected\n",
|
||||
ow, oh, pw, ph, l, t);
|
||||
movie_fit_free(f);
|
||||
f = NULL;
|
||||
}
|
||||
done:
|
||||
movie_fit_free(f);
|
||||
free(y), free(u), free(v), free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int probe_movie(void) {
|
||||
if (check_fit()) return 1;
|
||||
const char *dir = rt_config()->romfs;
|
||||
char image[PLAT_PATH_MAX];
|
||||
snprintf(image, sizeof image, "%s/romfs.bin", dir);
|
||||
|
||||
+11
-5
@@ -11,6 +11,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "gfx.h"
|
||||
#include "host.h"
|
||||
#include "nvn_api.h"
|
||||
@@ -667,8 +668,12 @@ static void sleep_until(uint64_t deadline) {
|
||||
}
|
||||
|
||||
// NVNwindowAcquireTextureResult nvnWindowAcquireTexture(NVNwindow *w, NVNsync *s, int *index): blocks here, not in the
|
||||
// present, until the image's previous present plus one period has passed (the game often presents with a lock held).
|
||||
// That is the time the next frame goes to the display, so the image is free as soon as that time is reached.
|
||||
// present, until the image is free (the game often presents with a lock held). Without the frame queue that is the
|
||||
// image's previous present plus one period, when the next frame goes to the display, so a frame gets exactly one period
|
||||
// from acquire to present. Its present is a copy already on the queue by its display time, so with the frame queue
|
||||
// (TOTK_FRAME_QUEUE=1) the image is free from that time on: the game draws the next frame while the previous one waits
|
||||
// for its turn, and a frame that takes up to one more period still makes its slot, for up to one frame more latency.
|
||||
// Town at 4K with DLAA at 60 on an RTX 5070: frames over 17.5 ms 1-8 per 30 s with the queue against 25-33 without.
|
||||
NVN_API(nvnWindowAcquireTexture) {
|
||||
win_t *o = arg_p(c, 0);
|
||||
sync_t *s = arg_p(c, 1);
|
||||
@@ -693,8 +698,9 @@ NVN_API(nvnWindowAcquireTexture) {
|
||||
|
||||
// void nvnQueuePresentTexture(NVNqueue *q, NVNwindow *w, int index): queues the copy and returns at once. The display
|
||||
// time comes from the pacer: the back end's present thread holds the host present until then (back-edge pacing, as the
|
||||
// Switch's display flips at vblank), so the present cadence does not follow the replay time; one period later the
|
||||
// image is free for the next acquire. Added latency: the slack of the frame (period minus its work), as on the Switch.
|
||||
// Switch's display flips at vblank), so the present cadence does not follow the replay time; one period later (at that
|
||||
// time with the frame queue) the image is free for the next acquire. Added latency: the slack of the frame (period minus
|
||||
// its work), as on the Switch, plus up to a period with the frame queue.
|
||||
NVN_API(nvnQueuePresentTexture) {
|
||||
win_t *o = arg_p(c, 1);
|
||||
int idx = (int)arg_x(c, 2);
|
||||
@@ -709,7 +715,7 @@ NVN_API(nvnQueuePresentTexture) {
|
||||
uint64_t slot = now + pace(&o->pace, now, period);
|
||||
gfx_backend()->present(t, o->crop, slot);
|
||||
o->ticket[idx] = o->presents;
|
||||
o->avail[idx] = slot + period;
|
||||
o->avail[idx] = cfg_bool("TOTK_FRAME_QUEUE") ? slot : slot + period;
|
||||
frame_step_present(slot, o->interval); // frame_rate.c: the real-time step follows the display times
|
||||
rt_frame_presented(t, o->presents);
|
||||
}
|
||||
+196
-4
@@ -73,6 +73,7 @@ typedef struct {
|
||||
unsigned owed; // events to signal on the next guest thread (bit per MOVIE_EV_*)
|
||||
uint64_t t0; // host time of the first frame handed over (pacing)
|
||||
int64_t pts0; // its presentation time (us)
|
||||
MovieFit *fit; // picture shrunk inside the frame; NULL = the shapes agree, copy
|
||||
MovieStats st;
|
||||
PlatMutex mu;
|
||||
PlatCond work, idle;
|
||||
@@ -207,6 +208,173 @@ static void to_nv12(const vpx_image_t *img, uint8_t *dst, int w, int h) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Keeping the movie's shape. The game draws a decoded frame over the whole canvas, so on an output that is not 16:9
|
||||
// (output_res in totk.ini) the 1920x1080 movies would be stretched. The frame keeps its size; the picture is shrunk
|
||||
// inside it and the rest is black, so that after the game's stretch the picture has the movie's own shape again: a wider
|
||||
// output gets a squeezed picture with black at the left and right, a taller one (16:10) black above and below.
|
||||
// The shrink is a box filter (each output pixel averages the source pixels it covers, weights in 1/4096), separable, with
|
||||
// the per-column and per-row source spans and weights built once per movie (movie_fit_new), so the frame loop only
|
||||
// multiplies and adds.
|
||||
enum { FIT_ONE = 4096, FIT_Y_BLACK = 16, FIT_UV_BLACK = 128 }; // black of limited-range video: Y 16, U = V 128
|
||||
typedef struct { int n, taps; int *start; uint16_t *wt; } FitAxis; // output i: taps weights from source start[i]
|
||||
struct MovieFit {
|
||||
int w, h, x0, y0, pw, ph; // frame, and the picture's corner and size in it (all even)
|
||||
FitAxis lx, ly, cx, cy; // luma, chroma; columns, rows; n = 0 where the axis is kept
|
||||
uint8_t *tmp; // horizontally shrunk rows, before the vertical pass
|
||||
uint32_t *acc; // one row of the vertical sum
|
||||
};
|
||||
|
||||
// Box filter from src_n to dst_n (dst_n < src_n): output i covers [i, i + 1) * src_n / dst_n of the source.
|
||||
static int axis_build(FitAxis *a, int src_n, int dst_n) {
|
||||
double s = (double)src_n / dst_n;
|
||||
a->n = dst_n;
|
||||
a->taps = (int)s + 2 > src_n ? src_n : (int)s + 2;
|
||||
a->start = malloc((size_t)dst_n * sizeof *a->start);
|
||||
a->wt = malloc((size_t)dst_n * (size_t)a->taps * sizeof *a->wt);
|
||||
if (!a->start || !a->wt) return 0;
|
||||
for (int i = 0; i < dst_n; i++) {
|
||||
double lo = i * s, hi = (i + 1) * s;
|
||||
int st = (int)lo;
|
||||
if (st > src_n - a->taps) st = src_n - a->taps;
|
||||
a->start[i] = st;
|
||||
int sum = 0, big = 0;
|
||||
uint16_t *w = a->wt + (size_t)i * a->taps;
|
||||
for (int t = 0; t < a->taps; t++) {
|
||||
double ov = (st + t + 1 < hi ? st + t + 1 : hi) - (st + t > lo ? st + t : lo);
|
||||
w[t] = ov > 0 ? (uint16_t)(ov / s * FIT_ONE + 0.5) : 0;
|
||||
sum += w[t];
|
||||
if (w[t] > w[big]) big = t;
|
||||
}
|
||||
w[big] = (uint16_t)(w[big] + FIT_ONE - sum); // the weights add up to exactly 1
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static void axis_free(FitAxis *a) {
|
||||
free(a->start);
|
||||
free(a->wt);
|
||||
}
|
||||
|
||||
void movie_fit_free(MovieFit *f) {
|
||||
if (!f) return;
|
||||
axis_free(&f->lx), axis_free(&f->ly), axis_free(&f->cx), axis_free(&f->cy);
|
||||
free(f->tmp);
|
||||
free(f->acc);
|
||||
free(f);
|
||||
}
|
||||
|
||||
// The plan for a w x h movie shown on an ow x oh output; NULL when the shapes agree within 1% (the frame is copied).
|
||||
MovieFit *movie_fit_new(int w, int h, unsigned ow, unsigned oh) {
|
||||
if (w < 16 || h < 16 || (w | h) & 1 || !ow || !oh) return NULL;
|
||||
double movie = (double)w / h, out = (double)ow / oh, r = out / movie;
|
||||
if (r > 0.99 && r < 1.01) return NULL;
|
||||
MovieFit *f = calloc(1, sizeof *f);
|
||||
if (!f) {
|
||||
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
|
||||
return NULL;
|
||||
}
|
||||
f->w = w, f->h = h, f->pw = w, f->ph = h;
|
||||
if (r > 1) f->pw = (int)(w / r / 2 + 0.5) * 2; // wider output: squeeze sideways
|
||||
else f->ph = (int)(h * r / 2 + 0.5) * 2; // taller output: squeeze vertically
|
||||
if (f->pw < 2) f->pw = 2;
|
||||
if (f->ph < 2) f->ph = 2;
|
||||
f->x0 = (w - f->pw) / 2 & ~1;
|
||||
f->y0 = (h - f->ph) / 2 & ~1;
|
||||
int ok = 1;
|
||||
if (f->pw < w) ok = axis_build(&f->lx, w, f->pw) && axis_build(&f->cx, w / 2, f->pw / 2);
|
||||
if (f->ph < h) ok = ok && axis_build(&f->ly, h, f->ph) && axis_build(&f->cy, h / 2, f->ph / 2);
|
||||
f->tmp = malloc((size_t)f->pw * h);
|
||||
f->acc = malloc((size_t)f->pw * sizeof *f->acc);
|
||||
if (!ok || !f->tmp || !f->acc) {
|
||||
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
|
||||
movie_fit_free(f);
|
||||
return NULL;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
// One output row of the horizontal pass: dst gets the shrunk src row (dstep 2 writes every other byte, for U and V).
|
||||
// The tap count is a constant in the 3 and 4 tap copies (what 2x and below squeezes need), so the compiler unrolls the sum.
|
||||
static inline __attribute__((always_inline)) void fit_row_taps(const FitAxis *a, const uint8_t *src, uint8_t *dst,
|
||||
int dstep, int taps) {
|
||||
for (int i = 0; i < a->n; i++) {
|
||||
const uint8_t *p = src + a->start[i];
|
||||
const uint16_t *w = a->wt + (size_t)i * taps;
|
||||
uint32_t v = FIT_ONE / 2;
|
||||
for (int t = 0; t < taps; t++) v += w[t] * p[t];
|
||||
dst[(size_t)i * dstep] = (uint8_t)(v / FIT_ONE);
|
||||
}
|
||||
}
|
||||
static void fit_row_h(const FitAxis *a, const uint8_t *src, uint8_t *dst, int dstep) {
|
||||
if (a->taps == 3) fit_row_taps(a, src, dst, dstep, 3);
|
||||
else if (a->taps == 4) fit_row_taps(a, src, dst, dstep, 4);
|
||||
else fit_row_taps(a, src, dst, dstep, a->taps);
|
||||
}
|
||||
|
||||
// One plane (sw x sh) shrunk into the pw x ph picture whose corner (ox, oy) is in dst; ax/ay NULL = that axis is kept.
|
||||
static void fit_plane(const MovieFit *f, const uint8_t *src, int ss, int sw, int sh, uint8_t *dst, int ds, int dstep,
|
||||
int ox, int oy, const FitAxis *ax, const FitAxis *ay, int pw, int ph) {
|
||||
uint8_t *base = dst + (size_t)oy * ds + (size_t)ox * dstep;
|
||||
if (!ay) { // sideways only: straight into the frame
|
||||
for (int y = 0; y < sh; y++) {
|
||||
const uint8_t *row = src + (size_t)y * ss;
|
||||
uint8_t *o = base + (size_t)y * ds;
|
||||
if (ax) fit_row_h(ax, row, o, dstep);
|
||||
else if (dstep == 1) memcpy(o, row, (size_t)sw);
|
||||
else for (int x = 0; x < sw; x++) o[(size_t)x * dstep] = row[x];
|
||||
}
|
||||
return;
|
||||
}
|
||||
const uint8_t *in = src;
|
||||
int is = ss;
|
||||
if (ax) {
|
||||
for (int y = 0; y < sh; y++) fit_row_h(ax, src + (size_t)y * ss, f->tmp + (size_t)y * pw, 1);
|
||||
in = f->tmp, is = pw;
|
||||
}
|
||||
for (int y = 0; y < ph; y++) {
|
||||
const uint16_t *w = ay->wt + (size_t)y * ay->taps;
|
||||
const uint8_t *r0 = in + (size_t)ay->start[y] * is;
|
||||
uint8_t *o = base + (size_t)y * ds;
|
||||
if (ay->taps == 3) { // the common case, summed in registers
|
||||
const uint8_t *r1 = r0 + is, *r2 = r1 + is;
|
||||
for (int x = 0; x < pw; x++)
|
||||
o[(size_t)x * dstep] = (uint8_t)((FIT_ONE / 2 + w[0] * r0[x] + w[1] * r1[x] + w[2] * r2[x]) / FIT_ONE);
|
||||
continue;
|
||||
}
|
||||
memset(f->acc, 0, (size_t)pw * sizeof *f->acc);
|
||||
for (int t = 0; t < ay->taps; t++) {
|
||||
const uint8_t *row = r0 + (size_t)t * is;
|
||||
uint32_t k = w[t];
|
||||
for (int x = 0; x < pw; x++) f->acc[x] += k * row[x];
|
||||
}
|
||||
for (int x = 0; x < pw; x++) o[(size_t)x * dstep] = (uint8_t)((f->acc[x] + FIT_ONE / 2) / FIT_ONE);
|
||||
}
|
||||
}
|
||||
|
||||
// I420 planes -> packed NV12 with the picture shrunk inside black bars: w x h of Y, then w x h/2 of interleaved U, V.
|
||||
void movie_fit_apply(const MovieFit *f, const uint8_t *const plane[3], const int stride[3], uint8_t *dst) {
|
||||
int w = f->w, h = f->h, cw = w / 2, ch = h / 2;
|
||||
uint8_t *uv = dst + (size_t)w * h;
|
||||
// Black only where the picture does not go: whole rows above and below it, the strips left and right of it.
|
||||
int rx = f->x0 + f->pw, ry = f->y0 + f->ph;
|
||||
memset(dst, FIT_Y_BLACK, (size_t)f->y0 * w);
|
||||
memset(dst + (size_t)ry * w, FIT_Y_BLACK, (size_t)(h - ry) * w);
|
||||
memset(uv, FIT_UV_BLACK, (size_t)(f->y0 / 2) * w);
|
||||
memset(uv + (size_t)(ry / 2) * w, FIT_UV_BLACK, (size_t)(ch - ry / 2) * w);
|
||||
for (int y = f->y0; y < ry; y++) {
|
||||
memset(dst + (size_t)y * w, FIT_Y_BLACK, (size_t)f->x0);
|
||||
memset(dst + (size_t)y * w + rx, FIT_Y_BLACK, (size_t)(w - rx));
|
||||
}
|
||||
for (int y = f->y0 / 2; y < ry / 2; y++) {
|
||||
memset(uv + (size_t)y * w, FIT_UV_BLACK, (size_t)f->x0);
|
||||
memset(uv + (size_t)y * w + rx, FIT_UV_BLACK, (size_t)(w - rx));
|
||||
}
|
||||
fit_plane(f, plane[0], stride[0], w, h, dst, w, 1, f->x0, f->y0, f->lx.n ? &f->lx : NULL, f->ly.n ? &f->ly : NULL,
|
||||
f->pw, f->ph);
|
||||
for (int c = 0; c < 2; c++)
|
||||
fit_plane(f, plane[1 + c], stride[1 + c], cw, ch, uv + c, w, 2, f->x0 / 2, f->y0 / 2,
|
||||
f->cx.n ? &f->cx : NULL, f->cy.n ? &f->cy : NULL, f->pw / 2, f->ph / 2);
|
||||
}
|
||||
|
||||
static int free_out(const Movie *m) {
|
||||
for (int i = 0; i < OUT_BUFS; i++)
|
||||
if (!m->out_held[i]) return i;
|
||||
@@ -250,7 +418,9 @@ static void worker(void *arg) {
|
||||
slot = free_out(m);
|
||||
m->out_held[slot] = 1; // reserved: in no queue, so the game cannot release it
|
||||
}
|
||||
uint64_t gen = m->gen, dt = 0;
|
||||
uint64_t gen = m->gen, dt = 0, fit_ns = 0;
|
||||
// A plan made for another frame size (a start that found the worker busy) is not used: it would overrun dst.
|
||||
const MovieFit *fit = m->fit && m->fit->w == m->w && m->fit->h == m->h ? m->fit : NULL;
|
||||
int got = 0, w = m->w, h = m->h;
|
||||
uint8_t *dst = slot >= 0 ? m->out[slot] : NULL;
|
||||
vpx_codec_err_t e = VPX_CODEC_OK;
|
||||
@@ -265,7 +435,12 @@ static void worker(void *arg) {
|
||||
const vpx_image_t *img;
|
||||
while ((img = vpx_codec_get_frame(&m->vpx, &it)) != NULL) {
|
||||
if (img->fmt != VPX_IMG_FMT_I420 || (int)img->d_w != w || (int)img->d_h != h) bad = "layout";
|
||||
else if (!got) to_nv12(img, dst, w, h);
|
||||
else if (!got) {
|
||||
uint64_t t1 = kern_now_ns();
|
||||
if (fit) movie_fit_apply(fit, (const uint8_t *const *)img->planes, img->stride, dst);
|
||||
else to_nv12(img, dst, w, h);
|
||||
fit_ns = kern_now_ns() - t1;
|
||||
}
|
||||
got++;
|
||||
}
|
||||
}
|
||||
@@ -279,6 +454,7 @@ static void worker(void *arg) {
|
||||
m->owed |= 1u << MOVIE_EV_INPUT;
|
||||
if (p.size > 0) {
|
||||
m->st.decode_ns += dt;
|
||||
m->st.fit_ns += fit_ns;
|
||||
if (dt > m->st.max_ns) m->st.max_ns = dt;
|
||||
if (bad) rt_fatal("movie: VP9 frame layout differs from the configured %dx%d I420", w, h);
|
||||
if (e != VPX_CODEC_OK || !got) {
|
||||
@@ -302,8 +478,10 @@ static void worker(void *arg) {
|
||||
push_decoded(m, slot, 0, p.time, frame_bytes(m));
|
||||
m->st.frames++;
|
||||
if (m->st.frames % 600 == 0)
|
||||
rt_log("movie: %llu frames, decode %.2f ms per frame (max %.2f)", (unsigned long long)m->st.frames,
|
||||
m->st.decode_ns / 1e6 / (double)m->st.frames, m->st.max_ns / 1e6);
|
||||
rt_log("movie: %llu frames, decode %.2f ms per frame (max %.2f), of it %.2f ms %s",
|
||||
(unsigned long long)m->st.frames, m->st.decode_ns / 1e6 / (double)m->st.frames,
|
||||
m->st.max_ns / 1e6, m->st.fit_ns / 1e6 / (double)m->st.frames,
|
||||
m->fit ? "fitting the picture" : "converting");
|
||||
}
|
||||
}
|
||||
if (p.flags & MOVIE_FLAG_EOS && !m->eos_in) {
|
||||
@@ -355,6 +533,8 @@ static void release_codec(Movie *m) { // m->mu held, worker quiesced
|
||||
free(m->out[i]);
|
||||
m->out[i] = NULL;
|
||||
}
|
||||
movie_fit_free(m->fit);
|
||||
m->fit = NULL;
|
||||
m->configured = m->started = 0;
|
||||
reset_queues(m);
|
||||
}
|
||||
@@ -597,6 +777,18 @@ static void ov_start(Cpu *c) {
|
||||
plat_mutex_lock(&m->mu);
|
||||
int st = ST_NOT_CONFIGURED;
|
||||
if (m->configured) {
|
||||
if (!m->started && !m->busy) { // the worker is idle: it reads m->fit only when it takes an input
|
||||
movie_fit_free(m->fit);
|
||||
m->fit = NULL;
|
||||
unsigned out[2];
|
||||
res_wanted(out);
|
||||
if (strcmp(cfg_value("TOTK_MOVIE_ASPECT"), "stretch")) {
|
||||
m->fit = movie_fit_new(m->w, m->h, out[0], out[1]);
|
||||
if (m->fit)
|
||||
rt_log("movie: %dx%d picture kept in shape on the %ux%u output (%dx%d inside the frame)", m->w,
|
||||
m->h, out[0], out[1], m->fit->pw, m->fit->ph);
|
||||
}
|
||||
}
|
||||
m->started = 1;
|
||||
m->owed |= 1u << MOVIE_EV_INPUT;
|
||||
plat_cond_signal(&m->work);
|
||||
|
||||
@@ -270,7 +270,8 @@ int plat_intr_resume(PlatIntr *t, uint64_t to) {
|
||||
// ---- fibers: a stack switch of our own. glibc's swapcontext saves and restores the signal mask with a system call
|
||||
// on every switch, and the game switches fibers often. fiber_swap saves the callee-saved registers, the MXCSR and the
|
||||
// x87 control word on the current stack, stores the stack pointer in *save and loads the other one. A new fiber's
|
||||
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which calls fn(arg) (r12 and r13).
|
||||
// stack is laid out as fiber_swap leaves one, returning into fiber_entry, which realigns the stack and calls fn(arg)
|
||||
// (r12 and r13).
|
||||
struct PlatFiber {
|
||||
void *sp;
|
||||
void *stack; // mapped stack (NULL for a thread's own context)
|
||||
@@ -312,6 +313,11 @@ __asm__(".text\n"
|
||||
".type plat_fiber_entry, @function\n"
|
||||
".p2align 4\n"
|
||||
"plat_fiber_entry:\n"
|
||||
// fiber_swap's ret lands here with rsp = top - 8, as at the entry of a called function. The call below must
|
||||
// be made with rsp 16-byte aligned so that fn starts with rsp = 8 mod 16, as the System V ABI demands; without
|
||||
// the realignment every 16-byte local of fn is off by 8, which the VEX instructions of a v3 build tolerate
|
||||
// but the SSE ones of a v2 or k10 build (movdqa, pcmpeqb with a memory operand) fault on.
|
||||
" andq $-16, %rsp\n"
|
||||
" movq %r13, %rdi\n"
|
||||
" callq *%r12\n"
|
||||
" ud2\n"
|
||||
@@ -993,6 +999,12 @@ int plat_mem_read(uint64_t addr, void *out, size_t n) {
|
||||
return process_vm_readv(getpid(), &local, 1, &remote, 1, 0) == (ssize_t)n;
|
||||
}
|
||||
|
||||
// Not known before the SDL window exists; the Steam Deck has its own profile (cfg.c)
|
||||
int plat_screen_size(int *w, int *h) {
|
||||
(void)w, (void)h;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int plat_mem_readable(uint64_t addr, uint64_t n) {
|
||||
uint8_t b;
|
||||
uint64_t end = addr + (n ? n : 1);
|
||||
|
||||
@@ -240,6 +240,7 @@ typedef struct {
|
||||
} PlatCrash;
|
||||
void plat_crash_hook(void (*fn)(const PlatCrash *c));
|
||||
int plat_mem_read(uint64_t addr, void *out, size_t n); // copies n bytes, 0 instead of faulting on unmapped memory
|
||||
int plat_screen_size(int *w, int *h); // the primary display's current mode in pixels; 0 unknown
|
||||
int plat_mem_readable(uint64_t addr, uint64_t n); // every page of [addr, addr + n) can be read; no fault
|
||||
void *plat_aligned_alloc(size_t size, size_t align); // free with plat_aligned_free; NULL on failure
|
||||
void plat_aligned_free(void *p);
|
||||
|
||||
@@ -775,6 +775,14 @@ int plat_mem_read(uint64_t addr, void *out, size_t n) {
|
||||
return ReadProcessMemory(GetCurrentProcess(), (const void *)(uintptr_t)addr, out, n, &got) && got == n;
|
||||
}
|
||||
|
||||
// The display mode's own size: physical pixels whatever the process's DPI awareness and the display scale
|
||||
int plat_screen_size(int *w, int *h) {
|
||||
DEVMODEW dm = {.dmSize = sizeof dm};
|
||||
if (!EnumDisplaySettingsW(NULL, ENUM_CURRENT_SETTINGS, &dm) || !dm.dmPelsWidth || !dm.dmPelsHeight) return 0;
|
||||
*w = (int)dm.dmPelsWidth, *h = (int)dm.dmPelsHeight;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int plat_mem_readable(uint64_t addr, uint64_t n) {
|
||||
for (uint64_t a = addr, end = addr + n; a < end || a == addr;) {
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
|
||||
@@ -234,3 +234,102 @@ uint32_t restbl_grow(const char *key, uint32_t entry, uint64_t old, uint64_t new
|
||||
uint64_t e = entry + ((new_size - old) * f + 999) / 1000;
|
||||
return clamp32((e + 31) & ~(uint64_t)31);
|
||||
}
|
||||
|
||||
// ---- packs
|
||||
// SARC: "SARC", u16 header size, u16 byte order mark 0xfeff, u32 archive size, u32 data offset, u16 version, u16 pad;
|
||||
// SFAT: "SFAT", u16 header size, u16 node count, u32 hash key, then per node {u32 name hash, u32 attributes (bit 24: it
|
||||
// has a name, bits 0-23: the name's offset in SFNT / 4), u32 data start, u32 data end} with the data offsets relative
|
||||
// to the archive's data offset; SFNT: "SFNT", u16 header size, u16 pad, then the NUL-terminated names.
|
||||
static uint32_t rd16(const uint8_t *p) { return p[0] | p[1] << 8; }
|
||||
|
||||
int restbl_sarc_each(const uint8_t *p, size_t n, RestblMember *each, void *ctx) {
|
||||
if (n < 0x14 || memcmp(p, "SARC", 4) || rd16(p + 6) != 0xfeff) return 0;
|
||||
size_t head = rd16(p + 4), data = rd32(p + 12);
|
||||
if (head + 12 > n || memcmp(p + head, "SFAT", 4) || data > n) return 0;
|
||||
size_t count = rd16(p + head + 6), nodes = head + rd16(p + head + 4), names = nodes + count * 16;
|
||||
if (names + 8 > n || memcmp(p + names, "SFNT", 4) || rd16(p + names + 4) < 8) return 0;
|
||||
names += rd16(p + names + 4);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const uint8_t *e = p + nodes + i * 16;
|
||||
uint32_t attr = rd32(e + 4), lo = rd32(e + 8), hi = rd32(e + 12);
|
||||
if (!(attr >> 24)) continue; // without a name: nothing the game looks up by path
|
||||
size_t s = names + (size_t)(attr & 0xffffff) * 4;
|
||||
if (s >= n || !memchr(p + s, 0, n - s) || lo > hi || hi > n - data) return 0;
|
||||
each((const char *)p + s, p + data + lo, hi - lo, ctx);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The decompressed size a zstd frame p[0..n) records (Frame_Content_Size), or 4 times n when it records none, as
|
||||
// romfs.c content_size takes a loose file's.
|
||||
static uint64_t frame_content_size(const uint8_t *p, size_t n) {
|
||||
static const uint8_t did_bytes[4] = {0, 1, 2, 4};
|
||||
if (n < 6 || rd32(p) != 0xFD2FB528u) return n;
|
||||
uint8_t fhd = p[4];
|
||||
int single = fhd >> 5 & 1, fcs = fhd >> 6;
|
||||
size_t at = 5 + !single + did_bytes[fhd & 3], len = fcs ? 1u << fcs : single;
|
||||
if (!len || at + len > n) return (uint64_t)n * 4;
|
||||
uint64_t v = 0;
|
||||
for (size_t i = 0; i < len; i++) v |= (uint64_t)p[at + i] << 8 * i;
|
||||
return len == 2 ? v + 256 : v;
|
||||
}
|
||||
|
||||
typedef struct { const char *name; uint64_t size; } Member;
|
||||
typedef struct { Member *m; size_t n, cap; int full; } Members;
|
||||
|
||||
static int ends_with(const char *s, const char *tail) {
|
||||
size_t n = strlen(s), k = strlen(tail);
|
||||
return n >= k && !strcmp(s + n - k, tail);
|
||||
}
|
||||
|
||||
// A member's size as the game unpacks it: a .zs member's content size, else its bytes (a .mc member's compressed ones).
|
||||
static void member_add(const char *name, const uint8_t *data, size_t size, void *ctx) {
|
||||
Members *ms = ctx;
|
||||
if (ms->n == ms->cap) {
|
||||
size_t cap = ms->cap ? ms->cap * 2 : 64;
|
||||
Member *grown = realloc(ms->m, cap * sizeof *grown);
|
||||
if (!grown) {
|
||||
ms->full = 1;
|
||||
return;
|
||||
}
|
||||
ms->m = grown, ms->cap = cap;
|
||||
}
|
||||
ms->m[ms->n++] = (Member){name, ends_with(name, ".zs") ? frame_content_size(data, size) : size};
|
||||
}
|
||||
|
||||
static int by_name(const void *a, const void *b) {
|
||||
return strcmp(((const Member *)a)->name, ((const Member *)b)->name);
|
||||
}
|
||||
|
||||
int restbl_pack_raise(Restbl *t, const Restbl *base, const uint8_t *mod, size_t mod_n, const uint8_t *orig,
|
||||
size_t orig_n, RestblNote *note, void *ctx) {
|
||||
Members m = {0}, o = {0};
|
||||
int raised = -1;
|
||||
if (!restbl_sarc_each(mod, mod_n, member_add, &m) || m.full ||
|
||||
(orig && (!restbl_sarc_each(orig, orig_n, member_add, &o) || o.full)))
|
||||
goto done;
|
||||
if (o.n) qsort(o.m, o.n, sizeof *o.m, by_name);
|
||||
raised = 0;
|
||||
for (size_t i = 0; i < m.n; i++) {
|
||||
char key[1024];
|
||||
uint32_t entry, before = 0, want;
|
||||
restbl_key(m.m[i].name, key, sizeof key);
|
||||
int mc = ends_with(m.m[i].name, ".mc");
|
||||
const Member *was = o.n ? bsearch(&m.m[i], o.m, o.n, sizeof *o.m, by_name) : NULL;
|
||||
if (was) {
|
||||
if (!restbl_get(base, key, &entry)) continue; // the game sizes it without the table (streamed)
|
||||
want = restbl_grow(key, entry, was->size, m.m[i].size, mc);
|
||||
} else { // new to this pack: no copy of the game's to grow from
|
||||
want = restbl_estimate(key, mc ? m.m[i].size * 8 : m.m[i].size);
|
||||
}
|
||||
restbl_get(t, key, &before);
|
||||
if (want && restbl_raise(t, key, want)) {
|
||||
raised++;
|
||||
if (note) note(key, before, want, ctx);
|
||||
}
|
||||
}
|
||||
done:
|
||||
free(m.m);
|
||||
free(o.m);
|
||||
return raised;
|
||||
}
|
||||
@@ -31,3 +31,20 @@ uint32_t restbl_estimate(const char *key, uint64_t size);
|
||||
// times the type's factor, never lowered. scaled: the sizes are compressed ones (a mesh codec .mc file), whose entry
|
||||
// grows in proportion.
|
||||
uint32_t restbl_grow(const char *key, uint32_t entry, uint64_t old, uint64_t new_size, int scaled);
|
||||
|
||||
// The members of the SARC archive p[0..n): each(name, data, size, ctx) for every named member, in the archive's order.
|
||||
// 0 when p is not a SARC archive or a member lies outside it.
|
||||
typedef void RestblMember(const char *name, const uint8_t *data, size_t size, void *ctx);
|
||||
int restbl_sarc_each(const uint8_t *p, size_t n, RestblMember *each, void *ctx);
|
||||
|
||||
// A pack (Pack/**/*.pack.zs, decompressed: a SARC archive) whose members the game loads as resources of their own, each
|
||||
// sized by its own entry. The entries of a mod's pack `mod` raised in t against the game's table `base` and the game's
|
||||
// copy of the pack `orig` (NULL for a pack the game lacks): a member that grew by restbl_grow, a member the game's copy
|
||||
// lacks by restbl_estimate, the others left as they are. note(key, entry before, entry after, ctx) for each raised
|
||||
// entry. The number raised; -1 when either pack is not a SARC archive.
|
||||
typedef void RestblNote(const char *key, uint32_t was, uint32_t now, void *ctx);
|
||||
int restbl_pack_raise(Restbl *t, const Restbl *base, const uint8_t *mod, size_t mod_n, const uint8_t *orig,
|
||||
size_t orig_n, RestblNote *note, void *ctx);
|
||||
|
||||
// restbl_probe.c, part of --probe-mods: the number of failed checks.
|
||||
int restbl_probe(void);
|
||||
@@ -0,0 +1,133 @@
|
||||
// The entries of the members of the mods' packs (restbl.c restbl_pack_raise), on archives and tables of its own; part
|
||||
// of --probe-mods (mods_probe.c). Needs no game files.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "restbl.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL restbl: %s\n", what); } } while (0)
|
||||
|
||||
// A member of a probe archive: its name and size; a .zs member is a zstd frame header recording `content` bytes:
|
||||
// fcs 4, a frame with a window descriptor and a 4-byte field; 2, a single-segment frame with a 2-byte field holding
|
||||
// content - 256.
|
||||
typedef struct { const char *name; size_t size, content; int fcs; } Member;
|
||||
|
||||
static void wr16(uint8_t *p, unsigned v) { p[0] = (uint8_t)v, p[1] = (uint8_t)(v >> 8); }
|
||||
static void wr32(uint8_t *p, uint32_t v) { wr16(p, v & 0xffff), wr16(p + 2, v >> 16); }
|
||||
|
||||
// A SARC archive of the members m[0..n); unnamed: the index of a member without a name, -1 for none. malloc'd.
|
||||
static uint8_t *sarc(const Member *m, int n, int unnamed, size_t *out_n) {
|
||||
size_t names = 0, data = 0;
|
||||
for (int i = 0; i < n; i++) names += (strlen(m[i].name) + 4) & ~(size_t)3, data += (m[i].size + 7) & ~(size_t)7;
|
||||
size_t sfat = 0x14, sfnt = sfat + 12 + 16 * (size_t)n, start = (sfnt + 8 + names + 15) & ~(size_t)15;
|
||||
size_t total = start + data;
|
||||
uint8_t *p = calloc(1, total);
|
||||
if (!p) return NULL;
|
||||
memcpy(p, "SARC", 4), wr16(p + 4, 0x14), wr16(p + 6, 0xfeff);
|
||||
wr32(p + 8, (uint32_t)total), wr32(p + 12, (uint32_t)start);
|
||||
memcpy(p + sfat, "SFAT", 4), wr16(p + sfat + 4, 12), wr16(p + sfat + 6, (unsigned)n), wr32(p + sfat + 8, 0x65);
|
||||
memcpy(p + sfnt, "SFNT", 4), wr16(p + sfnt + 4, 8);
|
||||
size_t name_at = 0, data_at = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint8_t *node = p + sfat + 12 + 16 * (size_t)i, *d = p + start + data_at;
|
||||
wr32(node + 4, i == unnamed ? 0 : 0x01000000u | (uint32_t)(name_at / 4));
|
||||
wr32(node + 8, (uint32_t)data_at), wr32(node + 12, (uint32_t)(data_at + m[i].size));
|
||||
memcpy(p + sfnt + 8 + name_at, m[i].name, strlen(m[i].name));
|
||||
memset(d, 0x5a, m[i].size);
|
||||
if (m[i].fcs == 4) wr32(d, 0xFD2FB528u), d[4] = 0x80, d[5] = 0x58, wr32(d + 6, (uint32_t)m[i].content);
|
||||
if (m[i].fcs == 2) wr32(d, 0xFD2FB528u), d[4] = 0x60, wr16(d + 5, (unsigned)(m[i].content - 256));
|
||||
name_at += (strlen(m[i].name) + 4) & ~(size_t)3;
|
||||
data_at += (m[i].size + 7) & ~(size_t)7;
|
||||
}
|
||||
*out_n = total;
|
||||
return p;
|
||||
}
|
||||
|
||||
// An empty table with the entries keys[i] = sizes[i].
|
||||
static Restbl *table(const char *const *keys, const uint32_t *sizes, int n) {
|
||||
uint8_t head[22] = "RESTBL";
|
||||
wr32(head + 6, 1), wr32(head + 10, 160), wr32(head + 14, 0), wr32(head + 18, 0);
|
||||
Restbl *t = restbl_parse(head, sizeof head);
|
||||
for (int i = 0; t && i < n; i++) restbl_raise(t, keys[i], sizes[i]);
|
||||
return t;
|
||||
}
|
||||
|
||||
static uint32_t get(const Restbl *t, const char *key) {
|
||||
uint32_t v = 0;
|
||||
return restbl_get(t, key, &v) ? v : 0;
|
||||
}
|
||||
|
||||
static int visited;
|
||||
static void count(const char *name, const uint8_t *data, size_t size, void *ctx) {
|
||||
(void)name, (void)data, (void)size, (void)ctx;
|
||||
visited++;
|
||||
}
|
||||
|
||||
static int notes;
|
||||
static void note(const char *key, uint32_t was, uint32_t now, void *ctx) {
|
||||
(void)key, (void)ctx;
|
||||
notes += now > was;
|
||||
}
|
||||
|
||||
int restbl_probe(void) {
|
||||
fails = 0;
|
||||
static const char *const keys[] = {"AI/Grow.root.ainb", "AI/Same.root.ainb", "AI/Shrink.root.ainb",
|
||||
"Component/Shared.bgyml", "Model/Mesh.bfres", "AI/Zs.ainb", "AI/Zs2.ainb"};
|
||||
static const uint32_t sizes[] = {13512, 5000, 6000, 9000, 30000, 4400, 3500};
|
||||
// The game's copy: an .ainb member a mod grows (by 288 bytes, past its entry's slack), one it keeps, one it
|
||||
// shrinks, one the table does not size, a mesh codec member and two compressed ones.
|
||||
static const Member orig[] = {{"AI/Grow.root.ainb", 13088, 0, 0}, {"AI/Same.root.ainb", 4600, 0, 0},
|
||||
{"AI/Shrink.root.ainb", 5600, 0, 0}, {"AI/NotSized.ainb", 100, 0, 0},
|
||||
{"Model/Mesh.bfres.mc", 1000, 0, 0}, {"AI/Zs.ainb.zs", 64, 4000, 4},
|
||||
{"AI/Zs2.ainb.zs", 64, 3900, 2}};
|
||||
// The mod's copy: those, plus members new to the pack (one the game sizes elsewhere, one it lacks, a streamed one).
|
||||
static const Member mod[] = {{"AI/Grow.root.ainb", 13376, 0, 0}, {"AI/Same.root.ainb", 4600, 0, 0},
|
||||
{"AI/Shrink.root.ainb", 5000, 0, 0}, {"AI/NotSized.ainb", 300, 0, 0},
|
||||
{"Component/New/X.bgyml", 1000, 0, 0}, {"Component/Shared.bgyml", 2000, 0, 0},
|
||||
{"Sound/Stream.bwav", 500, 0, 0}, {"Model/Mesh.bfres.mc", 1500, 0, 0},
|
||||
{"AI/Zs.ainb.zs", 64, 4500, 4}, {"AI/Zs2.ainb.zs", 64, 4200, 2}};
|
||||
size_t on = 0, mn = 0;
|
||||
uint8_t *op = sarc(orig, 7, -1, &on), *mp = sarc(mod, 10, 4, &mn); // the mod's New/X has no name: left alone
|
||||
Restbl *base = table(keys, sizes, 7), *t = table(keys, sizes, 7);
|
||||
CHECK(op && mp && base && t, "probe archives and tables");
|
||||
if (op && mp && base && t) {
|
||||
visited = 0;
|
||||
CHECK(restbl_sarc_each(mp, mn, count, NULL) && visited == 9, "SARC members: every named one");
|
||||
int r = restbl_pack_raise(t, base, mp, mn, op, on, note, NULL);
|
||||
CHECK(r == 5 && notes == 5, "five entries raised, each noted");
|
||||
CHECK(get(t, "AI/Grow.root.ainb") == 13824, "a member that grew: its entry plus the growth, 32-byte steps");
|
||||
CHECK(get(t, "AI/Same.root.ainb") == 5000 && get(t, "AI/Shrink.root.ainb") == 6000,
|
||||
"members that kept or lost bytes keep their entries");
|
||||
CHECK(!get(t, "AI/NotSized.ainb"), "a member the game sizes without the table gets no entry");
|
||||
CHECK(!get(t, "Component/New/X.bgyml"), "a member without a name is not looked up");
|
||||
CHECK(get(t, "Component/Shared.bgyml") == 16384, "a member new to the pack: the estimate of its type");
|
||||
CHECK(!get(t, "Sound/Stream.bwav"), "a streamed member new to the pack gets no entry");
|
||||
CHECK(get(t, "Model/Mesh.bfres") == 45000, "a mesh codec member's entry grows in proportion");
|
||||
CHECK(get(t, "AI/Zs.ainb") == 4928 && get(t, "AI/Zs2.ainb") == 3808,
|
||||
"a compressed member: the growth of the size its frame records (4- and 2-byte fields)");
|
||||
notes = 0;
|
||||
CHECK(restbl_pack_raise(t, base, mp, mn, op, on, note, NULL) == 0 && !notes, "a second pass raises nothing");
|
||||
Restbl *shipped = table(keys, (const uint32_t[]){20000, 5000, 6000, 9000, 30000, 4400, 3500}, 7);
|
||||
CHECK(shipped && restbl_pack_raise(shipped, base, mp, mn, op, on, NULL, NULL) == 4 &&
|
||||
get(shipped, "AI/Grow.root.ainb") == 20000,
|
||||
"an entry a mod's own table raised further stays");
|
||||
restbl_free(shipped);
|
||||
Restbl *added = table(keys, sizes, 7);
|
||||
CHECK(added && restbl_pack_raise(added, base, mp, mn, NULL, 0, NULL, NULL) == 7 &&
|
||||
get(added, "AI/Grow.root.ainb") == 24576 && get(added, "AI/NotSized.ainb") == 12288 &&
|
||||
get(added, "AI/Zs2.ainb") == 16384 && get(added, "Model/Mesh.bfres") == 30000,
|
||||
"a pack the game lacks: every member gets the estimate of its type, an entry above it stays");
|
||||
restbl_free(added);
|
||||
CHECK(restbl_pack_raise(t, base, (const uint8_t *)"SARC", 4, NULL, 0, NULL, NULL) == -1 &&
|
||||
restbl_pack_raise(t, base, mp, mn, mp, 0x20, NULL, NULL) == -1,
|
||||
"an archive that is not one, or whose game copy is cut short, raises nothing");
|
||||
}
|
||||
free(op);
|
||||
free(mp);
|
||||
restbl_free(base);
|
||||
restbl_free(t);
|
||||
printf("restbl: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails;
|
||||
}
|
||||
+120
-5
@@ -24,8 +24,8 @@
|
||||
// matches a RomFS path only without case is served under the RomFS spelling.
|
||||
//
|
||||
// The resource size table: with a player mod enabled, the game's table is rebuilt at start with the tables the mods
|
||||
// ship merged in and the entries of the mods' files raised to what they need (restbl_overlay), and served from
|
||||
// <profile>/mods/ in place of the game's or a mod's table.
|
||||
// ship merged in and the entries of the mods' files raised to what they need (restbl_overlay), the members of their
|
||||
// packs too (restbl_packs), and served from <profile>/mods/ in place of the game's or a mod's table.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -621,10 +621,122 @@ static Restbl *restbl_unpack(const void *z, size_t n) {
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---- the members of the mods' packs (restbl_pack_raise). The game loads each member of a pack (Pack/**/*.pack.zs, a
|
||||
// SARC archive compressed with a dictionary of Pack/ZsDic.pack.zs) as a resource of its own, in a heap sized by the
|
||||
// member's own entry, so a mod that changes a member and ships no table for it (or an older one) leaves the game's
|
||||
// entry. A member that outgrew its entry's slack loads, then its heap runs out: TotK 1.4.3 writes through a null
|
||||
// pointer at main+0x21794c when the 0x68-byte object an .ainb member builds after its file no longer fits.
|
||||
#define PACK_MAX (256u << 20) // the largest pack taken, decompressed (AI.Global.Product.140.pack: 24 MiB)
|
||||
typedef struct { ZSTD_DDict *dict[4]; unsigned id[4]; int n; ZSTD_DCtx *dctx; } Packs;
|
||||
|
||||
// The bytes of zstd frame z[0..n), with the dictionary of ps its header names; malloc'd. NULL when it is not such a
|
||||
// frame with a recorded size of at most PACK_MAX.
|
||||
static uint8_t *pack_unpack(Packs *ps, const void *z, size_t n, size_t *out_n) {
|
||||
unsigned long long raw = ZSTD_getFrameContentSize(z, n);
|
||||
unsigned id = ZSTD_getDictID_fromFrame(z, n);
|
||||
ZSTD_DDict *dd = NULL;
|
||||
for (int i = 0; i < ps->n; i++)
|
||||
if (ps->id[i] == id) dd = ps->dict[i];
|
||||
if (raw == ZSTD_CONTENTSIZE_UNKNOWN || raw == ZSTD_CONTENTSIZE_ERROR || raw > PACK_MAX || (id && !dd) || !ps->dctx)
|
||||
return NULL;
|
||||
uint8_t *d = malloc(raw ? (size_t)raw : 1);
|
||||
size_t got = !d ? 0 : dd ? ZSTD_decompress_usingDDict(ps->dctx, d, (size_t)raw, z, n, dd)
|
||||
: ZSTD_decompressDCtx(ps->dctx, d, (size_t)raw, z, n);
|
||||
if (!d || ZSTD_isError(got) || got != raw) {
|
||||
free(d);
|
||||
return NULL;
|
||||
}
|
||||
*out_n = (size_t)raw;
|
||||
return d;
|
||||
}
|
||||
|
||||
static void pack_dict_add(const char *name, const uint8_t *data, size_t size, void *ctx) {
|
||||
Packs *ps = ctx;
|
||||
ZSTD_DDict *dd = ps->n < 4 && ends_with(name, ".zsdic") ? ZSTD_createDDict(data, size) : NULL; // a copy
|
||||
if (!dd) return;
|
||||
ps->dict[ps->n] = dd;
|
||||
ps->id[ps->n++] = ZSTD_getDictID_fromDDict(dd);
|
||||
}
|
||||
|
||||
// The dictionaries of Pack/ZsDic.pack.zs as served (zs.zsdic, pack.zsdic, bcett.byml.zsdic): a SARC compressed
|
||||
// without one. 0 when it holds none.
|
||||
static int packs_open(Packs *ps) {
|
||||
Overlay *base;
|
||||
size_t n = 0, rn = 0;
|
||||
ps->dctx = ZSTD_createDCtx();
|
||||
void *z = read_served("Pack/ZsDic.pack.zs", &base, &n, "the members of the mods' packs keep their entries");
|
||||
uint8_t *raw = z ? pack_unpack(ps, z, n, &rn) : NULL;
|
||||
int ok = raw && restbl_sarc_each(raw, rn, pack_dict_add, ps) && ps->n;
|
||||
free(z);
|
||||
free(raw);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void packs_close(Packs *ps) {
|
||||
for (int i = 0; i < ps->n; i++) ZSTD_freeDDict(ps->dict[i]);
|
||||
ZSTD_freeDCtx(ps->dctx);
|
||||
}
|
||||
|
||||
// Overlay o (a pack) and the game's copy of it, decompressed; NULL when o cannot be read or unpacked. *orig stays NULL
|
||||
// for a pack the game lacks or whose copy cannot be read.
|
||||
static uint8_t *pack_pair(Packs *ps, const Overlay *o, size_t *n, uint8_t **orig, size_t *orig_n) {
|
||||
uint64_t zn = 0;
|
||||
void *z = plat_file_read(o->path, &zn);
|
||||
uint8_t *mod = z ? pack_unpack(ps, z, (size_t)zn, n) : NULL;
|
||||
free(z);
|
||||
*orig = NULL;
|
||||
if (!mod || o->index == ADDED) return mod;
|
||||
Entry e = entry(o->index);
|
||||
void *g = e.size < READ_CHUNK ? malloc(e.size ? (size_t)e.size : 1) : NULL;
|
||||
if (g && read_at(image, g, (uint32_t)e.size, e.offset) == (int64_t)e.size)
|
||||
*orig = pack_unpack(ps, g, (size_t)e.size, orig_n);
|
||||
free(g);
|
||||
return mod;
|
||||
}
|
||||
|
||||
typedef struct { const char *pack, *mod; int lines; } PackNote;
|
||||
|
||||
static void pack_note(const char *key, uint32_t was, uint32_t now, void *ctx) {
|
||||
PackNote *pn = ctx;
|
||||
if (pn->lines++ < 16)
|
||||
rt_log("RomFS: resource size table: %s in %s (%s): entry %u -> %u", key, pn->pack, pn->mod, was, now);
|
||||
}
|
||||
|
||||
// The entries of the members of every pack a mod serves, raised in out against the game's table base; the number
|
||||
// raised, *packs the number of packs with one.
|
||||
static int restbl_packs(Restbl *out, const Restbl *base, int *packs, int *lines) {
|
||||
Packs ps = {0};
|
||||
int raised = 0, opened = 0, bad = 0;
|
||||
*packs = 0;
|
||||
for (size_t k = 0; k < noverlay; k++) {
|
||||
const Overlay *o = &overlays[k];
|
||||
if ((!o->mod && o->prio != 1) || !ends_with(o->name, ".pack.zs")) continue;
|
||||
if (!opened++ && !packs_open(&ps)) {
|
||||
rt_log("RomFS: no zstd dictionary in Pack/ZsDic.pack.zs; the members of mods' packs keep their entries");
|
||||
break;
|
||||
}
|
||||
// A pack whose game copy cannot be read counts as new: each member gets an estimate, which only raises entries.
|
||||
size_t n = 0, orig_n = 0;
|
||||
uint8_t *orig, *mod = pack_pair(&ps, o, &n, &orig, &orig_n);
|
||||
PackNote pn = {o->name, owner_name(o), *lines};
|
||||
int r = mod ? restbl_pack_raise(out, base, mod, n, orig, orig_n, pack_note, &pn) : -1;
|
||||
if (r < 0 && bad++ < 4) rt_log("RomFS: %s (%s) is not a pack this build reads; its members keep their entries",
|
||||
o->name, owner_name(o));
|
||||
*lines = pn.lines;
|
||||
raised += r > 0 ? r : 0;
|
||||
*packs += r > 0;
|
||||
free(mod);
|
||||
free(orig);
|
||||
}
|
||||
packs_close(&ps);
|
||||
return raised;
|
||||
}
|
||||
|
||||
// With a player mod (or the TOTK_ROMFS_MODS manifest) serving files: the game's table with every shipped table of this
|
||||
// version merged in (an entry larger than the game's wins), the entries a mod's table for another version gives for
|
||||
// that mod's files, and each served mod file's entry raised to what its size needs (restbl_grow for a file the game
|
||||
// has, restbl_estimate for an added one). Never lowers the game's entries.
|
||||
// has, restbl_estimate for an added one), the members of the packs mods serve too (restbl_packs). Never lowers the
|
||||
// game's entries.
|
||||
static void restbl_overlay(void) {
|
||||
int player = 0;
|
||||
for (size_t k = 0; k < noverlay; k++) player |= overlays[k].mod || overlays[k].prio == 1;
|
||||
@@ -691,8 +803,10 @@ static void restbl_overlay(void) {
|
||||
}
|
||||
raised += want && restbl_raise(out, key, want);
|
||||
}
|
||||
int packs = 0, lines = 0, members = restbl_packs(out, base, &packs, &lines);
|
||||
if (lines > 16) rt_log("RomFS: resource size table: %d more entries of pack members raised", lines - 16);
|
||||
restbl_free(base);
|
||||
if (!merged && !raised && !served) {
|
||||
if (!merged && !raised && !members && !served) {
|
||||
restbl_free(out);
|
||||
rt_log("RomFS: resource size table: the mods' files fit the game's entries (%.0f ms)", (now_s() - t0) * 1000);
|
||||
return;
|
||||
@@ -711,7 +825,8 @@ static void restbl_overlay(void) {
|
||||
}
|
||||
if (serve_generated(restbl_rel, served, z, zn, "mods", "merged resource sizes", keeps))
|
||||
rt_log("RomFS: resource size table: %d tables merged (%d entries), %d entries raised for mod files (%d "
|
||||
"added files estimated), %.0f ms", tables, merged, raised, estimated, (now_s() - t0) * 1000);
|
||||
"added files estimated), %d for members of %d mod packs, %.0f ms", tables, merged, raised, estimated,
|
||||
members, packs, (now_s() - t0) * 1000);
|
||||
}
|
||||
|
||||
// The HeapDef table a bigger game resolution needs (resolution.c): its texture heap and that heap's parent grow. The
|
||||
|
||||
@@ -668,6 +668,8 @@ static void tab_screen(float scale) {
|
||||
opt_slider("TOTK_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%");
|
||||
static const UiTextId aspect[] = {UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH};
|
||||
opt_choice("TOTK_ASPECT", UI_MENU_ASPECT, aspect, 2, UI_MENU_ASPECT_DESC);
|
||||
static const UiTextId movie_aspect[] = {UI_MENU_MOVIE_ASPECT_KEEP, UI_MENU_MOVIE_ASPECT_STRETCH};
|
||||
opt_choice("TOTK_MOVIE_ASPECT", UI_MENU_MOVIE_ASPECT, movie_aspect, 2, UI_MENU_MOVIE_ASPECT_DESC);
|
||||
opt_slider("TOTK_FOV", UI_MENU_FOV, 70, 140, UI_MENU_FOV_DESC, 5, "%.0f %%");
|
||||
static const char *const aniso[] = {"game", "2", "4", "8", "16"};
|
||||
const char *aniso_names[] = {menu_text(UI_MENU_ANISO_GAME), "2x", "4x", "8x", "16x"};
|
||||
@@ -699,6 +701,7 @@ static void tab_screen(float scale) {
|
||||
static void tab_game(float scale) {
|
||||
opt_begin("##game", scale);
|
||||
row_fps_limit();
|
||||
opt_toggle("TOTK_FRAME_QUEUE", UI_MENU_FRAME_QUEUE, UI_MENU_FRAME_QUEUE_DESC);
|
||||
row_game_lang();
|
||||
static const UiTextId mode[] = {UI_MENU_MODE_DOCKED, UI_MENU_MODE_HANDHELD};
|
||||
opt_choice("TOTK_OPERATION_MODE", UI_MENU_OPERATION_MODE, mode, 2, UI_MENU_OPERATION_MODE_DESC);
|
||||
|
||||
+32
-4
@@ -79,6 +79,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_ASPECT_DESC] = "When the window's shape differs from the game resolution's (a 16:9 picture on a 21:9 "
|
||||
"screen): keep the picture's shape with black bars, or stretch it over the window.",
|
||||
[UI_MENU_ASPECT_FIT] = "Black bars", [UI_MENU_ASPECT_STRETCH] = "Stretch",
|
||||
[UI_MENU_MOVIE_ASPECT] = "Movie shape",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "When the game resolution is not 16:9, keep the cutscene movies' own shape with black "
|
||||
"bars, or stretch them over the whole picture. Applies from the next movie.",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "Black bars", [UI_MENU_MOVIE_ASPECT_STRETCH] = "Stretch",
|
||||
[UI_MENU_FOV] = "Field of view",
|
||||
[UI_MENU_FOV_DESC] = "How much of the world the camera shows, in percent of the game's own. Game version 1.4.3 only.",
|
||||
[UI_MENU_SHADOW_RES] = "Shadow map size",
|
||||
@@ -168,6 +172,9 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
// the monitor's refresh rate; printf format with the rate in Hz when it is known
|
||||
[UI_MENU_FPS_DISPLAY] = "Monitor rate", [UI_MENU_FPS_DISPLAY_HZ] = "Monitor rate (%.0f Hz)",
|
||||
[UI_MENU_FRAME_QUEUE] = "Frame queue",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "The game starts a frame while the previous one waits for its turn on screen: far fewer "
|
||||
"dropped frames at the limit, up to one frame more input delay. Applies at once.",
|
||||
[UI_MENU_REALTIME] = "Game speed follows the clock",
|
||||
[UI_MENU_REALTIME_DESC] = "Steps the game by real time, so a slow frame does not slow the game down.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Cloth on the clock",
|
||||
@@ -263,7 +270,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "How far a gamepad stick moves before the game sees it. Raise it if the character or the camera moves on its own: let go of the stick, read \"stick now\" and set the dead zone a few percent above it.",
|
||||
[UI_MENU_STICK_DEADZONE_NOW] = "stick now: %.0f %%",
|
||||
[UI_MENU_MOUSE_MODE] = "Mouse camera",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direct: the camera turns by how far the mouse moves, as in a PC game, with no stick dead zone or curve (game 1.4.3; other versions use stick emulation). Stick emulation: the mouse acts as the right stick.",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direct: the camera turns by how far the mouse moves, as in a PC game, with no stick dead zone or curve (games 1.4.3 and 1.2.1; 1.1.0 uses stick emulation). Stick emulation: the mouse acts as the right stick.",
|
||||
[UI_MENU_MOUSE_STICK] = "Stick emulation",
|
||||
[UI_MENU_MOUSE_DIRECT] = "Direct",
|
||||
[UI_MENU_MENU_CURSOR] = "Mouse in the game's menus",
|
||||
@@ -542,6 +549,10 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_ASPECT_DESC] = "Когда форма окна отличается от разрешения игры (картинка 16:9 на экране 21:9): сохранить "
|
||||
"форму картинки с чёрными полосами или растянуть её на всё окно.",
|
||||
[UI_MENU_ASPECT_FIT] = "Чёрные полосы", [UI_MENU_ASPECT_STRETCH] = "Растянуть",
|
||||
[UI_MENU_MOVIE_ASPECT] = "Форма видеороликов",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "Когда разрешение игры не 16:9: сохранить форму роликов с чёрными полосами или "
|
||||
"растянуть их на всю картинку. Действует со следующего ролика.",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "Чёрные полосы", [UI_MENU_MOVIE_ASPECT_STRETCH] = "Растянуть",
|
||||
[UI_MENU_FOV] = "Поле зрения",
|
||||
[UI_MENU_FOV_DESC] = "Сколько мира показывает камера, в процентах от игрового. Только для версии игры 1.4.3.",
|
||||
[UI_MENU_SHADOW_RES] = "Размер карты теней",
|
||||
@@ -631,6 +642,9 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_30] = "30 FPS", [UI_MENU_FPS_40] = "40 FPS", [UI_MENU_FPS_45] = "45 FPS",
|
||||
[UI_MENU_FPS_60] = "60 FPS", [UI_MENU_FPS_90] = "90 FPS", [UI_MENU_FPS_120] = "120 FPS", [UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_DISPLAY] = "Как у монитора", [UI_MENU_FPS_DISPLAY_HZ] = "Как у монитора (%.0f Гц)",
|
||||
[UI_MENU_FRAME_QUEUE] = "Очередь кадров",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "Игра начинает кадр, пока предыдущий ждёт показа: намного меньше пропущенных кадров на "
|
||||
"лимите, задержка ввода больше не более чем на кадр. Применяется сразу.",
|
||||
[UI_MENU_REALTIME] = "Скорость игры по часам",
|
||||
[UI_MENU_REALTIME_DESC] = "Игра идёт по реальному времени, медленный кадр её не замедляет.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Ткань по часам",
|
||||
@@ -724,7 +738,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_STICK_DEADZONE_DESC] = "Насколько нужно отклонить стик геймпада, чтобы игра это заметила. Увеличьте, если персонаж или камера двигаются сами: отпустите стик, посмотрите «стик сейчас» и поставьте мёртвую зону на несколько процентов выше.",
|
||||
[UI_MENU_STICK_DEADZONE_NOW] = "стик сейчас: %.0f %%",
|
||||
[UI_MENU_MOUSE_MODE] = "Управление мышью",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Прямое: камера поворачивается на столько, на сколько сдвинута мышь, как в играх для ПК, без мёртвой зоны и кривой стика (версия игры 1.4.3; на других версиях эмуляция стика). Эмуляция стика: мышь работает как правый стик.",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Прямое: камера поворачивается на столько, на сколько сдвинута мышь, как в играх для ПК, без мёртвой зоны и кривой стика (версии игры 1.4.3 и 1.2.1; на 1.1.0 эмуляция стика). Эмуляция стика: мышь работает как правый стик.",
|
||||
[UI_MENU_MOUSE_STICK] = "Эмуляция стика",
|
||||
[UI_MENU_MOUSE_DIRECT] = "Прямое",
|
||||
[UI_MENU_MENU_CURSOR] = "Мышь в меню игры",
|
||||
@@ -1040,6 +1054,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_ASPECT_DESC] = "Quando o formato da janela difere do da resolução do jogo (uma imagem 16:9 numa tela "
|
||||
"21:9): manter o formato da imagem com barras pretas ou esticá-la sobre a janela.",
|
||||
[UI_MENU_ASPECT_FIT] = "Barras pretas", [UI_MENU_ASPECT_STRETCH] = "Esticar",
|
||||
[UI_MENU_MOVIE_ASPECT] = "Formato dos vídeos",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "Quando a resolução do jogo não é 16:9: manter o formato dos vídeos das cenas com "
|
||||
"barras pretas ou esticá-los sobre a imagem toda. Vale a partir do próximo vídeo.",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "Barras pretas", [UI_MENU_MOVIE_ASPECT_STRETCH] = "Esticar",
|
||||
[UI_MENU_SHARPNESS] = "Nitidez do FSR",
|
||||
[UI_MENU_SHARPNESS_DESC] = "Aumento de nitidez após a ampliação do FSR; 0 desliga.",
|
||||
|
||||
@@ -1054,6 +1072,9 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
"a janela perde o foco, como no Switch ao apertar Home; um carregamento interrompido "
|
||||
"assim pode parar de vez.",
|
||||
[UI_MENU_FPS_DISPLAY] = "Taxa do monitor", [UI_MENU_FPS_DISPLAY_HZ] = "Taxa do monitor (%.0f Hz)",
|
||||
[UI_MENU_FRAME_QUEUE] = "Fila de quadros",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "O jogo começa um quadro enquanto o anterior espera a vez de aparecer: muito menos quadros "
|
||||
"perdidos no limite, com até um quadro a mais de atraso na entrada. Vale na hora.",
|
||||
[UI_MENU_REALTIME] = "Velocidade do jogo pelo relógio",
|
||||
[UI_MENU_REALTIME_DESC] = "Avança o jogo pelo tempo real, para que um quadro lento não deixe o jogo mais lento.",
|
||||
[UI_MENU_CLOTH_REALTIME] = "Tecidos pelo relógio",
|
||||
@@ -1349,7 +1370,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_XBOX] = "Xbox",
|
||||
[UI_MENU_PROMPTS_PC] = "PC",
|
||||
[UI_MENU_MOUSE_MODE] = "Câmera com o mouse",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direta: a câmera gira o quanto o mouse se move, como num jogo de PC, sem zona morta nem curva do analógico (jogo 1.4.3; as outras versões usam a emulação do analógico). Emulação do analógico: o mouse age como o analógico direito.",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "Direta: a câmera gira o quanto o mouse se move, como num jogo de PC, sem zona morta nem curva do analógico (jogos 1.4.3 e 1.2.1; o 1.1.0 usa a emulação do analógico). Emulação do analógico: o mouse age como o analógico direito.",
|
||||
[UI_MENU_MOUSE_STICK] = "Emulação do analógico",
|
||||
[UI_MENU_MOUSE_DIRECT] = "Direta",
|
||||
[UI_MENU_MENU_CURSOR] = "Mouse nos menus do jogo",
|
||||
@@ -1549,6 +1570,11 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
"用黑边保持画面比例,或拉伸铺满窗口。",
|
||||
[UI_MENU_ASPECT_FIT] = "黑边",
|
||||
[UI_MENU_ASPECT_STRETCH] = "拉伸",
|
||||
[UI_MENU_MOVIE_ASPECT] = "视频画面比例",
|
||||
[UI_MENU_MOVIE_ASPECT_DESC] = "当游戏分辨率不是 16:9 时:用黑边保持过场视频的原有比例,或拉伸铺满整个画面。"
|
||||
"从下一段视频起生效。",
|
||||
[UI_MENU_MOVIE_ASPECT_KEEP] = "黑边",
|
||||
[UI_MENU_MOVIE_ASPECT_STRETCH] = "拉伸",
|
||||
[UI_MENU_FOV] = "视野",
|
||||
[UI_MENU_FOV_DESC] = "镜头能看到多大范围,以游戏自身视野的百分比表示。"
|
||||
"仅限游戏版本 1.4.3。",
|
||||
@@ -1582,6 +1608,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_FPS_144] = "144 FPS",
|
||||
[UI_MENU_FPS_DISPLAY] = "显示器刷新率",
|
||||
[UI_MENU_FPS_DISPLAY_HZ] = "显示器刷新率(%.0f Hz)",
|
||||
[UI_MENU_FRAME_QUEUE] = "帧队列",
|
||||
[UI_MENU_FRAME_QUEUE_DESC] = "前一帧等待显示时游戏就开始绘制下一帧:在帧率上限下掉帧大幅减少,输入延迟最多增加一帧。立即生效。",
|
||||
[UI_MENU_REALTIME] = "游戏速度跟随时钟",
|
||||
[UI_MENU_REALTIME_DESC] = "按真实时间推进游戏,帧率低时游戏速度也不会变慢。",
|
||||
[UI_MENU_CLOTH_REALTIME] = "布料跟随时钟",
|
||||
@@ -1955,7 +1983,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
|
||||
[UI_MENU_PROMPTS_XBOX] = "Xbox",
|
||||
[UI_MENU_PROMPTS_PC] = "PC",
|
||||
[UI_MENU_MOUSE_MODE] = "鼠标视角",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "直接:镜头按鼠标移动的距离转动,与电脑游戏相同,没有摇杆死区和响应曲线(游戏 1.4.3;其他版本使用摇杆模拟)。摇杆模拟:鼠标相当于右摇杆。",
|
||||
[UI_MENU_MOUSE_MODE_DESC] = "直接:镜头按鼠标移动的距离转动,与电脑游戏相同,没有摇杆死区和响应曲线(游戏 1.4.3 和 1.2.1;1.1.0 使用摇杆模拟)。摇杆模拟:鼠标相当于右摇杆。",
|
||||
[UI_MENU_MOUSE_STICK] = "摇杆模拟",
|
||||
[UI_MENU_MOUSE_DIRECT] = "直接",
|
||||
[UI_MENU_MENU_CURSOR] = "在游戏菜单中使用鼠标",
|
||||
|
||||
@@ -37,6 +37,7 @@ typedef enum {
|
||||
UI_MENU_RENDER_SCALE, UI_MENU_RENDER_SCALE_DESC, UI_MENU_RENDER_SCALE_GAME,
|
||||
UI_MENU_UPSCALER, UI_MENU_UPSCALER_DESC, UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR,
|
||||
UI_MENU_ASPECT, UI_MENU_ASPECT_DESC, UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH,
|
||||
UI_MENU_MOVIE_ASPECT, UI_MENU_MOVIE_ASPECT_DESC, UI_MENU_MOVIE_ASPECT_KEEP, UI_MENU_MOVIE_ASPECT_STRETCH,
|
||||
UI_MENU_FOV, UI_MENU_FOV_DESC,
|
||||
UI_MENU_SHADOW_RES, UI_MENU_SHADOW_RES_DESC, UI_MENU_SHADOW_RES_GAME,
|
||||
UI_MENU_ANISO, UI_MENU_ANISO_DESC, UI_MENU_ANISO_GAME,
|
||||
@@ -62,6 +63,7 @@ typedef enum {
|
||||
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60, UI_MENU_FPS_90,
|
||||
UI_MENU_FPS_120, UI_MENU_FPS_144,
|
||||
UI_MENU_FPS_DISPLAY, UI_MENU_FPS_DISPLAY_HZ,
|
||||
UI_MENU_FRAME_QUEUE, UI_MENU_FRAME_QUEUE_DESC,
|
||||
UI_MENU_REALTIME, UI_MENU_REALTIME_DESC,
|
||||
UI_MENU_CLOTH_REALTIME, UI_MENU_CLOTH_REALTIME_DESC,
|
||||
UI_MENU_HIGHFPS_FIX, UI_MENU_HIGHFPS_FIX_DESC,
|
||||
|
||||
+18
-1
@@ -157,6 +157,17 @@ static void make_key(const tex_t *t, TexKey *k) {
|
||||
}
|
||||
|
||||
// Keys equal apart from the descriptor's swizzle selectors: the same storage, format and geometry.
|
||||
// The colour format without its sRGB decode: a target and its sRGB view share one texel layout (slot_resolve).
|
||||
static int linear_format(int f) {
|
||||
switch (f) {
|
||||
case NVN_FORMAT_RGBA8_SRGB: return NVN_FORMAT_RGBA8;
|
||||
case NVN_FORMAT_RGBX8_SRGB: return NVN_FORMAT_RGBX8;
|
||||
case NVN_FORMAT_BGRA8_SRGB: return NVN_FORMAT_BGRA8;
|
||||
case NVN_FORMAT_BGRX8_SRGB: return NVN_FORMAT_BGRX8;
|
||||
default: return f;
|
||||
}
|
||||
}
|
||||
|
||||
static int same_but_swizzle(const TexKey *a, const TexKey *b) {
|
||||
TexKey x = *a, y = *b;
|
||||
x.d.swizzle_r = x.d.swizzle_g = x.d.swizzle_b = x.d.swizzle_a = 0;
|
||||
@@ -1125,10 +1136,16 @@ static Tex *slot_resolve(TexSlot *s, view_t *view, int *has_view) {
|
||||
// Prefer the live entry that differs from the slot descriptor only in its swizzle: transient render targets
|
||||
// share storage, and the newest one at the address is usually another size. The map screen samples its
|
||||
// 2250x1080 map target through an (R,G,B,1) descriptor and followed a 960x60 target at the same address.
|
||||
// Only an entry of the slot's texel format follows (sRGB aside): Maxwell reads the storage through the slot
|
||||
// descriptor's format, and an entry of another format holds another texture's texels. The quick menu samples
|
||||
// its 768x432 RGB10A2 scene capture through a slot whose entry was released; after a fuse the Autobuild
|
||||
// thumbnail of the fused object, RGBA8 at the same storage, was the live entry, and the menu drew it behind
|
||||
// the wheel (the object's silhouette, or garbled colours). Without such an entry the released one keeps the
|
||||
// slot: its image holds what was last written through that descriptor.
|
||||
Tex *own = e, *any = NULL;
|
||||
e = NULL;
|
||||
for (Tex *p = nvn_hm_get(&by_addr, &k.addr); p; p = p->next_at) {
|
||||
if (p->released) continue;
|
||||
if (p->released || linear_format(p->key.d.format) != linear_format(k.d.format)) continue;
|
||||
if (!any) any = p;
|
||||
if (same_but_swizzle(&p->key, &k)) { e = p; break; }
|
||||
}
|
||||
|
||||
+244
-1
@@ -1,4 +1,5 @@
|
||||
"""The installer window: event model, form, catalogs, cancel, a self-closing Tk run and one end-to-end run.
|
||||
"""The installer window: event model, form, catalogs, cancel, DPI awareness, a self-closing Tk run and one end-to-end
|
||||
run.
|
||||
|
||||
.venv-py314\\Scripts\\python tests\\installer_gui_test.py
|
||||
|
||||
@@ -21,12 +22,19 @@ import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
import unittest.mock
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from installer import layout as layouts # noqa: E402
|
||||
from installer import dpi # noqa: E402
|
||||
from installer import recent # noqa: E402
|
||||
try:
|
||||
from installer import gui # noqa: E402
|
||||
except ImportError: # no tkinter
|
||||
gui = None
|
||||
from installer import state as statefile # noqa: E402
|
||||
from installer.events import Reporter # noqa: E402
|
||||
from installer.runner import Context # noqa: E402
|
||||
@@ -341,6 +349,241 @@ class CancelTest(unittest.TestCase):
|
||||
p.kill()
|
||||
|
||||
|
||||
class DpiTest(unittest.TestCase):
|
||||
def test_px_follows_the_display_scale(self):
|
||||
scale = dpi.scale()
|
||||
self.assertGreaterEqual(scale, 1.0)
|
||||
self.assertEqual(dpi.px(0), 0)
|
||||
self.assertEqual(dpi.px(14), round(14 * scale))
|
||||
|
||||
def test_px_at_the_scales_of_other_displays(self):
|
||||
"""A 96 DPI runner cannot show the scaling: give px() the scales Windows reports at 125, 150 and 200%."""
|
||||
saved = dpi._scale
|
||||
try:
|
||||
for scale, pad, wrap in ((1.0, 14, 620), (1.25, 18, 775), (1.5, 21, 930), (2.0, 28, 1240)):
|
||||
dpi._scale = scale
|
||||
self.assertEqual((dpi.px(14), dpi.px(620)), (pad, wrap), scale)
|
||||
finally:
|
||||
dpi._scale = saved
|
||||
|
||||
@unittest.skipUnless(os.name == 'nt', 'the awareness and the DPI Tk reads are Windows settings')
|
||||
def test_the_window_draws_at_the_display_scale(self):
|
||||
"""A fresh process imports the window (which makes itself DPI aware) and starts Tk: the fonts must be sized for
|
||||
the display, not for 96 DPI, which is what Windows then stretched (the blurry window)."""
|
||||
try:
|
||||
import tkinter # noqa: F401
|
||||
except ImportError:
|
||||
self.skipTest('no tkinter')
|
||||
code = ('import tkinter\n'
|
||||
'from installer import dpi, gui\n'
|
||||
'root = tkinter.Tk()\n'
|
||||
'print(dpi.awareness(), dpi.scale(), root.tk.call("tk", "scaling"), gui.PAD, gui.WRAP)\n'
|
||||
'root.destroy()\n')
|
||||
r = subprocess.run([sys.executable, '-c', code], cwd=ROOT, capture_output=True, text=True, timeout=120)
|
||||
self.assertEqual(r.returncode, 0, r.stderr[-2000:])
|
||||
awareness, scale, scaling, pad, wrap = r.stdout.split()
|
||||
self.assertGreaterEqual(int(awareness), 1) # 1 system aware, 2 per-monitor aware
|
||||
self.assertAlmostEqual(float(scaling), float(scale) * 96 / 72, places=6)
|
||||
self.assertEqual((int(pad), int(wrap)), (round(14 * float(scale)), round(620 * float(scale))))
|
||||
|
||||
|
||||
class RecentTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, self.tmp, True)
|
||||
self.file = self.tmp / 'sub' / 'setup.json'
|
||||
old = os.environ.get(recent.ENV)
|
||||
os.environ[recent.ENV] = str(self.file)
|
||||
self.addCleanup(lambda: os.environ.pop(recent.ENV, None) if old is None else os.environ.update({recent.ENV: old}))
|
||||
self.dest = self.tmp / 'TotK'
|
||||
self.dest.mkdir()
|
||||
|
||||
def test_override_is_honoured(self):
|
||||
self.assertEqual(recent.path(), self.file)
|
||||
|
||||
def test_missing_file(self):
|
||||
self.assertEqual(recent.load(), {})
|
||||
|
||||
def installed(self):
|
||||
(self.dest / statefile.NAME).write_text('{}', encoding='utf-8')
|
||||
|
||||
def files(self):
|
||||
out = {}
|
||||
for name in ('base', 'update', 'keys'):
|
||||
f = self.tmp / (name + '.bin')
|
||||
f.write_text('x', encoding='utf-8')
|
||||
out[name] = str(f)
|
||||
fw = self.tmp / 'fw'
|
||||
fw.mkdir()
|
||||
out['firmware'] = str(fw)
|
||||
return out
|
||||
|
||||
def write(self, **fields):
|
||||
self.file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.file.write_text(json.dumps(fields), encoding='utf-8')
|
||||
|
||||
def test_folder_without_state(self):
|
||||
recent.save(self.dest)
|
||||
self.assertEqual(recent.load(), {})
|
||||
|
||||
def test_round_trip(self):
|
||||
self.installed()
|
||||
src = self.files()
|
||||
recent.save(self.dest, src)
|
||||
want = {name: os.path.abspath(v) for name, v in src.items()}
|
||||
want['dest'] = os.path.abspath(self.dest)
|
||||
self.assertEqual(json.loads(self.file.read_text(encoding='utf-8')), {'format': 1, **want})
|
||||
self.assertEqual(recent.load(), want)
|
||||
self.assertFalse(self.file.with_name('setup.json.tmp').exists())
|
||||
|
||||
def test_empty_field_keeps_saved_value(self):
|
||||
self.installed()
|
||||
src = self.files()
|
||||
recent.save(self.dest, src)
|
||||
recent.save(self.dest, {'base': '', 'update': ' ', 'keys': src['keys']})
|
||||
self.assertEqual(recent.load()['base'], os.path.abspath(src['base']))
|
||||
self.assertEqual(recent.load()['firmware'], os.path.abspath(src['firmware']))
|
||||
|
||||
def test_missing_path_not_saved(self):
|
||||
self.installed()
|
||||
recent.save(self.dest, {'base': str(self.tmp / 'gone.xci'), 'firmware': str(self.tmp / 'nofw')})
|
||||
self.assertEqual(recent.load(), {'dest': os.path.abspath(self.dest)})
|
||||
self.assertNotIn('base', json.loads(self.file.read_text(encoding='utf-8')))
|
||||
|
||||
def test_vanished_path_dropped_on_load(self):
|
||||
self.installed()
|
||||
src = self.files()
|
||||
recent.save(self.dest, src)
|
||||
os.remove(src['base'])
|
||||
self.assertNotIn('base', recent.load())
|
||||
self.assertIn('update', recent.load())
|
||||
|
||||
def test_directory_as_file_dropped(self):
|
||||
self.installed()
|
||||
self.write(format=1, dest=str(self.dest), base=str(self.tmp), update=str(self.tmp), keys=str(self.tmp))
|
||||
self.assertEqual(recent.load(), {'dest': str(self.dest)})
|
||||
|
||||
def test_file_as_firmware_dropped(self):
|
||||
self.installed()
|
||||
src = self.files()
|
||||
self.write(format=1, dest=str(self.dest), firmware=src['base'])
|
||||
self.assertEqual(recent.load(), {'dest': str(self.dest)})
|
||||
|
||||
def test_dest_file_dropped(self):
|
||||
f = self.tmp / 'afile'
|
||||
f.write_text('x', encoding='utf-8')
|
||||
self.write(format=1, dest=str(f))
|
||||
self.assertEqual(recent.load(), {})
|
||||
|
||||
def test_bad_values_dropped(self):
|
||||
self.installed()
|
||||
src = self.files()
|
||||
self.write(format=1, dest=str(self.dest), base=5, update='', keys='rel/prod.keys', firmware=['x'])
|
||||
self.assertEqual(recent.load(), {'dest': str(self.dest)})
|
||||
self.write(format=1, dest=str(self.dest), base=src['base'] + '\0x', update=None, keys=True)
|
||||
self.assertEqual(recent.load(), {'dest': str(self.dest)})
|
||||
self.write(format=1, dest='relative/dir')
|
||||
self.assertEqual(recent.load(), {})
|
||||
|
||||
def test_stat_errors_dropped(self):
|
||||
self.installed()
|
||||
src = self.files()
|
||||
self.write(format=1, dest=str(self.dest), base=src['base'])
|
||||
for exc in (OSError('share gone'), ValueError('embedded null byte')):
|
||||
with unittest.mock.patch.object(Path, 'is_file', side_effect=exc):
|
||||
self.assertEqual(recent.load(), {})
|
||||
|
||||
def test_old_dest_only_format(self):
|
||||
self.installed()
|
||||
self.write(format=0, dest=str(self.dest))
|
||||
self.assertEqual(recent.load(), {'dest': str(self.dest)})
|
||||
|
||||
def test_garbage_file(self):
|
||||
self.file.parent.mkdir()
|
||||
for text in ('not json', '[]', '{"dest": 5}', '{"format": 1}'):
|
||||
self.file.write_text(text, encoding='utf-8')
|
||||
self.assertEqual(recent.load(), {}, text)
|
||||
|
||||
@unittest.skipIf(gui is None, 'no tkinter')
|
||||
def test_blocking_load_leaves_fields_empty(self):
|
||||
with unittest.mock.patch.object(recent, 'load', side_effect=lambda: time.sleep(5) or {'dest': 'x'}):
|
||||
start = time.monotonic()
|
||||
self.assertEqual(gui.load_recent(0.3), {})
|
||||
self.assertLess(time.monotonic() - start, 2)
|
||||
|
||||
@unittest.skipIf(gui is None, 'no tkinter')
|
||||
def test_fast_load_is_returned(self):
|
||||
with unittest.mock.patch.object(recent, 'load', return_value={'dest': 'x'}):
|
||||
self.assertEqual(gui.load_recent(), {'dest': 'x'})
|
||||
|
||||
|
||||
class FakeVar:
|
||||
def __init__(self, value=''):
|
||||
self.value = value
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
def set(self, value):
|
||||
self.value = value
|
||||
|
||||
|
||||
@unittest.skipIf(gui is None, 'no tkinter')
|
||||
class PrefillTest(unittest.TestCase):
|
||||
"""App.match_prefill on a stand-in object: no window needed."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, self.tmp, True)
|
||||
self.update = self.tmp / 'totk-1.4.3.nsp'
|
||||
self.update.write_bytes(b'x' * 10)
|
||||
self.base = self.tmp / 'totk.xci'
|
||||
self.base.write_bytes(b'y' * 20)
|
||||
self.app = SimpleNamespace(vars={n: FakeVar() for n in ('base', 'update')}, touched=set(),
|
||||
recorded={'base': str(self.base), 'update': str(self.update)})
|
||||
self.app.set_prefill = lambda name, value: self.app.vars[name].set(value)
|
||||
for name, value in self.app.recorded.items():
|
||||
self.app.vars[name].set(value)
|
||||
|
||||
def state(self, update_name, update_size, base_size=20):
|
||||
return {'inputs': {'base': {'name': 'totk.xci', 'size': base_size},
|
||||
'update': {'name': update_name, 'size': update_size}}}
|
||||
|
||||
def shown(self):
|
||||
return {n: v.get() for n, v in self.app.vars.items()}
|
||||
|
||||
def test_same_install_keeps(self):
|
||||
gui.App.match_prefill(self.app, self.state('totk-1.4.3.nsp', 10))
|
||||
self.assertEqual(self.shown(), {'base': str(self.base), 'update': str(self.update)})
|
||||
|
||||
def test_other_install_clears_and_restores(self):
|
||||
gui.App.match_prefill(self.app, self.state('totk-1.2.1.nsp', 10))
|
||||
self.assertEqual(self.shown(), {'base': str(self.base), 'update': ''})
|
||||
gui.App.match_prefill(self.app, self.state('totk-1.4.3.nsp', 99)) # same name, other size
|
||||
self.assertEqual(self.shown()['update'], '')
|
||||
gui.App.match_prefill(self.app, self.state('totk-1.4.3.nsp', 10))
|
||||
self.assertEqual(self.shown()['update'], str(self.update))
|
||||
|
||||
def test_install_without_recorded_inputs_clears(self):
|
||||
gui.App.match_prefill(self.app, {'inputs': {}})
|
||||
self.assertEqual(self.shown(), {'base': '', 'update': ''})
|
||||
|
||||
def test_user_picked_field_survives(self):
|
||||
self.app.vars['update'].set('C:/mine/other.nsp')
|
||||
self.app.touched.add('update')
|
||||
gui.App.match_prefill(self.app, self.state('totk-1.2.1.nsp', 10, base_size=1))
|
||||
self.assertEqual(self.shown(), {'base': '', 'update': 'C:/mine/other.nsp'})
|
||||
|
||||
def test_fresh_install_keeps(self):
|
||||
gui.App.match_prefill(self.app, None)
|
||||
self.assertEqual(self.shown(), {'base': str(self.base), 'update': str(self.update)})
|
||||
|
||||
def test_vanished_file_clears(self):
|
||||
self.update.unlink()
|
||||
gui.App.match_prefill(self.app, self.state('totk-1.4.3.nsp', 10))
|
||||
self.assertEqual(self.shown()['update'], '')
|
||||
|
||||
|
||||
class SmokeTest(unittest.TestCase):
|
||||
def test_every_screen_in_both_languages(self):
|
||||
try:
|
||||
|
||||
+373
-27
@@ -5,6 +5,7 @@ Usage: python tests/mouse_direct_check.py --cc <clang.exe>
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
@@ -13,14 +14,15 @@ HARNESS = r'''
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include "mouse_direct.c"
|
||||
const int rt_game_version = TEST_GAME_VERSION;
|
||||
static int raw_x, raw_y, takes, captured=1, foreground=1, allowed=1, mode=1, patched=1, selecting;
|
||||
static float ratio=0.5f;
|
||||
static uint64_t now=1000000000;
|
||||
static GuestFn hook, activity_hook, begin_hook, angles_hook, hand_hook;
|
||||
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,12 +40,16 @@ 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; }
|
||||
#if TEST_GAME_VERSION == 143
|
||||
int main(void) {
|
||||
assert(hook && activity_hook);
|
||||
Cpu c, expected;
|
||||
@@ -53,19 +59,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 +116,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 +136,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 +156,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 +192,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.
|
||||
c.x[20]=(uintptr_t)target; c.v[9]=c.v[10]=0; raw_x=100; raw_y=50; ratio=0.5f; hook(&c);
|
||||
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 HAND_PER_COUNT a count, 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=200; 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(200*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,7 +235,290 @@ 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);
|
||||
// A step without mouse motion keeps every load as the game made it: a stick value something else changed
|
||||
// after the input update stays, and so does a -0 base angle (bit for bit)
|
||||
{
|
||||
SAMPLE(0,0);
|
||||
guest_float_store((uintptr_t)input+0x420,pad_x+0.125f);
|
||||
assert(LOAD(0x23bee2c,1)==pad_x+0.125f);
|
||||
float neg_zero=-0.0f; memcpy(&frame[0x18-0x12],&neg_zero,4);
|
||||
float got=LOAD(0x23beeec,1); assert(!memcmp(&got,&neg_zero,4));
|
||||
frame[0x18-0x12]=40;
|
||||
}
|
||||
// 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");
|
||||
}
|
||||
#else
|
||||
// 1.2.1: the same hooks with that version's registers and stack slots (MouseLayout layout_121) and its load sites
|
||||
int main(void) {
|
||||
assert(hook && activity_hook && begin_hook && angles_hook && hand_hook && load_hook);
|
||||
(void)takes;
|
||||
Cpu c, expected;
|
||||
unsigned char target[0x500]={0}, camera[0x300]={0}, hand[0x300]={0};
|
||||
uint64_t frame[0x20]={0};
|
||||
memset(&c,0x5a,sizeof c);
|
||||
// The camera input update keeps x in s11 (y in s10) and stores s11 at +0x420; s9 is not the update's
|
||||
c.x[20]=(uintptr_t)target; c.x[21]=0; c.v[11]=c.v[10]=0; expected=c;
|
||||
raw_x=100; raw_y=-50; ratio=0.5f; hook(&c);
|
||||
assert(fabsf(scalar(c.v[11])-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[11]));
|
||||
expected.v[11]=c.v[11]; expected.v[10]=c.v[10]; assert(!memcmp(&expected,&c,sizeof c));
|
||||
// The activity hook: flags in w8, the displaced ldp d9, d8, [sp, #0x10]
|
||||
uint64_t stack[4]={0,0,0x123456789abcdef0ull,0x456789abcdef0123ull}; c.sp=(uintptr_t)stack;
|
||||
c.x[8]=0x28; activity_hook(&c); assert((uint32_t)c.x[8]==0x11 && c.v[9]==stack[2] && c.v[8]==stack[3]);
|
||||
// The free orbit: the final pitch and yaw at [sp+0x3c] / [sp+0x40], the displaced ldr s13, [sp, #0x3c]
|
||||
uint64_t input_address=(uintptr_t)target; memcpy(camera+0x20,&input_address,8);
|
||||
c.x[20]=(uintptr_t)target; c.v[11]=c.v[10]=0; raw_x=100; raw_y=-50; hook(&c);
|
||||
guest_float_store((uintptr_t)camera+0x108,30); guest_float_store((uintptr_t)camera+0x120,40);
|
||||
guest_float_store((uintptr_t)frame+0x3c,30); guest_float_store((uintptr_t)frame+0x40,40);
|
||||
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+0x3c)-(30-50*DEG_PER_COUNT))<0.0001f);
|
||||
assert(fabsf(guest_float((uintptr_t)frame+0x40)-(40-100*DEG_PER_COUNT))<0.0001f);
|
||||
assert(scalar(c.v[13])==guest_float((uintptr_t)frame+0x3c));
|
||||
// Ultrahand: x in s9, y in s15, the displaced str w9, [sp, #0x18]
|
||||
c.x[20]=(uintptr_t)target; c.v[11]=c.v[10]=0; raw_x=20; raw_y=10; hook(&c);
|
||||
c.x[19]=(uintptr_t)hand; c.x[9]=0x12345678; c.v[9]=0; c.v[15]=0; hand_hook(&c);
|
||||
assert(fabsf(scalar(c.v[9])-20*HAND_PER_COUNT/ratio)<1e-5f && fabsf(scalar(c.v[15])+10*HAND_PER_COUNT/ratio)<1e-5f);
|
||||
uint32_t w9; memcpy(&w9,(unsigned char *)frame+0x18,4); assert(w9==0x12345678);
|
||||
// Load sites
|
||||
{
|
||||
unsigned char input[0x500]={0}, cam[0x200]={0}; uint64_t lsp[0x10]={0}, fp[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[11]=0; c.v[10]=0; \
|
||||
memcpy(&c.v[11],&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)
|
||||
c.x[19]=(uintptr_t)cam; c.sp=(uintptr_t)lsp; c.x[29]=(uintptr_t)(fp+0x10);
|
||||
// Aiming: the pitch target the helper computed (+0x15c + turn) is stored with the degrees, and s0 holds it
|
||||
SAMPLE(100,-50); c.x[22]=(uintptr_t)input;
|
||||
assert(LOAD(0x1d34338,1)==pad_y);
|
||||
float target_pitch=12; c.v[0]=0; memcpy(&c.v[0],&target_pitch,4);
|
||||
c.x[30]=MOD_MAIN_BASE+0x1d34374+4; load_hook(&c);
|
||||
assert(fabsf(scalar(c.v[0])-(12-50*DEG_PER_COUNT))<1e-5f && guest_float((uintptr_t)cam+0x154)==scalar(c.v[0]));
|
||||
// with no stick load before it the store is the game's, bit for bit
|
||||
float neg_zero=-0.0f; memcpy(&c.v[0],&neg_zero,4); c.x[30]=MOD_MAIN_BASE+0x1d34374+4; load_hook(&c);
|
||||
float got=guest_float((uintptr_t)cam+0x154); assert(!memcmp(&got,&neg_zero,4));
|
||||
assert(LOAD(0x1d34584,1)==pad_x);
|
||||
guest_float_store((uintptr_t)(fp+0x10)-0x48,40);
|
||||
assert(fabsf(LOAD(0x1d345d0,1)-(40-100*DEG_PER_COUNT))<1e-5f);
|
||||
// Horse yaw: the turn ([sp+0x18]) the helper adds to +0x68 gets the degrees
|
||||
SAMPLE(100,-50); c.x[20]=(uintptr_t)input; guest_float_store((uintptr_t)lsp+0x18,0.5f);
|
||||
assert(LOAD(0x1d4070c,8)==pad_x && fabsf(LOAD(0x1d40740,0)-(0.5f-100*DEG_PER_COUNT))<1e-5f);
|
||||
// Wakeboard reads each axis twice: the first reads give the same values and leave the degrees to the second
|
||||
SAMPLE(100,-50); c.x[22]=(uintptr_t)input;
|
||||
float total=guest_float((uintptr_t)input+0x424);
|
||||
guest_float_store((uintptr_t)cam+0xd8,12); guest_float_store((uintptr_t)lsp+0x38,20);
|
||||
assert(LOAD(0x1d50738,0)==pad_x && LOAD(0x1d50740,1)==total);
|
||||
assert(LOAD(0x1d50768,0)==total && LOAD(0x1d5076c,1)==pad_x);
|
||||
assert(fabsf(LOAD(0x1d50e5c,0)-(12-50*DEG_PER_COUNT))<1e-5f);
|
||||
assert(fabsf(LOAD(0x1d50f20,12)-(20-100*DEG_PER_COUNT))<1e-5f);
|
||||
assert(LOAD(0x1d50f20,12)==20); // once
|
||||
// Waterfall climbing: the yaw is loaded into w8 as bits by ldr w8, [sp, #0x38]
|
||||
SAMPLE(100,-50); c.x[22]=(uintptr_t)input; guest_float_store((uintptr_t)lsp+0x38,11);
|
||||
assert(LOAD(0x1d51e10,13)==pad_x);
|
||||
c.x[30]=MOD_MAIN_BASE+0x1d51fb8+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);
|
||||
// Dialogue: the patched mov set x23 = x19 + 0x5c; the pitch load is ldr s1, [x23]
|
||||
SAMPLE(100,-50); c.x[24]=(uintptr_t)input; c.x[23]=(uintptr_t)cam+0x5c; guest_float_store((uintptr_t)cam+0x5c,1);
|
||||
assert(LOAD(0x1d3a3f8,11)==pad_y && fabsf(LOAD(0x1d3a420,1)-(1-50*DEG_PER_COUNT))<1e-5f);
|
||||
assert(c.x[23]==(uintptr_t)cam+0x5c);
|
||||
#undef LOAD
|
||||
#undef SAMPLE
|
||||
}
|
||||
puts("direct mouse 1.2.1: camera input in s11, activity, orbit angles at sp+0x3c/0x40, Ultrahand in s9 passed");
|
||||
puts("direct mouse 1.2.1: aiming pitch store, horse turn, repeated stick reads, w8 yaw and dialogue loads passed");
|
||||
}
|
||||
#endif
|
||||
'''
|
||||
|
||||
INPUT_HARNESS = r'''
|
||||
@@ -283,6 +596,36 @@ int main(int argc,char **argv) {
|
||||
}
|
||||
'''
|
||||
|
||||
def bl_target(address, word):
|
||||
"""The target of a bl at address, None for another instruction."""
|
||||
if word >> 26 != 0x25:
|
||||
return None
|
||||
imm = word & 0x3ffffff
|
||||
return address + 4 * (imm - (1 << 26) if imm & (1 << 25) else imm)
|
||||
|
||||
|
||||
def check_tables():
|
||||
"""Each version's load-site table (runtime/mouse_direct.c) against its patch file: one bl to page +0xf80 a site, the
|
||||
same original word (a pre-indexed ldr s<t>, [x<n>, #imm]! is the plain ldr s<t>, [x<n>] after its patched mov)."""
|
||||
source = (ROOT / 'runtime' / 'mouse_direct.c').read_text()
|
||||
for version, page in (('143', 0x3adf000), ('121', 0x472b000)):
|
||||
block = re.search(r'load_sites_%s\[\] = \{(.*?)\n\};' % version, source, re.S)[1]
|
||||
table = {int(a, 16): int(w, 16) for a, w in re.findall(r'\{0x([0-9a-f]+), 0x([0-9a-f]+),', block)}
|
||||
assert len(table) == block.count('{0x'), f'v{version}: a site twice in the table'
|
||||
sites = {}
|
||||
for line in (ROOT / 'recomp' / f'patches-v{version}.txt').read_text().splitlines():
|
||||
m = re.match(r'site mouse_direct main ([0-9a-f]+) ([0-9a-f]+) ([0-9a-f]+)', line)
|
||||
if m and bl_target(int(m[1], 16), int(m[3], 16)) == page + 0xf80:
|
||||
assert int(m[1], 16) not in sites, f'v{version} {m[1]} twice in the patch file'
|
||||
sites[int(m[1], 16)] = int(m[2], 16)
|
||||
assert sites.keys() == table.keys(), (version, sorted(hex(a) for a in set(sites) ^ set(table)))
|
||||
for a, orig in sites.items():
|
||||
# ldr s<t>, [x<n>, #imm]! -> ldr s<t>, [x<n>]
|
||||
plain = 0xbd400000 | (orig & 0x3ff) if (orig & 0xffe00c00) == 0xbc400c00 else orig
|
||||
assert table[a] == plain, f'v{version} {a:x}: table {table[a]:08x}, patch file {orig:08x}'
|
||||
print(f'direct mouse: the v{version} load-site table matches recomp/patches-v{version}.txt ({len(table)} sites)')
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('--cc', required=True)
|
||||
@@ -291,13 +634,16 @@ 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')
|
||||
link = [] if os.name == 'nt' else ['-lm']
|
||||
subprocess.run([args.cc, '-O2', '-Wall', '-Wextra', '-I', str(folder), '-I', str(ROOT/'runtime'),
|
||||
str(source), *link, '-o', str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
for version in (143, 121):
|
||||
subprocess.run([args.cc, '-O2', '-Wall', '-Wextra', f'-DTEST_GAME_VERSION={version}', '-I', str(folder),
|
||||
'-I', str(ROOT/'runtime'), str(source), *link, '-o', str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
check_tables()
|
||||
if args.deps:
|
||||
source.write_text(INPUT_HARNESS)
|
||||
script = folder / 'input.txt'
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Every setting the runtime knows is described: in totk.ini.example for players or in docs/settings.md for the rest.
|
||||
|
||||
python tests/settings_doc_check.py
|
||||
|
||||
Fails when a name of known[] (runtime/cfg.c) is in neither file, when docs/settings.md lists a name the runtime does
|
||||
not know, or when a default written in docs/settings.md differs from the one the code holds: the metas[] table of
|
||||
runtime/cfg.c or an rt_knob(name, default) call. No game files, no build.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def known_names(cfg):
|
||||
start = cfg.index('static const char *const known[]')
|
||||
return set(re.findall(r'"([A-Z0-9_]+)"', cfg[start:cfg.index('};', start)]))
|
||||
|
||||
|
||||
def code_defaults(cfg):
|
||||
"""{name: default} of metas[] and of the rt_knob calls; metas[] wins, as it is where a default lives."""
|
||||
found = {}
|
||||
for path in (ROOT / 'runtime').rglob('*'):
|
||||
if path.suffix in ('.c', '.cpp', '.h'):
|
||||
text = path.read_text(encoding='utf-8', errors='replace')
|
||||
found.update(re.findall(r'rt_knob\("(?:TOTK_)?([A-Z0-9_]+)", *"([^"]*)"\)', text))
|
||||
found.update(re.findall(r'\{"([A-Z0-9_]+)", "([^"]*)", (?:"[^"]*"|NULL)', cfg))
|
||||
return found
|
||||
|
||||
|
||||
def main():
|
||||
cfg = (ROOT / 'runtime' / 'cfg.c').read_text(encoding='utf-8')
|
||||
example = (ROOT / 'totk.ini.example').read_text(encoding='utf-8')
|
||||
doc = (ROOT / 'docs' / 'settings.md').read_text(encoding='utf-8')
|
||||
known = known_names(cfg)
|
||||
in_example = {name.upper() for name in re.findall(r'^#? ?([a-z][a-z0-9_]*) = ', example, re.M)}
|
||||
rows = re.findall(r'^\| `([a-z0-9_<>]+)`(?: \(default ([^)]*)\))? \|(.*)$', doc, re.M)
|
||||
in_doc = {name.upper() for name, _, _ in rows if '<' not in name}
|
||||
# The row of the button bindings names each key in its text.
|
||||
for name, _, text in rows:
|
||||
if '<' in name:
|
||||
in_doc |= {key.upper() for key in re.findall(r'`(bind_[a-z_]+)`', text)}
|
||||
|
||||
problems = [f'{name}: in runtime/cfg.c known[], described in neither totk.ini.example nor docs/settings.md'
|
||||
for name in sorted(known - in_example - in_doc)]
|
||||
problems += [f'{name}: in docs/settings.md, not in runtime/cfg.c known[]' for name in sorted(in_doc - known)]
|
||||
defaults = code_defaults(cfg)
|
||||
for name, written, _ in rows:
|
||||
code = defaults.get(name.upper())
|
||||
if code and written != f'`{code}`':
|
||||
problems.append(f'{name}: docs/settings.md says default {written or "none"}, the code says {code}')
|
||||
|
||||
for problem in problems:
|
||||
print(problem)
|
||||
print(f'settings_doc_check: {len(known)} names, {len(known & in_example)} in totk.ini.example, '
|
||||
f'{len(known & in_doc)} in docs/settings.md, {len(problems)} problems')
|
||||
return 1 if problems else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -33,6 +33,13 @@ MARKER_ROOT = {
|
||||
'AttentionLockOn_00': ('N_Offset_00',), # N_PosRoot_00 moves; N_Offset_00 animates y
|
||||
'EnemyInfo_00': ('Pa_EnemyLife_00', 'Pa_EnemyNotice_00'), # the parts move; N_EnemyLifeArea_* stay screen-sized
|
||||
'BalloonMessage_00': (), # the speech balloon part is pooled: its scale goes inside, see BALLOON_PART
|
||||
# The ability reticle (seen with Recall): N_App_00 moves to the target (a pane's own scale doesn't scale its
|
||||
# position); its children's scales are animated. The bow and throw pointers stay centred.
|
||||
'Pointer_00': ('N_App_00', 'N_Arrow_00', 'N_WeaponThrow_00'),
|
||||
# The Recall gauge ring: N_Trans_00 moves to the object and the code sets its scale by distance, so the scale goes
|
||||
# on the ring's pictures (mirrored, -1 as shipped; turned 0 or 180 degrees, so a scale on x stays undistorted).
|
||||
'ModorecoGauge_00': ('P_Smoke_00', 'P_Sh_00') + tuple(f'P_{p}{s}_00' for p in ('Base', 'Glow', 'Main', 'MainLight',
|
||||
'BandLight') for s in 'LR'),
|
||||
}
|
||||
|
||||
# Speech balloons are pooled: the game clones Pa_BalloonMessage_00 into Pa_BalloonMessage_%02d at run time, and the clones
|
||||
@@ -260,8 +267,8 @@ def apply(blarc, members, width, height, hud_corner=True):
|
||||
for pane in ('RootPane',) + MARKER_ROOT[layout]:
|
||||
at = pane_offset(layout, pane) + FIELD[root_scale]
|
||||
have = struct.unpack_from('<f', blarc, at)[0]
|
||||
if abs(have - 1.0) > 1e-3: raise SystemExit(f'{layout} {pane} {root_scale}: {have}, expected 1.0')
|
||||
if pane != 'RootPane': struct.pack_into('<f', blarc, at, k)
|
||||
if abs(abs(have) - 1.0) > 1e-3: raise SystemExit(f'{layout} {pane} {root_scale}: {have}, expected ±1.0')
|
||||
if pane != 'RootPane': struct.pack_into('<f', blarc, at, k * have) # mirrored panes stay mirrored
|
||||
continue
|
||||
at = pane_offset(member[5:-6], 'RootPane') + FIELD[root_scale]
|
||||
have = struct.unpack_from('<f', blarc, at)[0]
|
||||
|
||||
+19
-4
@@ -17,6 +17,9 @@
|
||||
# fps_limit = 60
|
||||
# Older key, read only while fps_limit is unset: fps60 = 0 means a limit of 30.
|
||||
# fps60 = 1
|
||||
# Frame queue: the game starts a frame while the previous one waits for its display time, so a frame that runs long
|
||||
# still makes its turn. Far fewer dropped frames at the limit for up to one frame more input delay. Takes effect at once.
|
||||
# frame_queue = 0
|
||||
# Real-time frame step: game speed follows the clock instead of the frame count.
|
||||
# realtime = 1
|
||||
# Cloth simulation on the real-time clock. Needs realtime = 1.
|
||||
@@ -144,7 +147,10 @@
|
||||
# (32:9), 1280x800, 1680x1050, 1920x1200, 2560x1600 (16:10), 1920x1280, 2160x1440, 2256x1504, 2880x1920 and 3000x2000
|
||||
# (3:2). Any other size
|
||||
# gets the camera's view but a 16:9 HUD, stretched over the picture, unless you build its layout with
|
||||
# tools/make_romfs_patches.py --aspect WxH. Read at start.
|
||||
# tools/make_romfs_patches.py --aspect WxH. Read at start. Default: 1920x1080, or on a primary display of one of those
|
||||
# shapes the smallest of its sizes at least 1080 high that fits the display (1920x1280 on a 3:2 laptop, 2560x1080 on
|
||||
# a 21:9 monitor), so the picture fills the screen without black bars; with prompts = xbox or pc it stays 1920x1080,
|
||||
# the only shape the setup builds those prompts for.
|
||||
# output_res = 1920x1080
|
||||
# 3D render scale: the scene's size relative to the game resolution, game (5/6, the game's own 1600x900 at 1080p) or
|
||||
# a percentage (25 to 400, at most 4096 pixels a side); above 100 supersamples the scene (anti-aliasing). Read at start.
|
||||
@@ -158,6 +164,10 @@
|
||||
# window is no longer stretched as it used to be; stretch fills the window. Changes while the game runs take effect at
|
||||
# once.
|
||||
# aspect = fit
|
||||
# Shape of the pre-rendered movies (16:9) when the game resolution (output_res) is not 16:9: keep, the default, draws
|
||||
# them in their own shape with black bars at the sides or above and below; stretch fills the whole picture, as the game
|
||||
# does. Read when a movie starts, so a change takes effect at the next movie.
|
||||
# movie_aspect = keep
|
||||
# Field of view, in percent of the game's own: 70 to 140, 100 (the default) leaves the camera as it is. Higher shows
|
||||
# more of the world around Link. Game version 1.4.3 only. Changes while the game runs take effect at once.
|
||||
# fov = 100
|
||||
@@ -301,15 +311,20 @@
|
||||
# lstick_deadzone = 8
|
||||
# rstick_deadzone = 8
|
||||
# Mouse camera mode: direct (default) or stick. Direct turns the camera by the raw mouse motion, the same on both axes,
|
||||
# with no stick dead zone or response curve (game 1.4.3; other game versions use stick emulation). Stick makes the
|
||||
# with no stick dead zone or response curve (games 1.4.3 and 1.2.1; 1.1.0 uses stick emulation). Stick makes the
|
||||
# mouse a right stick. Neither changes the gamepad. The Controls tab changes this while the game runs.
|
||||
# mouse_mode = direct
|
||||
# The mouse in the game's menus, 1.4.3 only (title, inventory, System tab, Options, shops, dialogue choices): while a
|
||||
# menu has the game's focus box the pointer shows, the item under it takes the focus, a left click is A on it and a
|
||||
# right click is B. 0 leaves the menus to the keys and the pad. Changes while the game runs.
|
||||
# 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.08 degrees per count, the same in every camera mode (about 14.3 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.08 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