// --probe-cfg: totk.ini parsing, the environment override and a missing file (cfg.c). Needs no game files. #include #include #include #include #include #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_T] = 1; CHECK(input_map_buttons(dn) == 1ull << B_Y, "keys: T is Y too (the left button is A in the menus)"); plat_setenv("TOTK_BIND_X", "key:t"); input_map_configure(); CHECK(input_map_buttons(dn) == 1ull << B_X, "bind: bind_X = key:t takes T from Y's defaults"); dn[KEY_T] = 0, dn[KEY_MOUSE_L] = 1; CHECK(input_map_buttons(dn) == 1ull << B_Y, "bind: Y keeps the left mouse button"); plat_setenv("TOTK_BIND_X", NULL); input_map_reset(); 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; }