mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
This takes back the three commits that added it (cd2f9f1,e60677e,a01910d): its runtime layer and Steam binding, the Co-op tab and what it drew over the game, the launcher's copy of Steam's library file, its settings, tests and docs. The tree is as it was before them, with the bug capture that came after kept: its note on screen no longer makes room for co-op's lines, and dbg_area, which the co-op had brought and the capture uses, stays. The four settings of that co-op join the retired names, so a settings file that still has them logs no unknown key. A steam_api64.dll that the launcher put beside the game is left where it is: nothing loads it any more. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
672 lines
41 KiB
C
672 lines
41 KiB
C
// --probe-cfg: nx2navi.ini parsing, the environment override, a missing file and the settings waiting for a restart
|
|
// (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"
|
|
#include "vk_dlssg.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) {
|
|
// cfg_pending, before this probe's files replace the settings cfg_init read: a restart-only setting counts while its
|
|
// value differs from the one at start, a live one never
|
|
if (!cfg_from_env("NX2NAVI_MICROPHONE") && !cfg_from_env("NX2NAVI_VOLUME")) {
|
|
const char *mic = cfg_value("NX2NAVI_MICROPHONE");
|
|
int base = cfg_pending_count();
|
|
CHECK(!cfg_pending("NX2NAVI_MICROPHONE"), "pending: unchanged since the start");
|
|
cfg_set("NX2NAVI_MICROPHONE", strcmp(mic, "0") ? "0" : "1");
|
|
CHECK(cfg_pending("NX2NAVI_MICROPHONE") && cfg_pending_count() == base + 1, "pending: a restart-only setting changed");
|
|
cfg_set("NX2NAVI_MICROPHONE", mic);
|
|
CHECK(!cfg_pending("NX2NAVI_MICROPHONE") && cfg_pending_count() == base, "pending: set back to its start value");
|
|
cfg_set("NX2NAVI_VOLUME", "7");
|
|
CHECK(!cfg_pending("NX2NAVI_VOLUME") && cfg_pending_count() == base, "pending: a live setting never counts");
|
|
// a value the environment sets after the start (the command line's --monitor arrives that way) never waits:
|
|
// no row can change it and a restart keeps it
|
|
if (!cfg_from_env("NX2NAVI_MONITOR")) {
|
|
cfg_setenv("NX2NAVI_MONITOR", "2");
|
|
CHECK(!cfg_pending("NX2NAVI_MONITOR") && cfg_pending_count() == base, "pending: set by the environment after the start");
|
|
cfg_setenv("NX2NAVI_MONITOR", NULL);
|
|
}
|
|
}
|
|
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"
|
|
"NX2NAVI_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";
|
|
cfg_setenv("NX2NAVI_LOCALE", NULL);
|
|
cfg_setenv("NX2NAVI_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("NX2NAVI_FPS60"), "0"), "value read, whitespace trimmed, lower-case key");
|
|
CHECK(EQ(cfg_get("NX2NAVI_REALTIME"), "0"), "key without spaces, mixed case");
|
|
CHECK(EQ(cfg_get("NX2NAVI_GPU_NAME"), "My GPU 1"), "quoted value, section ignored");
|
|
CHECK(EQ(cfg_get("NX2NAVI_LOCALE"), "de-DE"), "NX2NAVI_ prefix accepted in the file");
|
|
CHECK(cfg_get("NX2NAVI_LANG") == NULL, "empty value is unset");
|
|
CHECK(EQ(cfg_get("NX2NAVI_TRACE"), "b"), "later line wins");
|
|
CHECK(cfg_get("NX2NAVI_CLOTH_REALTIME") == NULL, "absent key is NULL");
|
|
CHECK(cfg_get("NX2NAVI_comment") == NULL, "comment is not a key");
|
|
CHECK(EQ(cfg_get("NX2NAVI_NO_SUCH_SETTING"), "1"), "unknown key is still readable");
|
|
{
|
|
int n = -1;
|
|
const char *u = cfg_unknown_keys(&n);
|
|
CHECK(n == 2 && EQ(u, "no_such_setting, last"), "the keys that are no settings: listed lower-case, in the file's order");
|
|
}
|
|
cfg_setenv("NX2NAVI_FPS60", "1");
|
|
CHECK(EQ(cfg_get("NX2NAVI_FPS60"), "1"), "environment overrides the file");
|
|
cfg_setenv("NX2NAVI_FPS60", NULL);
|
|
CHECK(EQ(cfg_get("NX2NAVI_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("NX2NAVI_FPS60") == NULL, "missing file leaves no keys");
|
|
{
|
|
int n = -1;
|
|
CHECK(EQ(cfg_unknown_keys(&n), "") && n == 0, "a missing file leaves no unknown keys either");
|
|
}
|
|
|
|
// A player's file as a report showed it: settings the menu wrote, and three lines added by hand that are no
|
|
// settings of this version (the player took the first for the fix of a crash). The three are named as such, a
|
|
// repeated one once, a key of other characters is counted without being listed, and the menu's save keeps every
|
|
// line it does not change byte for byte, the three included: only the changed, the reset and the new key move.
|
|
#define PLAYER_HEAD \
|
|
"[display]\n" \
|
|
"async_compute = 1\n" \
|
|
"fullscreen = 1\n" \
|
|
"window_size = 1920x1080\n" \
|
|
"gpu_index = 0\n" \
|
|
"perf_overlay = fps\n" \
|
|
"vk_present = mailbox\n" \
|
|
"upscaler = bilinear\n" \
|
|
"dlaa = 1\n" \
|
|
"framegen = off\n" \
|
|
"taa = fsr3\n" \
|
|
"hdr = 0\n" \
|
|
"aspect = fit\n" \
|
|
"game_res = 1800\n"
|
|
#define PLAYER_MID \
|
|
"aniso = 2\n" \
|
|
"assume_focus = 1\n" \
|
|
"texture_detail = auto\n" \
|
|
"\n" \
|
|
"[input]\n" \
|
|
"bind_a = key:default, pad:south\n" \
|
|
"bind_b = key:default, pad:east\n" \
|
|
"\n" \
|
|
"[language]\n" \
|
|
"operation_mode = docked\n" \
|
|
"\n" \
|
|
"[performance]\n" \
|
|
"wb_lazy = 0\n" \
|
|
"vram_safe_mode = 1\n" \
|
|
"exception_handler = 1\n" \
|
|
"ASYNC_COMPUTE = 1\n" \
|
|
"f\xC3\xBCr sp\xC3\xA4ter = 1\n" \
|
|
"\n" \
|
|
"[frame rate]\n"
|
|
static const char player[] = PLAYER_HEAD "sharpness = 80\n" PLAYER_MID "fps_limit = 120\n";
|
|
static const char player_saved[] = PLAYER_HEAD PLAYER_MID "fps_limit = 60\n\n[audio]\nvolume = 55\n";
|
|
#undef PLAYER_HEAD
|
|
#undef PLAYER_MID
|
|
cfg_setenv("NX2NAVI_FPS_LIMIT", NULL);
|
|
cfg_setenv("NX2NAVI_VOLUME", NULL);
|
|
cfg_setenv("NX2NAVI_SHARPNESS", NULL);
|
|
cfg_setenv("NX2NAVI_WB_LAZY", NULL);
|
|
CHECK(plat_file_write(path, player, sizeof player - 1), "write the player's ini");
|
|
CHECK(cfg_load(path) == 1, "load the player's ini");
|
|
{
|
|
int n = -1;
|
|
const char *u = cfg_unknown_keys(&n);
|
|
CHECK(n == 4 && EQ(u, "async_compute, vram_safe_mode, exception_handler"),
|
|
"the player's three lines are named as no settings; a repeat counts once, a key of other characters is only counted");
|
|
CHECK(EQ(cfg_get("NX2NAVI_WB_LAZY"), "0") && cfg_is_set("NX2NAVI_WB_LAZY"), "wb_lazy = 0 is a setting, and set");
|
|
CHECK(!cfg_is_set("NX2NAVI_VK_NAN_PRESERVE"), "a key the file lacks is not set");
|
|
static const char *const reset[] = {"SHARPNESS"};
|
|
cfg_set("NX2NAVI_FPS_LIMIT", "60");
|
|
cfg_set("NX2NAVI_VOLUME", "55");
|
|
CHECK(cfg_reset(reset, 1) == 1 && cfg_save_to(path) == 1, "the menu changes, adds and resets a key and saves");
|
|
uint64_t pn = 0;
|
|
uint8_t *p = plat_file_read(path, &pn);
|
|
CHECK(p && pn == sizeof player_saved - 1 && !memcmp(p, player_saved, pn),
|
|
"the save keeps every other line byte for byte, the lines that are no settings included");
|
|
if (p && (pn != sizeof player_saved - 1 || memcmp(p, player_saved, pn))) fprintf(stderr, "---- saved:\n%s\n----\n", p);
|
|
free(p);
|
|
}
|
|
plat_file_delete(path);
|
|
CHECK(cfg_load(path) == 0, "the player's ini is gone again");
|
|
|
|
// 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";
|
|
cfg_setenv("NX2NAVI_MOTION", NULL);
|
|
cfg_setenv("NX2NAVI_VOLUME", NULL);
|
|
cfg_setenv("NX2NAVI_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("NX2NAVI_FPS60", "0");
|
|
cfg_set("NX2NAVI_MOTION", "0");
|
|
cfg_set("NX2NAVI_RUMBLE_STRENGTH", "80");
|
|
cfg_set("NX2NAVI_VOLUME", "40");
|
|
CHECK(cfg_version() == v0 + 4, "cfg_set bumps the version");
|
|
CHECK(EQ(cfg_get("NX2NAVI_FPS60"), "0") && EQ(cfg_get("NX2NAVI_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("NX2NAVI_FPS60"), "0") && EQ(cfg_get("NX2NAVI_MOTION"), "0") &&
|
|
EQ(cfg_get("NX2NAVI_RUMBLE_STRENGTH"), "80") && EQ(cfg_get("NX2NAVI_VOLUME"), "40") &&
|
|
EQ(cfg_get("NX2NAVI_ROMFS_MODS"), "C:\\mods"), "the saved file reads back");
|
|
const char *held = cfg_get("NX2NAVI_VOLUME");
|
|
cfg_setenv("NX2NAVI_VOLUME", "70");
|
|
cfg_set("NX2NAVI_VOLUME", "90");
|
|
CHECK(cfg_from_env("NX2NAVI_VOLUME") && EQ(cfg_get("NX2NAVI_VOLUME"), "70"), "environment wins over cfg_set");
|
|
CHECK(EQ(held, "40"), "a pointer cfg_get handed out stays valid after cfg_set");
|
|
cfg_setenv("NX2NAVI_VOLUME", NULL);
|
|
CHECK(!cfg_from_env("NX2NAVI_VOLUME") && EQ(cfg_get("NX2NAVI_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);
|
|
// values cfg_load would trim or unquote: mod folder names in the order list start with a space or are quoted
|
|
cfg_set("NX2NAVI_MODS_ORDER", " Lead|Tail ");
|
|
cfg_set("NX2NAVI_MODS_DISABLED", "\"Q\"");
|
|
CHECK(cfg_save_to(path) == 1 && cfg_load(path) == 1 && EQ(cfg_get("NX2NAVI_MODS_ORDER"), " Lead|Tail ") &&
|
|
EQ(cfg_get("NX2NAVI_MODS_DISABLED"), "\"Q\""), "blanks at the ends and quotes survive a save");
|
|
cfg_set("NX2NAVI_MODS_ORDER", "A|B");
|
|
CHECK(cfg_save_to(path) == 1 && cfg_load(path) == 1 && EQ(cfg_get("NX2NAVI_MODS_ORDER"), "A|B"),
|
|
"a plain value replacing a quoted one");
|
|
got = plat_file_read(path, &got_n);
|
|
CHECK(got && strstr((const char *)got, "mods_order = A|B\n") &&
|
|
strstr((const char *)got, "mods_disabled = \"\"Q\"\"\n"),
|
|
"only the values that need it are quoted");
|
|
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("nx2navi_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("MONITOR");
|
|
CHECK(m && EQ(m->def, "auto") && !m->values && !m->live, "meta: monitor is auto by default, free text, read at start");
|
|
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");
|
|
m = cfg_meta("AUDIO_DEVICE");
|
|
CHECK(m && EQ(m->def, "default") && m->values == NULL && !m->live, "meta: audio_device is free text, default, needs a restart");
|
|
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");
|
|
m = cfg_meta("FRAMEGEN_MULTIPLIER");
|
|
CHECK(m && EQ(m->def, "2") && EQ(m->values, "2|3|4") && m->live, "meta: framegen_multiplier is 2|3|4, 2 by default, live");
|
|
|
|
// DLSS frame generation's multiplier (vk_dlssg.c): the setting clamped to what the GPU and driver allow, with the
|
|
// reason vk_dlssg_frame logs when it is not taken as it is
|
|
const char *why = "";
|
|
CHECK(vk_dlssg_multiplier_of("3", 4, &why) == 3 && !why, "multiplier: 3 where 4x runs");
|
|
CHECK(vk_dlssg_multiplier_of("4x", 4, &why) == 4 && !why, "multiplier: 4x reads as 4");
|
|
CHECK(vk_dlssg_multiplier_of("4", 2, &why) == 2 && why, "multiplier: 4 where 2x is the most is 2, with a reason");
|
|
CHECK(vk_dlssg_multiplier_of("4", 3, &why) == 3 && why, "multiplier: 4 where 3x is the most is 3, with a reason");
|
|
CHECK(vk_dlssg_multiplier_of("9", 0, &why) == 4 && why, "multiplier: above 4 is 4, with a reason");
|
|
CHECK(vk_dlssg_multiplier_of("1", 4, &why) == 2 && why, "multiplier: below 2 is 2, with a reason");
|
|
CHECK(vk_dlssg_multiplier_of("fast", 4, &why) == 2 && why, "multiplier: not a number is 2, with a reason");
|
|
CHECK(vk_dlssg_multiplier_of("3y", 4, &why) == 2 && why, "multiplier: a number with other text after it is 2");
|
|
CHECK(vk_dlssg_multiplier_of("", 3, &why) == 2 && !why && vk_dlssg_multiplier_of(NULL, 3, NULL) == 2,
|
|
"multiplier: unset is 2");
|
|
CHECK(vk_dlssg_multiplier_of("4", 0, &why) == 4 && !why, "multiplier: kept as it is while the GPU's limit is not known");
|
|
CHECK(vk_dlssg_multiplier_max(NULL) == 0 && vk_dlssg_multiplier() == 1, "multiplier: 1, no limit, while DLSS-G does not run");
|
|
|
|
// cfg_reset: the key's active lines leave nx2navi.ini, the commented example stays
|
|
static const char rbase[] = "[audio]\n# volume = 100\n[input]\nmotion = 0\n";
|
|
static const char *const vol[] = {"VOLUME"};
|
|
cfg_setenv("NX2NAVI_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("NX2NAVI_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("NX2NAVI_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("NX2NAVI_VOLUME", "55");
|
|
CHECK(cfg_reset(vol, 1) == 1 && cfg_get("NX2NAVI_VOLUME") == NULL, "reset: a value set after a save resets too");
|
|
cfg_set("NX2NAVI_VOLUME", "60");
|
|
CHECK(cfg_save_to(path) == 1 && EQ(cfg_get("NX2NAVI_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("NX2NAVI_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("NX2NAVI_VOLUME", "60");
|
|
CHECK(cfg_save_to(path) == 1, "reset: volume 60 again for the environment case");
|
|
|
|
// a file saved as UTF-16 (little and big endian, with a line ending of two characters) reads as the same lines
|
|
static const char wtext[] = "[audio]\r\nvolume = 35\r\n[input]\r\nmotion = 0\r\n";
|
|
for (int be = 0; be < 2; be++) {
|
|
uint8_t wbuf[2 + 2 * sizeof wtext];
|
|
size_t wn = 0;
|
|
wbuf[wn++] = be ? 0xFE : 0xFF, wbuf[wn++] = be ? 0xFF : 0xFE;
|
|
for (size_t i = 0; i + 1 < sizeof wtext; i++)
|
|
wbuf[wn++] = be ? 0 : (uint8_t)wtext[i], wbuf[wn++] = be ? (uint8_t)wtext[i] : 0;
|
|
CHECK(plat_file_write("cfg-probe-utf16.ini", wbuf, wn) && cfg_load("cfg-probe-utf16.ini") == 1 &&
|
|
EQ(cfg_get("NX2NAVI_VOLUME"), "35") && EQ(cfg_get("NX2NAVI_MOTION"), "0"),
|
|
be ? "an ini saved as UTF-16, big endian, is not read" : "an ini saved as UTF-16, little endian, is not read");
|
|
}
|
|
plat_file_delete("cfg-probe-utf16.ini");
|
|
|
|
// a key the environment sets is skipped and keeps its file line
|
|
CHECK(cfg_load(path) == 1 && EQ(cfg_get("NX2NAVI_VOLUME"), "60"), "reset: reload");
|
|
cfg_setenv("NX2NAVI_VOLUME", "70");
|
|
CHECK(cfg_reset(vol, 1) == 0, "reset: an environment-set key is skipped");
|
|
cfg_setenv("NX2NAVI_VOLUME", NULL);
|
|
CHECK(EQ(cfg_get("NX2NAVI_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("NX2NAVI_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("NX2NAVI_VOLUME"), "20"),
|
|
"save status: an unwritable path fails, the value stays");
|
|
cfg_set("NX2NAVI_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("NX2NAVI_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_V] = 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, V stick click, 3 D-pad right");
|
|
memset(dn, 0, sizeof dn);
|
|
dn[KEY_LSHIFT] = 1;
|
|
CHECK(input_map_buttons(dn) == 1ull << B_A, "keys: Left Shift is A (Ocarina dashes on A held)");
|
|
dn[KEY_LSHIFT] = 0, dn[KEY_LCTRL] = dn[KEY_O] = dn[KEY_N] = 1;
|
|
CHECK(input_map_buttons(dn) == (1ull << B_B | 1ull << B_LEFT | 1ull << B_RIGHT),
|
|
"keys: Left Ctrl puts away (B), O the ocarina (D-pad left), N Navi (D-pad right)");
|
|
memset(dn, 0, sizeof dn);
|
|
dn[KEY_G] = 1;
|
|
CHECK(input_map_buttons(dn) == 0 && !input_map_held(dn, B_SHAKE), "keys: G is free (no shake key by default)");
|
|
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, 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, 1, st);
|
|
CHECK(st[0] == 0 && st[1] > 0, "mouse: moving up is stick up");
|
|
input_mouse_stick(30, 0, 1.0 / 30, 1, 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, 1, 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, 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, 1, st);
|
|
CHECK(st[0] == 0 && st[1] == 0, "mouse: sensitivity 0 is off");
|
|
input_mouse_stick(30, 0, 1.0 / 60, 1, 1.5f, st);
|
|
CHECK(st[0] == 13107 && st[1] == 0, "mouse: a camera speed of 1.5 divides the deflection, so the turn per count stays");
|
|
input_mouse_stick(1000, 0, 1.0 / 60, 1, 1.5f, st);
|
|
CHECK(st[0] == 32767 && st[1] == 0, "mouse: a fast flick reaches full tilt at a camera speed of 1.5 too");
|
|
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");
|
|
cfg_setenv("NX2NAVI_STICK_DEADZONE", "12");
|
|
cfg_setenv("NX2NAVI_LSTICK_DEADZONE", NULL);
|
|
cfg_setenv("NX2NAVI_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");
|
|
cfg_setenv("NX2NAVI_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");
|
|
cfg_setenv("NX2NAVI_LSTICK_DEADZONE", "80");
|
|
CHECK(fabsf(input_pad_deadzone(0) - 0.5f) < 1e-6f, "pad dead zone: clamped to 50%");
|
|
cfg_setenv("NX2NAVI_STICK_DEADZONE", NULL);
|
|
cfg_setenv("NX2NAVI_LSTICK_DEADZONE", NULL);
|
|
cfg_setenv("NX2NAVI_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)");
|
|
CHECK(input_pad_layout_pick("PlayStation", 1) == PAD_LAYOUT_XBOX && input_pad_layout_pick("playstation", 0) == PAD_LAYOUT_XBOX,
|
|
"pad layout: playstation plays by label as xbox does (cross A), whatever the pad's labels");
|
|
// 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");
|
|
CHECK(input_prompts_pick("PlayStation", "nintendo") == PROMPTS_PLAYSTATION &&
|
|
input_prompts_pick("playstation", "xbox_prompts") == PROMPTS_PLAYSTATION &&
|
|
input_prompts_pick("ps", "nintendo") == PROMPTS_NINTENDO,
|
|
"prompts: playstation in any case; an unknown value is the game's own");
|
|
CHECK(input_prompts_pick("SteamDeck", "nintendo") == PROMPTS_STEAMDECK &&
|
|
input_prompts_pick("steamdeck", "xbox_prompts") == PROMPTS_STEAMDECK &&
|
|
EQ(input_prompts_folder(PROMPTS_STEAMDECK, "nintendo"), "romfs-patches-steamdeck") &&
|
|
EQ(input_prompts_folder(PROMPTS_STEAMDECK, "xbox"), "romfs-patches-steamdeck-label") &&
|
|
EQ(input_prompts_folder(PROMPTS_STEAMDECK, "auto"), "romfs-patches-steamdeck-label") &&
|
|
EQ(input_prompts_folder(PROMPTS_STEAMDECK, NULL), "romfs-patches-steamdeck-label"),
|
|
"prompts: steamdeck in any case, its face buttons by position for the Nintendo layout, by label otherwise");
|
|
CHECK(EQ(input_prompts_folder(PROMPTS_PLAYSTATION, "nintendo"), "romfs-patches-playstation") &&
|
|
EQ(input_prompts_folder(PROMPTS_PLAYSTATION, "xbox_prompts"), "romfs-patches-playstation") &&
|
|
EQ(input_prompts_folder(PROMPTS_PLAYSTATION, "xbox"), "romfs-patches-playstation-label") &&
|
|
EQ(input_prompts_folder(PROMPTS_PLAYSTATION, "auto"), "romfs-patches-playstation-label") &&
|
|
EQ(input_prompts_folder(PROMPTS_PLAYSTATION, NULL), "romfs-patches-playstation-label"),
|
|
"prompts: the PlayStation face buttons by position for the Nintendo layout, by label for xbox and auto");
|
|
// romfs-patches-xbox shows A as B and romfs-patches-playstation A as circle (tools/pad_prompts.py FACE). That is
|
|
// right exactly where the layout puts A on the east button, which an Xbox pad labels B and a PlayStation pad circle.
|
|
input_map_reset();
|
|
static const char *const layouts[] = {"nintendo", "xbox", "playstation", "auto", "xbox_prompts"};
|
|
for (int i = 0; i < 5; 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");
|
|
CHECK(a_east == EQ(input_prompts_folder(PROMPTS_PLAYSTATION, layouts[i]), "romfs-patches-playstation"),
|
|
"prompts: the folder names A as circle exactly when the layout puts A on the circle button");
|
|
}
|
|
// The default layout is the Switch's own buttons by position, whatever the pad's labels (an Xbox pad's east button
|
|
// is labelled B and does Switch A): A right, B bottom, X top, Y left, L/R the bumpers, ZL/ZR the triggers.
|
|
const CfgMeta *pl = cfg_meta("PAD_LAYOUT");
|
|
CHECK(pl && input_pad_layout_pick(pl->def, 0) == PAD_LAYOUT_NINTENDO, "pad layout: the default is Nintendo, also for an Xbox pad");
|
|
static const struct { int pad, button; } sw[] = {{PAD_EAST, B_A}, {PAD_SOUTH, B_B}, {PAD_NORTH, B_X}, {PAD_WEST, B_Y},
|
|
{PAD_L, B_L}, {PAD_R, B_R}, {PAD_ZL, B_ZL}, {PAD_ZR, B_ZR},
|
|
{PAD_START, B_PLUS}, {PAD_BACK, B_MINUS}, {PAD_LSTICK, B_STICK_L},
|
|
{PAD_RSTICK, B_STICK_R}, {PAD_DPAD_UP, B_UP}, {PAD_DPAD_DOWN, B_DOWN},
|
|
{PAD_DPAD_LEFT, B_LEFT}, {PAD_DPAD_RIGHT, B_RIGHT}};
|
|
int sw_ok = 1;
|
|
for (size_t i = 0; i < sizeof sw / sizeof *sw; i++) {
|
|
uint8_t one[PAD_COUNT] = {0};
|
|
one[sw[i].pad] = 1;
|
|
sw_ok &= input_map_pad_buttons(one, PAD_LAYOUT_NINTENDO) == 1ull << sw[i].button;
|
|
}
|
|
CHECK(sw_ok, "pad layout: Nintendo puts every Switch button at its position on the Switch");
|
|
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, key:lshift, pad:default"), "edit: A's line is key:f, key:lshift (its second key), 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");
|
|
cfg_setenv("NX2NAVI_FPS60", NULL);
|
|
cfg_setenv("NX2NAVI_MOUSE_SENS", NULL);
|
|
cfg_setenv("NX2NAVI_FREE_CORES", NULL);
|
|
CHECK(cfg_bool("NX2NAVI_FPS60") == 1, "default: unset FPS60 is on");
|
|
CHECK(EQ(cfg_value("NX2NAVI_VK_PRESENT"), "mailbox") && cfg_int("NX2NAVI_SPLIT_DRAWS") == 64, "default: text and integer");
|
|
cfg_setenv("NX2NAVI_MOUSE_SENS", "2.5");
|
|
CHECK(cfg_double("NX2NAVI_MOUSE_SENS") == 2.5, "double: the environment value");
|
|
cfg_setenv("NX2NAVI_MOUSE_SENS", "fast");
|
|
CHECK(cfg_double("NX2NAVI_MOUSE_SENS") == 1.0, "double: a value that does not parse gives the default");
|
|
cfg_setenv("NX2NAVI_MOUSE_SENS", NULL);
|
|
cfg_set("NX2NAVI_MOUSE_SENS", "3");
|
|
CHECK(cfg_double("NX2NAVI_MOUSE_SENS") == 3.0 && cfg_int("NX2NAVI_MOUSE_SENS") == 3, "a cfg_set value beats the default");
|
|
CHECK(cfg_bool("NX2NAVI_FREE_CORES") == 0, "free_cores unset: off");
|
|
cfg_setenv("NX2NAVI_FREE_CORES", "0");
|
|
CHECK(cfg_bool("NX2NAVI_FREE_CORES") == 0, "free_cores 0 gives the same as unset");
|
|
cfg_setenv("NX2NAVI_FREE_CORES", "1");
|
|
CHECK(cfg_bool("NX2NAVI_FREE_CORES") == 1, "free_cores 1: on");
|
|
cfg_setenv("NX2NAVI_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
|
|
cfg_setenv("NX2NAVI_PAD_LAYOUT", NULL);
|
|
cfg_setenv("NX2NAVI_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("NX2NAVI_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("NX2NAVI_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("NX2NAVI_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("NX2NAVI_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, "NX2NAVI_%s", m->key);
|
|
cfg_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;
|
|
}
|