diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index 1cf3a84c..be7c952d 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -332,6 +332,10 @@ const char* hn_mic(int v, int variant) { return missing ? "Not plugged in: Default stands in until it is back." : "This microphone, by its name. A change applies at once."; } const char* tx_rumble(int v) { return v ? tx_percent(v) : "Off"; } +const char* tx_stick_dead_zone(int v) { + static const char* t[] = {"Default (24%)", "5%", "10%", "15%", "20%", "30%"}; + return (unsigned)v < 6 ? t[v] : t[0]; +} // (hookshot) Audio output: the speakers the game mixes for, read once at its start (host/nn_audio.c au_menu_output) const char* tx_audio_output(int v) { static const char* t[] = {"Auto", "Stereo", "5.1"}; @@ -497,7 +501,7 @@ Row rows[] = { "What the ocarina hears when the game's microphone option is on (playing an instrument, or humming).", hn_mic, false, true}, - // Controls (14 rows) + // Controls (15 rows) {TAB_CONTROLS, "Controller in background", &opts.pad_background, 0, 1, 1, tx_onoff, "Whether the controller plays the game while another window has the focus.", hn_background}, {TAB_CONTROLS, "Mouse camera", &opts.mouse_native, 0, 1, 1, tx_mousecam, "How the captured mouse turns the camera.", @@ -511,6 +515,9 @@ Row rows[] = { "Inverts horizontal look (left/right) on the right analog stick.", nullptr, false, true}, {TAB_CONTROLS, "Invert camera Y", &opts.invert_cam_y, 0, 1, 1, tx_onoff, "Inverts vertical look (up/down) on the right analog stick.", nullptr, false, true}, + {TAB_CONTROLS, "Stick dead zone", &opts.stick_dead_zone, 0, 5, 1, tx_stick_dead_zone, + "How far a stick moves before the game sees it, on each axis. Lower for a precise stick, higher for one that drifts. " + "The game's own dead zone still follows.", nullptr, false, true}, {TAB_CONTROLS, "Rumble strength", &opts.rumble, 0, 150, 25, tx_rumble, "How strongly the controller rumbles, in percent of the game's rumble. Off turns it off.", nullptr, false, true}, {TAB_CONTROLS, "Motion controls", &opts.gyro, 0, 1, 1, tx_gyro, diff --git a/gpu/vk/vk_menu.h b/gpu/vk/vk_menu.h index ee5e1206..7a8f545f 100644 --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -33,6 +33,7 @@ struct Options { int invert_cam_x = 0; // right-stick camera horizontal inversion: 0 normal, 1 inverted int invert_cam_y = 0; // right-stick camera vertical inversion: 0 normal, 1 inverted int rumble = 100; // the controllers' rumble in percent of the game's: 0 Off, 25 to 150 (host/pad_sdl.c) + int stick_dead_zone = 0; // a stick axis below it is 0: 0 XInput's 7849 (~24%), 1..5 5% to 30% (host/nn_hid.c) int pad_layout = 0; // 0 Nintendo (default), 1 Xbox / PC, 2 Swap Face Buttons, 3 Custom int gyro = 1; // motion controls from a pad's gyroscope (host/pad_sdl.c, host/hos_motion.h); on by default int gyro_sens_x = 100; // the motion aim's horizontal sensitivity in percent diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index e6b315d9..649e08f3 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -3731,6 +3731,7 @@ void fill_replay_table(MkwVkRenderer& t) { t.nvn_register_texture = register_texture_queued; t.nvn_event_signal = event_signal_queued; t.option = [](int id) -> int { // (3: the fixed path's scale in 1/10000, vk_menu.cpp; DLAA: full scale, vk_dlss.cpp) + if (id == 11) return opts.stick_dead_zone; if (id == HOS_OPT_CHEAT_HEARTS) return menu_heart_protection(); if (id == HOS_OPT_CHEAT_FLIGHT) return menu_player_flight(); if (id == HOS_OPT_CHEAT_ACTION) return menu_take_cheat_action(); diff --git a/host/nn_hid.c b/host/nn_hid.c index 64180e08..387e8fd1 100644 --- a/host/nn_hid.c +++ b/host/nn_hid.c @@ -496,6 +496,15 @@ void hos_bind(int set, int button, int slot, int code) { static volatile LONG pad_raw_now; /* the gamepads' inputs at the last read, 1 << HOS_PAD_* */ uint32_t hos_pad_raw(void) { return (uint32_t)pad_raw_now; } +/* A stick axis cut at the dead zone, as XInput's and SDL's pads both are (pad_sdl.c): zone is the Insert menu's "Stick + dead zone" (option 11), 0 XInput's own 7849 (~24%, the only one before issue 752), 1 to 5 5%, 10%, 15%, + 20%, 30%. Per axis, as before: the game's own dead zone still follows */ +int32_t hos_stick_axis(int16_t v, int zone) { + static const int32_t cut[6] = {7849, 1638, 3277, 4915, 6553, 9830}; + const int32_t a = v < -32767 ? -32767 : v, t = cut[(unsigned)zone < 6 ? zone : 0]; + return (a > t || a < -t) ? a : 0; +} + static void pads_read(uint64_t *buttons, int32_t stick[4]) { static int64_t next_read, retry[XUSER_MAX_COUNT]; static int was_connected[XUSER_MAX_COUNT]; @@ -555,11 +564,9 @@ static void pads_read(uint64_t *buttons, int32_t stick[4]) { if (xs.Gamepad.bRightTrigger > 64) raw |= 1ULL << HOS_PAD_RTRIGGER; const int16_t thumb[4] = {xs.Gamepad.sThumbLX, xs.Gamepad.sThumbLY, xs.Gamepad.sThumbRX, xs.Gamepad.sThumbRY}; int32_t v[4]; - for (int i = 0; i < 4; i++) { - const int a = thumb[i] < -32767 ? -32767 : thumb[i]; - v[i] = (a > 7849 || a < -7849) ? a : 0; - } int rx_option(int id, int otherwise); + const int zone = rx_option(11, 0); /* (the menu's Stick dead zone) */ + for (int i = 0; i < 4; i++) v[i] = hos_stick_axis(thumb[i], zone); if (rx_option(16, 0)) v[2] = -v[2]; if (rx_option(17, 0)) v[3] = -v[3]; stick_max(s, v); diff --git a/host/pad_sdl.c b/host/pad_sdl.c index 3e3419a3..1f8e5cad 100644 --- a/host/pad_sdl.c +++ b/host/pad_sdl.c @@ -70,10 +70,11 @@ static HosMotionSample motion_hist[HOS_MOTION_HISTORY]; static int motion_head, motion_count, motion_live; static volatile LONG gyro_status; /* 0 no controller, 1 controllers but none with a gyro, 2 one with a gyro */ -static int32_t stick_value(int16_t v, int flip) { - int32_t a = v < -32767 ? -32767 : v; - if (flip) a = -a; - return (a > 7849 || a < -7849) ? a : 0; /* XInput's dead zone, as nn_hid.c uses */ +int32_t hos_stick_axis(int16_t v, int zone); /* nn_hid.c: the menu's Stick dead zone, as XInput's pads */ + +static int32_t stick_value(int16_t v, int flip, int zone) { + const int32_t a = hos_stick_axis(v, zone); + return flip ? -a : a; } /* SDL's controller support started; 0 when it could not be */ @@ -328,8 +329,11 @@ static void pads_poll(void) { if (hos_sdl_pad_axis(gc, AX_RT) > 8000) b |= 1ULL << HOS_PAD_RTRIGGER; const int inv_x = rx_option(16, 0); const int inv_y = rx_option(17, 0); - const int32_t v[4] = {stick_value(hos_sdl_pad_axis(gc, AX_LX), 0), stick_value(hos_sdl_pad_axis(gc, AX_LY), 1), - stick_value(hos_sdl_pad_axis(gc, AX_RX), inv_x), stick_value(hos_sdl_pad_axis(gc, AX_RY), inv_y ? 0 : 1)}; + const int z = rx_option(11, 0); /* (the menu's Stick dead zone: nn_hid.c's hos_stick_axis) */ + const int32_t v[4] = {stick_value(hos_sdl_pad_axis(gc, AX_LX), 0, z), + stick_value(hos_sdl_pad_axis(gc, AX_LY), 1, z), + stick_value(hos_sdl_pad_axis(gc, AX_RX), inv_x, z), + stick_value(hos_sdl_pad_axis(gc, AX_RY), inv_y ? 0 : 1, z)}; for (int k = 0; k < 4; k++) if ((v[k] < 0 ? -v[k] : v[k]) > (s[k] < 0 ? -s[k] : s[k])) s[k] = v[k]; { /* (motion: the pad last used; a stick counts by which way it is pushed, not by how far) */ diff --git a/readme.md b/readme.md index aa30efd3..3fcaabc5 100644 --- a/readme.md +++ b/readme.md @@ -239,6 +239,7 @@ choices and starts the game again, so progress since your last save is lost. | | Invert mouse vertical | Off, On | Off | | | Invert camera X | Off, On | Off | | | Invert camera Y | Off, On | Off | +| | Stick dead zone | Default (24%), 5%, 10%, 15%, 20%, 30% | Default (24%) | | | Rumble strength | Off, 25% to 150%, in steps of 25 | 100% | | | Motion controls | Off, On | On | | | Motion sensitivity X | 10% to 300% | 100% | @@ -293,6 +294,8 @@ choices and starts the game again, so progress since your last save is lost. mouse still need the game's window. - **Mouse camera:** Native turns the camera with the mouse one to one, as in a PC game. As the stick uses the mouse in place of the right stick. The - and = keys change the sensitivity in game too. +- **Stick dead zone:** how far a stick moves before it counts, on each axis. Default is XInput's (about 24%); lower suits + a precise stick, higher one that drifts. The game keeps a dead zone of its own after it. - **Rumble strength** scales the controller's rumble: 100% is the game's own, Off turns it off. The game's HD rumble goes to a controller's heavy and light motors by its frequency. - **Motion controls:** the controller's gyroscope aims where the game uses motion, as on the Switch. It works with diff --git a/tests/run_all.py b/tests/run_all.py index 3aee3872..2ec0e832 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -46,6 +46,7 @@ "tests/test_taau_env_lookup.py": ["--cxx", "g++"], "tests/test_target_overlap.py": ["--cxx", "g++"], "tests/test_menu_hit.py": ["--cxx", "g++"], + "tests/test_stick_dead_zone.py": ["--cc", "gcc", "--cxx", "g++"], "tests/test_menu_restart.py": ["--cxx", "g++", "--cc", "gcc"], "tests/test_output_resolution.py": ["--cxx", "{cxx}"], "tests/test_widescreen_shapes.py": ["--cc", "gcc"], diff --git a/tests/test_stick_dead_zone.py b/tests/test_stick_dead_zone.py new file mode 100644 index 00000000..0b1c22ee --- /dev/null +++ b/tests/test_stick_dead_zone.py @@ -0,0 +1,162 @@ +"""The sticks' dead zone is the player's: the Insert menu's "Stick dead zone" row (stick_dead_zone in +hookshot_options.ini), issue 752. + +Both pad paths, SDL (host/pad_sdl.c) and XInput (host/nn_hid.c), cut each axis at XInput's 7849 (~24%), a lot on a +worn or a precise stick. They now cut through one function, host/nn_hid.c's hos_stick_axis, whose threshold is the +row's (option 11): Default (7849, the same output as before for every input), 5%, 10%, 15%, 20%, 30%. +The function, pad_sdl.c's stick_value and the menu's row, label and ini parsing are compiled alone (gcc, g++) and +checked over every int16 input. + +python -I -B tests/test_stick_dead_zone.py [--cc gcc] [--cxx g++] +""" +import argparse +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +THRESHOLDS = [7849, 1638, 3277, 4915, 6553, 9830] # (Default, then 5% ... 30% of 32767) + + +def block(text, marker): + start = text.index(marker) + end = text.index("{", start) + 1 + depth = 1 + while depth: + depth += (text[end] == "{") - (text[end] == "}") + end += 1 + return text[start:end] + + +def check(cond, what): + if not cond: + raise SystemExit("FAIL: " + what) + + +def run(compiler, src, name, tmp, flags): + path = Path(tmp) / name + path.write_text(src, encoding="utf-8") + exe = Path(tmp) / (name + ".exe") + subprocess.run([compiler, *flags, "-Wall", "-Werror", "-o", str(exe), str(path)], check=True) + r = subprocess.run([str(exe)], capture_output=True, text=True) + sys.stdout.write(r.stdout) + check(r.returncode == 0, name + ": " + r.stderr.strip()) + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--cc", default="gcc") + p.add_argument("--cxx", default="g++") + a = p.parse_args() + hid = (ROOT / "host" / "nn_hid.c").read_text(encoding="utf-8") + sdl = (ROOT / "host" / "pad_sdl.c").read_text(encoding="utf-8") + api = (ROOT / "gpu" / "vk" / "vk_api.h").read_text(encoding="utf-8") + replay = (ROOT / "gpu" / "vk" / "vk_replay.cpp").read_text(encoding="utf-8") + menu = (ROOT / "gpu" / "vk" / "vk_menu.cpp").read_text(encoding="utf-8") + header = (ROOT / "gpu" / "vk" / "vk_menu.h").read_text(encoding="utf-8") + readme = (ROOT / "readme.md").read_text(encoding="utf-8") + + check(not re.search(r"HOS_OPT_\w+ = 11\b", api), "a HOS_OPT_ enum also takes option 11") + check("int32_t hos_stick_axis(" in hid, "nn_hid.c has no hos_stick_axis") + pads = block(hid, "static void pads_read(") + check("rx_option(11, 0)" in pads and "hos_stick_axis(thumb[i], zone)" in pads, + "XInput's sticks do not go through hos_stick_axis with the row's zone (option 11)") + check("7849" not in pads, "XInput's sticks still cut at 7849 themselves") + poll = block(sdl, "static void pads_poll") + check("rx_option(11, 0)" in poll, "SDL's sticks do not read the row (option 11)") + check(poll.count("stick_value(") == 4, "SDL's four axes do not all go through stick_value") + value = block(sdl, "static int32_t stick_value(") + check("hos_stick_axis(" in value and "7849" not in value, "pad_sdl.c's stick_value cuts on its own") + option = replay[replay.index("t.option = "):] + option = option[:option.index("};") + 2] + check("if (id == 11) return opts.stick_dead_zone;" in option, "the host cannot see the menu's row (option 11)") + check("int stick_dead_zone = 0;" in header, "the option's default is not 0 (Default)") + row = re.search(r'\{TAB_CONTROLS, "Stick dead zone", &opts\.stick_dead_zone, 0, 5, 1, tx_stick_dead_zone,\s*"[^}]*\},', + menu) + check(row is not None, "the Insert menu has no Stick dead zone row (0 to 5)") + check(row.group(0).rstrip().endswith("nullptr, false, true},"), + "the row is not preset_exempt: changing the dead zone would set the quality preset to Custom") + check("| | Stick dead zone | Default (24%), 5%, 10%, 15%, 20%, 30% | Default (24%) |" in readme, + "readme.md's option table has no Stick dead zone row") + + c = r'''#include +#include +#include +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL:%d %s\n", __LINE__, #x); exit(1); } } while (0) +''' + block(hid, "int32_t hos_stick_axis(") + "\n" + block(sdl, "static int32_t stick_value(") + r''' +/* (before issue 752: nn_hid.c's XInput cut, and pad_sdl.c's, which flips first) */ +static int32_t old_xinput(int16_t t) { const int a = t < -32767 ? -32767 : t; return (a > 7849 || a < -7849) ? a : 0; } +static int32_t old_sdl(int16_t v, int flip) { + int32_t a = v < -32767 ? -32767 : v; + if (flip) a = -a; + return (a > 7849 || a < -7849) ? a : 0; +} +int main(void) { + static const int32_t t[6] = {''' + ", ".join(map(str, THRESHOLDS)) + r'''}; + static const int bad[] = {-1, 6, 99, -2147483647 - 1}; + for (int32_t i = -32768; i <= 32767; i++) { + const int16_t v = (int16_t)i; + CHECK(hos_stick_axis(v, 0) == old_xinput(v)); + for (int k = 0; k < 4; k++) CHECK(hos_stick_axis(v, bad[k]) == old_xinput(v)); /* (out of range: Default) */ + CHECK(stick_value(v, 0, 0) == old_sdl(v, 0) && stick_value(v, 1, 0) == old_sdl(v, 1)); + for (int z = 0; z < 6; z++) { + const int32_t a = i < -32767 ? -32767 : i, want = a > t[z] || a < -t[z] ? a : 0; + CHECK(hos_stick_axis(v, z) == want); + CHECK(stick_value(v, 0, z) == want && stick_value(v, 1, z) == -want); + } + } + for (int z = 0; z < 6; z++) + CHECK(hos_stick_axis(t[z], z) == 0 && hos_stick_axis(t[z] + 1, z) == t[z] + 1 && hos_stick_axis(-t[z], z) == 0 && + hos_stick_axis(-t[z] - 1, z) == -t[z] - 1 && hos_stick_axis(-32768, z) == -32767); + printf("stick: Default is the old cut for all 65536 inputs, both paths; 5 other thresholds exact\n"); + return 0; +} +''' + + init = block(menu, "void menu_init(") + loop = block(init, "for (Row& r : rows) {") + check("ini_key(r)" in loop, "menu_init's options loop no longer reads rows by ini_key") + cxx = r'''#include +#include +#include +#include +#include +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL:%d %s\n", __LINE__, #x); exit(1); } } while (0) +enum MenuTab { TAB_CONTROLS }; +struct Options { int output_res = 0, resolution = 0, upscale = 0, dlss = 0, stick_dead_zone = 0; } opts; +static int output_choice(int v) { return v; } +''' + block(menu, "struct Row {") + ";\n" + block(menu, "const char* tx_stick_dead_zone(") + "\n" + \ + "Row rows[] = {" + row.group(0) + "};\n" + block(menu, "const char* ini_key(") + r''' +static int parse(const char* text) { + opts.stick_dead_zone = 0; + char line[80]; + snprintf(line, sizeof line, "%s", text); + char* eq = strchr(line, '='); + *eq = 0; + ''' + loop + r''' + return opts.stick_dead_zone; +} +int main() { + CHECK(!strcmp(ini_key(rows[0]), "stick_dead_zone")); + CHECK(rows[0].preset_exempt && !rows[0].restart); + CHECK(parse("stick_dead_zone=0\n") == 0 && parse("stick_dead_zone=3\n") == 3 && parse("stick_dead_zone=5\n") == 5); + CHECK(parse("stick_dead_zone=9\n") == 5 && parse("stick_dead_zone=-4\n") == 0 && parse("stick_dead_zone=x\n") == 0); + CHECK(parse("stick_deadzone=3\n") == 0); + static const char* want[] = {"Default (24%)", "5%", "10%", "15%", "20%", "30%"}; + for (int v = 0; v < 6; v++) CHECK(!strcmp(rows[0].text(v), want[v])); + CHECK(!strcmp(rows[0].text(-1), want[0]) && !strcmp(rows[0].text(6), want[0])); + (void)output_choice(0); + printf("menu: stick_dead_zone parses and clamps to 0..5; labels Default (24%%) ... 30%%\n"); + return 0; +} +''' + with tempfile.TemporaryDirectory(prefix="stick-dead-zone-") as tmp: + run(a.cc, c, "stick.c", tmp, ["-std=c11", "-O1"]) + run(a.cxx, cxx, "menu.cpp", tmp, ["-std=c++17"]) + print("ok") + + +if __name__ == "__main__": + main()