mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-13 02:41:02 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
314 lines
16 KiB
C
314 lines
16 KiB
C
// --probe-cfg: totk.ini parsing, the environment override and a missing file (cfg.c). Needs no game files.
|
|
#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 = strrchr(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";
|
|
_putenv_s("TOTK_LOCALE", ""); // an empty variable is how _putenv_s spells "remove" on Windows
|
|
_putenv_s("TOTK_FPS60", "");
|
|
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");
|
|
_putenv_s("TOTK_FPS60", "1");
|
|
CHECK(EQ(cfg_get("TOTK_FPS60"), "1"), "environment overrides the file");
|
|
_putenv_s("TOTK_FPS60", "");
|
|
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";
|
|
_putenv_s("TOTK_MOTION", "");
|
|
_putenv_s("TOTK_VOLUME", "");
|
|
_putenv_s("TOTK_RUMBLE_STRENGTH", "");
|
|
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");
|
|
_putenv_s("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");
|
|
_putenv_s("TOTK_VOLUME", "");
|
|
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"};
|
|
_putenv_s("TOTK_VOLUME", "");
|
|
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");
|
|
_putenv_s("TOTK_VOLUME", "70");
|
|
CHECK(cfg_reset(vol, 1) == 0, "reset: an environment-set key is skipped");
|
|
_putenv_s("TOTK_VOLUME", "");
|
|
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(strrchr(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_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");
|
|
// defaults live in the CfgMeta table: cfg_value and the typed accessors fall back to it
|
|
cfg_load("cfg-probe-missing.ini");
|
|
_putenv_s("TOTK_FPS60", "");
|
|
_putenv_s("TOTK_MOUSE_SENS", "");
|
|
_putenv_s("TOTK_FREE_CORES", "");
|
|
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");
|
|
_putenv_s("TOTK_MOUSE_SENS", "2.5");
|
|
CHECK(cfg_double("TOTK_MOUSE_SENS") == 2.5, "double: the environment value");
|
|
_putenv_s("TOTK_MOUSE_SENS", "fast");
|
|
CHECK(cfg_double("TOTK_MOUSE_SENS") == 1.0, "double: a value that does not parse gives the default");
|
|
_putenv_s("TOTK_MOUSE_SENS", "");
|
|
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");
|
|
_putenv_s("TOTK_FREE_CORES", "0");
|
|
CHECK(cfg_bool("TOTK_FREE_CORES") == 0, "free_cores 0 gives the same as unset");
|
|
_putenv_s("TOTK_FREE_CORES", "1");
|
|
CHECK(cfg_bool("TOTK_FREE_CORES") == 1, "free_cores 1: on");
|
|
_putenv_s("TOTK_FREE_CORES", "");
|
|
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);
|
|
_putenv_s(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, "..")) {
|
|
long lo = strtol(m->values, NULL, 10), hi = strtol(strstr(m->values, "..") + 2, NULL, 10);
|
|
double d = strtod(m->def, NULL);
|
|
ok = d >= (double)lo && d <= (double)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");
|
|
printf("probe-cfg: %s\n", fails ? "FAIL" : "ok");
|
|
return fails ? 1 : 0;
|
|
}
|