Files
totk-pc-recomp/runtime/cfg_probe.c
T
Wintego cb628b08b7 Add keyboard and mouse button prompts (prompts = pc)
The Button prompts row gets a third choice, PC: every button glyph of the game's button font and every image of the
button guide is drawn as the key or mouse button the keyboard defaults press it with (E, Shift, Space, left click, Q,
R, right click, F, Tab, M, 1 to 4, W A S D, Ctrl, C) and the right stick as the mouse, whatever the pad layout. The
setup writes romfs-patches-pc next to the Xbox folders (make_romfs_patches.py --prompts pc, button_icons.py pc_icons);
--binds <totk.ini> draws the keys of its bind_ lines instead. At a game resolution of another shape the game takes
romfs-patches-pc-<w>x<h> (--prompts pc --aspect) in place of romfs-patches-<w>x<h> when it is there.

(cherry picked from commit 3f81cf02be664100bd9c569c53588d620d90b527)
2026-11-04 08:36:28 +03:00

476 lines
28 KiB
C

// --probe-cfg: totk.ini parsing, the environment override and a missing file (cfg.c). Needs no game files.
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "input.h"
#include "input_map.h"
#include "platform/platform.h"
static int fails;
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL cfg: %s\n", what); } } while (0)
#define EQ(a, b) ((a) && !strcmp((a), (b)))
int probe_cfg(void) {
char path[1024];
if (!plat_exe_path(path, sizeof path)) return 1;
char *cut = plat_last_sep(path);
if (!cut) return 1;
strcpy(cut + 1, "cfg-probe.ini");
static const char ini[] =
"\xEF\xBB\xBF# comment\r\n"
"; another comment\r\n"
"[frame rate]\r\n"
" fps60 = 0 \r\n"
"Realtime=0\r\n"
"[Display]\r\n"
"GPU_name = \"My GPU 1\"\r\n"
"TOTK_Locale = de-DE\r\n"
"lang =\r\n"
"trace = a\r\n"
"trace = b\r\n"
"no_such_setting = 1\r\n"
"no equals sign\r\n"
"last = no newline at the end";
plat_setenv("TOTK_LOCALE", NULL);
plat_setenv("TOTK_FPS60", NULL);
CHECK(plat_file_write(path, ini, sizeof ini - 1), "write the probe ini");
CHECK(cfg_load(path) == 1, "load the probe ini");
CHECK(EQ(cfg_get("TOTK_FPS60"), "0"), "value read, whitespace trimmed, lower-case key");
CHECK(EQ(cfg_get("TOTK_REALTIME"), "0"), "key without spaces, mixed case");
CHECK(EQ(cfg_get("TOTK_GPU_NAME"), "My GPU 1"), "quoted value, section ignored");
CHECK(EQ(cfg_get("TOTK_LOCALE"), "de-DE"), "TOTK_ prefix accepted in the file");
CHECK(cfg_get("TOTK_LANG") == NULL, "empty value is unset");
CHECK(EQ(cfg_get("TOTK_TRACE"), "b"), "later line wins");
CHECK(cfg_get("TOTK_CLOTH_REALTIME") == NULL, "absent key is NULL");
CHECK(cfg_get("TOTK_comment") == NULL, "comment is not a key");
CHECK(EQ(cfg_get("TOTK_NO_SUCH_SETTING"), "1"), "unknown key is still readable");
plat_setenv("TOTK_FPS60", "1");
CHECK(EQ(cfg_get("TOTK_FPS60"), "1"), "environment overrides the file");
plat_setenv("TOTK_FPS60", NULL);
CHECK(EQ(cfg_get("TOTK_FPS60"), "0"), "file value returns when the variable is removed");
plat_file_delete(path);
CHECK(cfg_load(path) == 0, "missing file is not an error");
CHECK(cfg_get("TOTK_FPS60") == NULL, "missing file leaves no keys");
// cfg_set / cfg_save: only changed lines move, comments stay, a missing key goes under its section
static const char before[] =
"\xEF\xBB\xBF# my settings\r\n"
"[frame rate]\r\n"
" FPS60 = 1\r\n"
"; keep this\r\n"
"\r\n"
"[input]\r\n"
"# Gamepad motion controls (0 = off) and vibration (0 = off).\r\n"
"# motion = 1\r\n"
"# rumble = 1\r\n"
"rumble_curve = 0.6\r\n"
"\r\n"
"[files]\r\n"
"romfs_mods = C:\\mods";
static const char after[] =
"\xEF\xBB\xBF# my settings\r\n"
"[frame rate]\r\n"
" FPS60 = 0\r\n"
"; keep this\r\n"
"\r\n"
"[input]\r\n"
"# Gamepad motion controls (0 = off) and vibration (0 = off).\r\n"
"# motion = 1\r\n"
"motion = 0\r\n"
"# rumble = 1\r\n"
"rumble_curve = 0.6\r\n"
"rumble_strength = 80\r\n"
"\r\n"
"[files]\r\n"
"romfs_mods = C:\\mods\r\n"
"\r\n"
"[audio]\r\n"
"volume = 40\r\n";
plat_setenv("TOTK_MOTION", NULL);
plat_setenv("TOTK_VOLUME", NULL);
plat_setenv("TOTK_RUMBLE_STRENGTH", NULL);
CHECK(plat_file_write(path, before, sizeof before - 1), "write the save ini");
CHECK(cfg_load(path) == 1, "load the save ini");
unsigned v0 = cfg_version();
cfg_set("TOTK_FPS60", "0");
cfg_set("TOTK_MOTION", "0");
cfg_set("TOTK_RUMBLE_STRENGTH", "80");
cfg_set("TOTK_VOLUME", "40");
CHECK(cfg_version() == v0 + 4, "cfg_set bumps the version");
CHECK(EQ(cfg_get("TOTK_FPS60"), "0") && EQ(cfg_get("TOTK_VOLUME"), "40"), "cfg_set values are read back at once");
CHECK(cfg_save_to(path) == 1, "cfg_save_to writes");
uint64_t got_n = 0;
uint8_t *got = plat_file_read(path, &got_n);
CHECK(got && got_n == sizeof after - 1 && !memcmp(got, after, got_n),
"saved file: changed line replaced, comments kept, missing keys under their sections");
if (got && (got_n != sizeof after - 1 || memcmp(got, after, got_n))) fprintf(stderr, "---- saved:\n%s\n----\n", got);
free(got);
CHECK(cfg_save_to(path) == 1, "a save with nothing changed");
got = plat_file_read(path, &got_n);
CHECK(got && got_n == sizeof after - 1 && !memcmp(got, after, got_n), "a save with nothing changed keeps the file");
free(got);
CHECK(cfg_load(path) == 1 && EQ(cfg_get("TOTK_FPS60"), "0") && EQ(cfg_get("TOTK_MOTION"), "0") &&
EQ(cfg_get("TOTK_RUMBLE_STRENGTH"), "80") && EQ(cfg_get("TOTK_VOLUME"), "40") &&
EQ(cfg_get("TOTK_ROMFS_MODS"), "C:\\mods"), "the saved file reads back");
const char *held = cfg_get("TOTK_VOLUME");
plat_setenv("TOTK_VOLUME", "70");
cfg_set("TOTK_VOLUME", "90");
CHECK(cfg_from_env("TOTK_VOLUME") && EQ(cfg_get("TOTK_VOLUME"), "70"), "environment wins over cfg_set");
CHECK(EQ(held, "40"), "a pointer cfg_get handed out stays valid after cfg_set");
plat_setenv("TOTK_VOLUME", NULL);
CHECK(!cfg_from_env("TOTK_VOLUME") && EQ(cfg_get("TOTK_VOLUME"), "90"), "cfg_set value returns without the variable");
plat_file_delete(path);
CHECK(cfg_save_to(path) == 1, "cfg_save_to creates a missing file");
got = plat_file_read(path, &got_n);
CHECK(got && !strcmp((const char *)got, "[audio]\nvolume = 90\n"), "new file: the section and the key");
free(got);
// Defaults table: the menu's "default" marks and Reset
const CfgMeta *m = cfg_meta("FPS60");
CHECK(m && EQ(m->def, "1") && m->live, "meta: fps60 defaults to 1 and is live");
m = cfg_meta("ASYNC_PREEMPT");
CHECK(m && EQ(m->values, "0|1|2") && !m->live, "meta: async_preempt takes 0|1|2 and needs a restart");
m = cfg_meta("totk_vk_present");
CHECK(m && EQ(m->key, "VK_PRESENT") && EQ(m->values, "mailbox|fifo|immediate") && m->live,
"meta: prefix and case are accepted");
m = cfg_meta("RUMBLE_STRENGTH");
CHECK(m && EQ(m->def, "130") && EQ(m->values, "0..300"), "meta: rumble strength 130 in 0..300");
m = cfg_meta("PERF_OVERLAY");
CHECK(m && EQ(m->def, "off") && EQ(m->values, "off|fps|full") && m->live, "meta: perf_overlay is live, off by default");
CHECK(cfg_meta("BIND_A") == NULL && cfg_meta("FPS_COUNTER") == NULL && cfg_meta("NO_SUCH") == NULL,
"meta: keys the menu does not show are NULL");
// cfg_reset: the key's active lines leave totk.ini, the commented example stays
static const char rbase[] = "[audio]\n# volume = 100\n[input]\nmotion = 0\n";
static const char *const vol[] = {"VOLUME"};
plat_setenv("TOTK_VOLUME", NULL);
CHECK(plat_file_write(path, rbase, sizeof rbase - 1), "write the reset ini");
CHECK(cfg_load(path) == 1, "load the reset ini");
cfg_set("TOTK_VOLUME", "40");
CHECK(cfg_save_to(path) == 1 && cfg_last_save_ok(), "reset: volume saved");
got = plat_file_read(path, &got_n);
CHECK(got && strstr((const char *)got, "\nvolume = 40\n"), "reset: the active line is in the file");
free(got);
unsigned v1 = cfg_version();
CHECK(cfg_reset(vol, 1) == 1, "reset: one key reset");
CHECK(cfg_version() > v1 && cfg_get("TOTK_VOLUME") == NULL, "reset: the value is gone in memory, the version moved");
CHECK(cfg_reset(vol, 1) == 0, "reset: a key already reset counts 0");
CHECK(cfg_save_to(path) == 1, "reset: save");
got = plat_file_read(path, &got_n);
CHECK(got && got_n == sizeof rbase - 1 && !memcmp(got, rbase, got_n) && strstr((const char *)got, "# volume = 100"),
"reset: no active volume line, the commented example and the other keys stay");
if (got && (got_n != sizeof rbase - 1 || memcmp(got, rbase, got_n))) fprintf(stderr, "---- saved:\n%s\n----\n", got);
free(got);
cfg_set("TOTK_VOLUME", "55");
CHECK(cfg_reset(vol, 1) == 1 && cfg_get("TOTK_VOLUME") == NULL, "reset: a value set after a save resets too");
cfg_set("TOTK_VOLUME", "60");
CHECK(cfg_save_to(path) == 1 && EQ(cfg_get("TOTK_VOLUME"), "60"), "reset then set before the save: the set wins");
got = plat_file_read(path, &got_n);
CHECK(got && strstr((const char *)got, "\nvolume = 60\n"), "reset then set: the new line is written");
free(got);
// an active line with an empty value has no entry in memory; Reset still removes it from the file
static const char rempty[] = "[audio]\n# volume = 100\nvolume =\n[input]\nmotion = 0\n";
CHECK(plat_file_write(path, rempty, sizeof rempty - 1) && cfg_load(path) == 1 && cfg_get("TOTK_VOLUME") == NULL,
"reset: load an ini with an empty value");
CHECK(cfg_reset(vol, 1) == 0 && cfg_save_to(path) == 1, "reset: an empty-valued key saves");
got = plat_file_read(path, &got_n);
CHECK(got && got_n == sizeof rbase - 1 && !memcmp(got, rbase, got_n), "reset: the empty-valued line is gone");
free(got);
cfg_set("TOTK_VOLUME", "60");
CHECK(cfg_save_to(path) == 1, "reset: volume 60 again for the environment case");
// a key the environment sets is skipped and keeps its file line
CHECK(cfg_load(path) == 1 && EQ(cfg_get("TOTK_VOLUME"), "60"), "reset: reload");
plat_setenv("TOTK_VOLUME", "70");
CHECK(cfg_reset(vol, 1) == 0, "reset: an environment-set key is skipped");
plat_setenv("TOTK_VOLUME", NULL);
CHECK(EQ(cfg_get("TOTK_VOLUME"), "60"), "reset: the skipped key keeps its value");
CHECK(cfg_save_to(path) == 1, "reset: save after a skip");
got = plat_file_read(path, &got_n);
CHECK(got && strstr((const char *)got, "\nvolume = 60\n"), "reset: the skipped key keeps its file line");
free(got);
// a failed save: the call fails, cfg_last_save_ok says so, the value stays set and a later save works
CHECK(cfg_last_save_ok() == 1, "save status: ok after a good save");
cfg_set("TOTK_VOLUME", "20");
char bad[1100];
snprintf(bad, sizeof bad, "%s", path);
strcpy(plat_last_sep(bad) + 1, "no-such-dir/cfg-probe.ini");
CHECK(cfg_save_to(bad) == 0 && cfg_last_save_ok() == 0 && EQ(cfg_get("TOTK_VOLUME"), "20"),
"save status: an unwritable path fails, the value stays");
cfg_set("TOTK_VOLUME", "21");
CHECK(plat_file_write(path, rbase, sizeof rbase - 1), "write the read-only ini");
CHECK(plat_file_set_readonly(path, 1), "set the file read-only");
CHECK(cfg_save_to(path) == 0 && cfg_last_save_ok() == 0 && EQ(cfg_get("TOTK_VOLUME"), "21"),
"save status: a read-only file fails, cfg_get returns the new value");
plat_file_set_readonly(path, 0);
CHECK(cfg_save_to(path) == 1 && cfg_last_save_ok() == 1, "save status: the retry after the failure works");
got = plat_file_read(path, &got_n);
CHECK(got && strstr((const char *)got, "\nvolume = 21\n"), "save status: the retried save wrote the value");
free(got);
plat_file_delete(path);
cfg_load(path);
CHECK(rumble_motor(0, 0.6f, 1.3f) == 0 && rumble_motor(0.5f, 0.6f, 0) == 0, "rumble: zero amplitude or strength is silent");
CHECK(rumble_motor(1, 0.6f, 3) == 65535 && rumble_motor(5, 1, 1) == 65535, "rumble: saturates at 65535");
CHECK(rumble_motor(0.25f, 1, 1) > 16000 && rumble_motor(0.25f, 1, 1) < 16800, "rumble: curve 1, strength 100% is linear");
CHECK(rumble_motor(0.1f, 0.6f, 1.3f) > rumble_motor(0.1f, 1, 1.3f), "rumble: curve lifts weak values");
CHECK(rumble_motor(0.3f, 0.6f, 1.3f) > rumble_motor(0.3f, 0.6f, 1), "rumble: strength scales");
// Keyboard and mouse bindings (input_map.c)
uint8_t dn[KEY_COUNT] = {0};
int32_t st[2];
CHECK(input_map_buttons(dn) == 0, "keys: nothing held, no buttons");
dn[KEY_MOUSE_L] = 1;
CHECK(input_map_buttons(dn) == 1ull << B_Y, "keys: left mouse button is Y");
dn[KEY_MOUSE_R] = dn[KEY_Z] = 1;
CHECK(input_map_buttons(dn) == (1ull << B_Y | 1ull << B_ZL), "keys: right mouse button and Z both press ZL");
memset(dn, 0, sizeof dn);
dn[KEY_X] = dn[KEY_TAB] = dn[KEY_M] = dn[KEY_LCTRL] = dn[KEY_3] = 1;
CHECK(input_map_buttons(dn) == (1ull << B_ZR | 1ull << B_PLUS | 1ull << B_MINUS | 1ull << B_STICK_L | 1ull << B_RIGHT),
"keys: X ZR, Tab Plus, M Minus, Left Ctrl stick click, 3 D-pad right");
memset(dn, 0, sizeof dn);
dn[KEY_G] = 1;
CHECK(input_map_buttons(dn) == 0 && input_map_held(dn, B_SHAKE) && !input_map_held(dn, B_A),
"keys: G shakes and presses no button");
memset(dn, 0, sizeof dn);
dn[KEY_W] = 1;
input_map_stick(dn, 0, st);
CHECK(st[0] == 0 && st[1] == 32767, "keys: W is left stick up");
dn[KEY_A] = 1;
input_map_stick(dn, 0, st);
CHECK(st[0] == -23170 && st[1] == 23170, "keys: W A is a diagonal on the unit circle");
dn[KEY_W] = dn[KEY_A] = dn[KEY_S] = 1;
dn[KEY_D] = 1;
input_map_stick(dn, 0, st);
CHECK(st[0] == 0 && st[1] == 0, "keys: opposite keys cancel");
memset(dn, 0, sizeof dn);
dn[KEY_LEFT] = 1;
input_map_stick(dn, 1, st);
CHECK(st[0] == -32767 && st[1] == 0, "keys: left arrow is right stick left");
input_map_stick(dn, 0, st);
CHECK(st[0] == 0 && st[1] == 0, "keys: arrows leave the left stick alone");
input_mouse_stick(30, 0, 1.0 / 60, 1, st);
CHECK(st[0] == 32767 * 1800 / 3000 && st[1] == 0, "mouse: 30 counts in 1/60 s is 60% right");
input_mouse_stick(0, -10, 1.0 / 60, 1, st);
CHECK(st[0] == 0 && st[1] > 0, "mouse: moving up is stick up");
input_mouse_stick(30, 0, 1.0 / 30, 1, st);
CHECK(st[0] == 32767 * 900 / 3000, "mouse: the same counts over twice the time are half the deflection");
input_mouse_stick(15, 0, 1.0 / 60, 2, st);
CHECK(st[0] == 32767 * 1800 / 3000, "mouse: doubling sensitivity halves the counts needed (15 counts at 2 = 30 at 1)");
input_mouse_stick(1000, 1000, 1.0 / 60, 1, st);
CHECK((int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768, "mouse: clamped to the unit circle");
input_mouse_stick(30, 0, 1.0 / 60, 0, st);
CHECK(st[0] == 0 && st[1] == 0, "mouse: sensitivity 0 is off");
input_pad_stick(3000, -2000, 0.15f, st);
CHECK(st[0] == 0 && st[1] == 0, "pad stick: an 11% drift inside a 15% dead zone reads centred");
input_pad_stick(4000, -3000, 0, st);
CHECK(st[0] == 4000 && st[1] == -3000, "pad stick: dead zone 0 passes the stick unchanged");
input_pad_stick(32767, 0, 0.15f, st);
CHECK(st[0] == 32767 && st[1] == 0, "pad stick: full deflection still reaches full");
input_pad_stick(-32768, -32768, 0.15f, st);
CHECK(st[0] < 0 && st[1] < 0 && st[0] >= -32767 && (int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768,
"pad stick: a square gate's corner stays on the unit circle");
input_pad_stick(0, 32767 / 2 + 1, 0.2f, st);
CHECK(abs(st[1] - 32767 * 3 / 8) <= 3, "pad stick: half deflection past a 20% zone is (0.5 - 0.2) / 0.8 of full");
plat_setenv("TOTK_STICK_DEADZONE", "12");
plat_setenv("TOTK_LSTICK_DEADZONE", NULL);
plat_setenv("TOTK_RSTICK_DEADZONE", NULL);
CHECK(fabsf(input_pad_deadzone(0) - 0.12f) < 1e-6f && fabsf(input_pad_deadzone(1) - 0.12f) < 1e-6f,
"pad dead zone: both sticks follow stick_deadzone while lstick/rstick are unset");
plat_setenv("TOTK_RSTICK_DEADZONE", "20");
CHECK(fabsf(input_pad_deadzone(0) - 0.12f) < 1e-6f && fabsf(input_pad_deadzone(1) - 0.20f) < 1e-6f,
"pad dead zone: rstick_deadzone sets the right stick alone");
plat_setenv("TOTK_LSTICK_DEADZONE", "80");
CHECK(fabsf(input_pad_deadzone(0) - 0.5f) < 1e-6f, "pad dead zone: clamped to 50%");
plat_setenv("TOTK_STICK_DEADZONE", NULL);
plat_setenv("TOTK_LSTICK_DEADZONE", NULL);
plat_setenv("TOTK_RSTICK_DEADZONE", NULL);
CHECK(input_stick_percent(32767, 0) > 99.99 && input_stick_percent(32767, 0) < 100.01 &&
input_stick_percent(0, 0) == 0 && fabs(input_stick_percent(3000, -4000) - 5000 * 100.0 / 32767) < 1e-9,
"stick percent: an axis's end is 100%, the centre 0%, round in between");
// Gamepad layouts (input_map.c)
CHECK(input_pad_layout_pick("auto", 1) == PAD_LAYOUT_NINTENDO && input_pad_layout_pick(NULL, 0) == PAD_LAYOUT_XBOX,
"pad layout: automatic is Nintendo for an east button labelled A, Xbox otherwise");
CHECK(input_pad_layout_pick("Xbox", 1) == PAD_LAYOUT_XBOX && input_pad_layout_pick("nintendo", 0) == PAD_LAYOUT_NINTENDO,
"pad layout: a set layout wins over the labels");
CHECK(input_pad_layout_pick("xbox_prompts", 0) == PAD_LAYOUT_NINTENDO &&
input_pad_layout_pick("Xbox_Prompts", 1) == PAD_LAYOUT_NINTENDO,
"pad layout: 0.1.9's xbox_prompts keeps the Nintendo positions (only the prompts change, romfs.c overlay_init)");
// Button prompts (input_map.c)
CHECK(input_prompts_pick(NULL, "auto") == PROMPTS_NINTENDO && input_prompts_pick("", "xbox") == PROMPTS_NINTENDO &&
input_prompts_pick("Xbox", "nintendo") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox") == PROMPTS_NINTENDO,
"prompts: the game's own unless set to Xbox");
CHECK(input_prompts_pick("PC", "nintendo") == PROMPTS_PC && input_prompts_pick("pc", "xbox_prompts") == PROMPTS_PC &&
EQ(input_prompts_folder(PROMPTS_PC, "nintendo"), "romfs-patches-pc") &&
EQ(input_prompts_folder(PROMPTS_PC, "xbox"), "romfs-patches-pc") &&
EQ(input_prompts_folder(PROMPTS_PC, NULL), "romfs-patches-pc"),
"prompts: pc shows the keyboard and mouse whatever the pad layout");
CHECK(input_prompts_pick(NULL, "xbox_prompts") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox_prompts") == PROMPTS_NINTENDO,
"prompts: 0.1.9's pad_layout = xbox_prompts means Xbox while prompts is unset");
CHECK(!input_prompts_folder(PROMPTS_NINTENDO, "nintendo") && !input_prompts_folder(PROMPTS_NINTENDO, "xbox"),
"prompts: the game's own need no folder");
CHECK(EQ(input_prompts_folder(PROMPTS_XBOX, "nintendo"), "romfs-patches-xbox") &&
EQ(input_prompts_folder(PROMPTS_XBOX, "xbox_prompts"), "romfs-patches-xbox") &&
EQ(input_prompts_folder(PROMPTS_XBOX, "xbox"), "romfs-patches-xbox-label") &&
EQ(input_prompts_folder(PROMPTS_XBOX, "auto"), "romfs-patches-xbox-label") &&
EQ(input_prompts_folder(PROMPTS_XBOX, NULL), "romfs-patches-xbox-label"),
"prompts: the face buttons by position for the Nintendo layout, by label for xbox and auto");
// romfs-patches-xbox shows A as B (tests/romfs_prompts_test.py checks the folders' contents). It is right exactly
// where the layout puts A on the east button, which an Xbox pad labels B.
input_map_reset();
static const char *const layouts[] = {"nintendo", "xbox", "auto", "xbox_prompts"};
for (int i = 0; i < 4; i++) {
int a_east = input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_EAST] = 1}, input_pad_layout_pick(layouts[i], 0)) == 1ull << B_A;
CHECK(a_east == EQ(input_prompts_folder(PROMPTS_XBOX, layouts[i]), "romfs-patches-xbox"),
"prompts: the folder names A as B exactly when the layout puts A on the button labelled B");
}
uint8_t pd[PAD_COUNT] = {0};
pd[PAD_SOUTH] = pd[PAD_NORTH] = 1;
CHECK(input_map_pad_buttons(pd, PAD_LAYOUT_NINTENDO) == (1ull << B_B | 1ull << B_X) &&
input_map_pad_buttons(pd, PAD_LAYOUT_XBOX) == (1ull << B_A | 1ull << B_Y),
"pad layout: south and north are B and X by position, A and Y on Xbox");
pd[PAD_SOUTH] = pd[PAD_NORTH] = 0;
pd[PAD_START] = pd[PAD_ZR] = 1;
CHECK(input_map_pad_buttons(pd, PAD_LAYOUT_XBOX) == (1ull << B_PLUS | 1ull << B_ZR), "pad layout: Xbox keeps the other buttons");
CHECK(input_map_bind("A", "pad:west") && input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_WEST] = 1}, PAD_LAYOUT_XBOX) ==
(1ull << B_A | 1ull << B_X),
"pad layout: a bind line adds its physical button in every layout");
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_SOUTH] = 1}, PAD_LAYOUT_XBOX) == 0,
"pad layout: a bind line replaces the layout's own button");
input_map_reset();
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_XBOX) == 0,
"pad layout: a back paddle is unbound by default");
CHECK(input_map_bind("ZL", "pad:default, pad:rpaddle1") &&
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL &&
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_ZL] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL,
"bind: pad:rpaddle1 adds a back paddle, pad:default keeps the trigger");
input_map_reset();
// Bind lines for stick directions and default parts; the remapping rows' edits (input_map.c)
memset(dn, 0, sizeof dn);
dn[KEY_I] = 1;
CHECK(input_map_bind("LSTICK_UP", "key:i, pad:south") == 1, "bind: a stick direction takes keys, skips pad entries");
input_map_stick(dn, 0, st);
CHECK(st[1] == 32767, "bind: I moves forward after bind_LSTICK_UP = key:i");
dn[KEY_I] = 0, dn[KEY_W] = 1;
input_map_stick(dn, 0, st);
CHECK(st[1] == 0, "bind: W no longer moves forward");
CHECK(input_map_bind("A", "key:default, pad:north") == 1, "bind: key:default with a pad entry");
memset(dn, 0, sizeof dn);
dn[KEY_E] = 1;
CHECK(input_map_buttons(dn) == 1ull << B_A &&
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_NORTH] = 1}, PAD_LAYOUT_XBOX) == (1ull << B_A | 1ull << B_Y),
"bind: key:default keeps E, pad:north adds the top button in every layout");
CHECK(input_map_bind("B", "key:j, pad:default") == 1 &&
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_EAST] = 1}, PAD_LAYOUT_XBOX) == 1ull << B_B &&
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_SOUTH] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_B,
"bind: pad:default keeps each layout's own button");
input_map_reset();
InputMap mp = input_map;
char line[256];
input_map_line(&mp, B_ZR, line, sizeof line);
CHECK(!line[0], "edit: an action at its defaults needs no line");
CHECK(input_map_set_key(&mp, B_A, 0, KEY_F) == B_ZR, "edit: F taken from ZR");
input_map_line(&mp, B_A, line, sizeof line);
CHECK(!strcmp(line, "key:f, pad:default"), "edit: A's line is key:f, pad:default");
input_map_line(&mp, B_ZR, line, sizeof line);
CHECK(!strcmp(line, "key:x, pad:default"), "edit: ZR keeps X");
CHECK(input_map_set_key(&mp, B_ZL, 1, KEY_NONE) == -1, "edit: clearing a key takes from no one");
uint8_t ks[8];
CHECK(input_map_keys_of(&mp, B_ZL, ks, 8) == 1 && ks[0] == KEY_MOUSE_R, "edit: ZL keeps its first key only");
mp = input_map;
CHECK(input_map_set_pad(&mp, B_X, PAD_LAYOUT_XBOX, PAD_SOUTH) == B_A, "edit: south swaps X with A (Xbox layout)");
uint8_t ps[8];
CHECK(input_map_pads_of(&mp, B_A, PAD_LAYOUT_XBOX, ps, 8) == 1 && ps[0] == PAD_WEST, "edit: A takes X's west button");
CHECK(input_map_pads_of(&mp, B_A, PAD_LAYOUT_NINTENDO, ps, 8) == 1 && ps[0] == PAD_WEST &&
input_map_pads_of(&mp, B_B, PAD_LAYOUT_NINTENDO, ps, 8) == 1 && ps[0] == PAD_EAST,
"edit: the face buttons are pinned in every layout after a gamepad edit");
input_map_line(&mp, B_X, line, sizeof line);
CHECK(!strcmp(line, "key:default, pad:south"), "edit: X's line is key:default, pad:south");
input_map_set_pad(&mp, B_X, PAD_LAYOUT_XBOX, PAD_WEST); // and back
input_map_line(&mp, B_A, line, sizeof line);
CHECK(!line[0] && input_map_pads_of(&mp, B_A, PAD_LAYOUT_NINTENDO, ps, 8) == 1 && ps[0] == PAD_EAST,
"edit: swapping back restores every layout's defaults");
mp = input_map;
input_map_set_key(&mp, B_SL_UP, 0, KEY_NONE);
input_map_line(&mp, B_SL_UP, line, sizeof line);
CHECK(!strcmp(line, "none"), "edit: a stick direction without keys is none");
// defaults live in the CfgMeta table: cfg_value and the typed accessors fall back to it
cfg_load("cfg-probe-missing.ini");
plat_setenv("TOTK_FPS60", NULL);
plat_setenv("TOTK_MOUSE_SENS", NULL);
plat_setenv("TOTK_FREE_CORES", NULL);
CHECK(cfg_bool("TOTK_FPS60") == 1, "default: unset FPS60 is on");
CHECK(EQ(cfg_value("TOTK_VK_PRESENT"), "mailbox") && cfg_int("TOTK_SPLIT_DRAWS") == 64, "default: text and integer");
plat_setenv("TOTK_MOUSE_SENS", "2.5");
CHECK(cfg_double("TOTK_MOUSE_SENS") == 2.5, "double: the environment value");
plat_setenv("TOTK_MOUSE_SENS", "fast");
CHECK(cfg_double("TOTK_MOUSE_SENS") == 1.0, "double: a value that does not parse gives the default");
plat_setenv("TOTK_MOUSE_SENS", NULL);
cfg_set("TOTK_MOUSE_SENS", "3");
CHECK(cfg_double("TOTK_MOUSE_SENS") == 3.0 && cfg_int("TOTK_MOUSE_SENS") == 3, "a cfg_set value beats the default");
CHECK(cfg_bool("TOTK_FREE_CORES") == 0, "free_cores unset: off");
plat_setenv("TOTK_FREE_CORES", "0");
CHECK(cfg_bool("TOTK_FREE_CORES") == 0, "free_cores 0 gives the same as unset");
plat_setenv("TOTK_FREE_CORES", "1");
CHECK(cfg_bool("TOTK_FREE_CORES") == 1, "free_cores 1: on");
plat_setenv("TOTK_FREE_CORES", NULL);
// 0.1.9's pad_layout = xbox_prompts (cfg.c migrate): read as the Nintendo layout with the Xbox prompts, saved so
plat_setenv("TOTK_PAD_LAYOUT", NULL);
plat_setenv("TOTK_PROMPTS", NULL);
static const char old_layout[] = "[input]\r\npad_layout = xbox_prompts\r\n";
CHECK(plat_file_write(path, old_layout, sizeof old_layout - 1) && cfg_load(path) == 1 &&
EQ(cfg_get("TOTK_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("TOTK_PROMPTS"), "xbox"),
"0.1.9's pad_layout = xbox_prompts reads as nintendo with prompts = xbox");
CHECK(cfg_save_to(path) == 1, "the read pad layout and prompts save");
uint64_t saved_n = 0;
char *saved = (char *)plat_file_read(path, &saved_n);
CHECK(saved && strstr(saved, "pad_layout = nintendo") && strstr(saved, "prompts = xbox") && !strstr(saved, "xbox_prompts"),
"the next save writes the pad layout and the prompts");
free(saved);
static const char old_layout_own[] = "[input]\r\npad_layout = xbox_prompts\r\nprompts = nintendo\r\n";
CHECK(plat_file_write(path, old_layout_own, sizeof old_layout_own - 1) && cfg_load(path) == 1 &&
EQ(cfg_get("TOTK_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("TOTK_PROMPTS"), "nintendo"),
"a prompts line of the file's own wins over 0.1.9's pad_layout = xbox_prompts");
plat_file_delete(path);
cfg_load("cfg-probe-missing.ini");
int listed = 0;
for (int i = 0; cfg_meta_at(i); i++) {
const CfgMeta *m = cfg_meta_at(i);
if (!*m->def) continue;
listed++;
char name[96];
snprintf(name, sizeof name, "TOTK_%s", m->key);
plat_setenv(name, "");
CHECK(EQ(cfg_value(name), m->def), "cfg_value returns the table default");
int ok = 0;
if (!m->values) ok = 1;
else if (strstr(m->values, "..")) {
// strtod: ranges such as 0.2..0.9 have fractional ends
double lo = strtod(m->values, NULL), hi = strtod(strstr(m->values, "..") + 2, NULL);
double d = strtod(m->def, NULL);
ok = d >= lo && d <= hi;
} else {
size_t dl = strlen(m->def);
for (const char *v = m->values; *v;) {
size_t n = strcspn(v, "|");
ok |= n == dl && !strncmp(v, m->def, n);
v += n + (v[n] == '|');
}
}
if (!ok) fprintf(stderr, "FAIL cfg: default '%s' of %s is not one of its values '%s'\n", m->def, m->key, m->values), fails++;
}
CHECK(listed >= 30, "the defaults table was walked");
// cfg_meta answers with the first row of a key: a second row would be dead, and Reset would visit the key twice
for (int i = 0; cfg_meta_at(i); i++)
for (int j = i + 1; cfg_meta_at(j); j++)
if (!strcmp(cfg_meta_at(i)->key, cfg_meta_at(j)->key))
fprintf(stderr, "FAIL cfg: %s has two rows in the defaults table\n", cfg_meta_at(i)->key), fails++;
printf("probe-cfg: %s\n", fails ? "FAIL" : "ok");
return fails ? 1 : 0;
}