Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion gpu/vk/vk_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"};
Expand Down Expand Up @@ -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.",
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions gpu/vk/vk_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions gpu/vk/vk_replay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
15 changes: 11 additions & 4 deletions host/nn_hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 10 additions & 6 deletions host/pad_sdl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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) */
Expand Down
3 changes: 3 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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% |
Expand Down Expand Up @@ -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 <kbd>-</kbd> and <kbd>=</kbd> 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
Expand Down
1 change: 1 addition & 0 deletions tests/run_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
162 changes: 162 additions & 0 deletions tests/test_stick_dead_zone.py
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#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 <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#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()
Loading