Adding support for a configuration file
This commit is contained in:
@@ -0,0 +1,613 @@
|
||||
// Persistent, human-editable JSON config file - see qs_config.h and
|
||||
// CONFIG.md (repo root) for the documented schema and behavior. This file
|
||||
// deliberately touches engine internals directly (gShockPrefs, mode_id,
|
||||
// change_svga_screen_mode(), message_info()) - unlike xr_session.c/
|
||||
// xr_input.c/xr_swapchain.c, which stay purely XR/SDL-facing and never
|
||||
// reach into engine state this way. It's still compiled into the same
|
||||
// target as the rest of the engine (see android/app/build.gradle's
|
||||
// ANDROID_EXTRA_SOURCES, same as those files), so engine headers resolve
|
||||
// via the same include_directories() every engine .c file already uses.
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include "qs_config.h"
|
||||
|
||||
#include "Prefs.h"
|
||||
#include "fullscrn.h"
|
||||
#include "lg.h"
|
||||
#include "tools.h"
|
||||
|
||||
#define QS_CONFIG_FILENAME "questshock.config.json"
|
||||
|
||||
// ===========================================================================
|
||||
// Minimal JSON parser
|
||||
//
|
||||
// Just enough to read this file's own schema (nested one level: group ->
|
||||
// string-valued option) while safely skipping over anything it doesn't
|
||||
// recognize - unknown keys, wrong-typed values, arrays, nested objects - so
|
||||
// a hand-edited file with extra/future content never breaks parsing of the
|
||||
// parts this version does care about. No third-party JSON library exists
|
||||
// anywhere in this repo; this matches the project's own existing style of a
|
||||
// small hand-rolled parser for its persisted files (see Prefs.c's prefs.txt/
|
||||
// keybinds.txt parsing).
|
||||
//
|
||||
// `error` is sticky: once any function below sets it, every other parser
|
||||
// function becomes a no-op returning false, so a syntax error anywhere
|
||||
// cleanly aborts the whole parse rather than continuing from a
|
||||
// cursor position left in an indeterminate spot by the failure.
|
||||
// ===========================================================================
|
||||
|
||||
typedef struct {
|
||||
const char *p;
|
||||
const char *end;
|
||||
bool error;
|
||||
} QsJsonParser;
|
||||
|
||||
static void qs_json_fail(QsJsonParser *p) { p->error = true; }
|
||||
|
||||
static void qs_json_skip_ws(QsJsonParser *p) {
|
||||
while (!p->error && p->p < p->end && isspace((unsigned char)*p->p))
|
||||
p->p++;
|
||||
}
|
||||
|
||||
static bool qs_json_expect(QsJsonParser *p, char c) {
|
||||
if (p->error || p->p >= p->end || *p->p != c) {
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
p->p++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parses (and unescapes \" \\ \/ \n \t \r \b \f) a JSON string literal at
|
||||
// the cursor into out, NUL-terminated, truncated (not treated as an error)
|
||||
// if longer than out_size - 1 bytes. out may be NULL (e.g. when skipping a
|
||||
// string value we don't care about) - the bytes are simply discarded.
|
||||
// \uXXXX escapes aren't supported (not needed for anything this file writes
|
||||
// or a realistic hand edit) and fail the parse like any other malformed
|
||||
// input.
|
||||
static bool qs_json_parse_string(QsJsonParser *p, char *out, size_t out_size) {
|
||||
if (p->error || !qs_json_expect(p, '"'))
|
||||
return false;
|
||||
size_t n = 0;
|
||||
while (p->p < p->end && *p->p != '"') {
|
||||
char c = *p->p;
|
||||
if (c == '\\') {
|
||||
p->p++;
|
||||
if (p->p >= p->end) {
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
switch (*p->p) {
|
||||
case '"':
|
||||
c = '"';
|
||||
break;
|
||||
case '\\':
|
||||
c = '\\';
|
||||
break;
|
||||
case '/':
|
||||
c = '/';
|
||||
break;
|
||||
case 'n':
|
||||
c = '\n';
|
||||
break;
|
||||
case 't':
|
||||
c = '\t';
|
||||
break;
|
||||
case 'r':
|
||||
c = '\r';
|
||||
break;
|
||||
case 'b':
|
||||
c = '\b';
|
||||
break;
|
||||
case 'f':
|
||||
c = '\f';
|
||||
break;
|
||||
default:
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (out && n + 1 < out_size)
|
||||
out[n++] = c;
|
||||
p->p++;
|
||||
}
|
||||
if (p->p >= p->end) {
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
p->p++; // closing quote
|
||||
if (out)
|
||||
out[n] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool qs_json_skip_number(QsJsonParser *p) {
|
||||
const char *start = p->p;
|
||||
if (p->p < p->end && *p->p == '-')
|
||||
p->p++;
|
||||
while (p->p < p->end && (isdigit((unsigned char)*p->p) || *p->p == '.' || *p->p == 'e' ||
|
||||
*p->p == 'E' || *p->p == '+' || *p->p == '-'))
|
||||
p->p++;
|
||||
if (p->p == start) {
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool qs_json_skip_literal(QsJsonParser *p, const char *literal) {
|
||||
size_t len = strlen(literal);
|
||||
if ((size_t)(p->end - p->p) < len || strncmp(p->p, literal, len) != 0) {
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
p->p += len;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool qs_json_skip_value(QsJsonParser *p);
|
||||
|
||||
typedef void (*QsJsonFieldCallback)(const char *key, QsJsonParser *p, void *ctx);
|
||||
|
||||
// Parses a JSON object (cursor at '{'). For each "key": value pair, calls
|
||||
// field_cb with the cursor positioned right after the colon - field_cb must
|
||||
// fully consume that value itself, one way or another (see
|
||||
// qs_json_take_string_field() below), since this function only ever
|
||||
// advances past the key + colon on its own.
|
||||
static bool qs_json_parse_object(QsJsonParser *p, QsJsonFieldCallback field_cb, void *ctx) {
|
||||
if (p->error || !qs_json_expect(p, '{'))
|
||||
return false;
|
||||
qs_json_skip_ws(p);
|
||||
if (!p->error && p->p < p->end && *p->p == '}') {
|
||||
p->p++;
|
||||
return true;
|
||||
}
|
||||
while (!p->error) {
|
||||
char key[64];
|
||||
if (!qs_json_parse_string(p, key, sizeof(key)))
|
||||
return false;
|
||||
qs_json_skip_ws(p);
|
||||
if (!qs_json_expect(p, ':'))
|
||||
return false;
|
||||
qs_json_skip_ws(p);
|
||||
field_cb(key, p, ctx);
|
||||
qs_json_skip_ws(p);
|
||||
if (p->error || p->p >= p->end) {
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
if (*p->p == ',') {
|
||||
p->p++;
|
||||
qs_json_skip_ws(p);
|
||||
continue;
|
||||
}
|
||||
if (*p->p == '}') {
|
||||
p->p++;
|
||||
return true;
|
||||
}
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool qs_json_skip_array(QsJsonParser *p) {
|
||||
if (p->error || !qs_json_expect(p, '['))
|
||||
return false;
|
||||
qs_json_skip_ws(p);
|
||||
if (!p->error && p->p < p->end && *p->p == ']') {
|
||||
p->p++;
|
||||
return true;
|
||||
}
|
||||
while (!p->error) {
|
||||
if (!qs_json_skip_value(p))
|
||||
return false;
|
||||
qs_json_skip_ws(p);
|
||||
if (p->error || p->p >= p->end) {
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
if (*p->p == ',') {
|
||||
p->p++;
|
||||
qs_json_skip_ws(p);
|
||||
continue;
|
||||
}
|
||||
if (*p->p == ']') {
|
||||
p->p++;
|
||||
return true;
|
||||
}
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// field_cb for qs_json_parse_object() when it's only being used to skip a
|
||||
// nested object we don't recognize - every field's value is itself skipped.
|
||||
static void qs_json_skip_field(const char *key, QsJsonParser *p, void *ctx) {
|
||||
(void)key;
|
||||
(void)ctx;
|
||||
qs_json_skip_value(p);
|
||||
}
|
||||
|
||||
static bool qs_json_skip_value(QsJsonParser *p) {
|
||||
if (p->error)
|
||||
return false;
|
||||
qs_json_skip_ws(p);
|
||||
if (p->p >= p->end) {
|
||||
qs_json_fail(p);
|
||||
return false;
|
||||
}
|
||||
switch (*p->p) {
|
||||
case '"':
|
||||
return qs_json_parse_string(p, NULL, 0);
|
||||
case '{':
|
||||
return qs_json_parse_object(p, qs_json_skip_field, NULL);
|
||||
case '[':
|
||||
return qs_json_skip_array(p);
|
||||
case 't':
|
||||
return qs_json_skip_literal(p, "true");
|
||||
case 'f':
|
||||
return qs_json_skip_literal(p, "false");
|
||||
case 'n':
|
||||
return qs_json_skip_literal(p, "null");
|
||||
default:
|
||||
return qs_json_skip_number(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Tries to parse the value at the cursor as a string into out (truncating
|
||||
// safely if too long); if it isn't a string at all, skips it instead so the
|
||||
// parse can continue past this field regardless of a wrong-typed value in
|
||||
// the file. Either way, fully consumes exactly one value.
|
||||
static void qs_json_take_string_field(QsJsonParser *p, char *out, size_t out_size) {
|
||||
if (!p->error && p->p < p->end && *p->p == '"')
|
||||
qs_json_parse_string(p, out, out_size);
|
||||
else
|
||||
qs_json_skip_value(p);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Schema: "video_options" group
|
||||
// ===========================================================================
|
||||
|
||||
typedef struct {
|
||||
char renderer[16]; // "" = not present in the file
|
||||
char resolution[16]; // "" = not present in the file
|
||||
} QsParsedVideoOptions;
|
||||
|
||||
typedef struct {
|
||||
QsParsedVideoOptions video_options;
|
||||
bool has_video_options;
|
||||
} QsParsedConfig;
|
||||
|
||||
static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void *ctx) {
|
||||
QsParsedVideoOptions *out = (QsParsedVideoOptions *)ctx;
|
||||
if (strcmp(key, "renderer") == 0)
|
||||
qs_json_take_string_field(p, out->renderer, sizeof(out->renderer));
|
||||
else if (strcmp(key, "resolution") == 0)
|
||||
qs_json_take_string_field(p, out->resolution, sizeof(out->resolution));
|
||||
else
|
||||
qs_json_skip_value(p);
|
||||
}
|
||||
|
||||
static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx) {
|
||||
QsParsedConfig *out = (QsParsedConfig *)ctx;
|
||||
if (strcmp(key, "video_options") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
||||
out->has_video_options = qs_json_parse_object(p, qs_parse_video_options_field, &out->video_options);
|
||||
else
|
||||
qs_json_skip_value(p);
|
||||
}
|
||||
|
||||
// Reads and parses QS_CONFIG_FILENAME into *out. Returns false (with *out
|
||||
// zeroed) if the file doesn't exist, is empty, or fails to parse as valid
|
||||
// JSON - logging a WARN only for the last case, since a missing file is the
|
||||
// normal/expected state, not an error.
|
||||
static bool qs_config_read_file(QsParsedConfig *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
FILE *f = fopen(QS_CONFIG_FILENAME, "rb");
|
||||
if (!f)
|
||||
return false;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (size <= 0) {
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
char *buf = (char *)malloc((size_t)size);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
size_t got = fread(buf, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
|
||||
QsJsonParser parser = {buf, buf + got, false};
|
||||
qs_json_skip_ws(&parser);
|
||||
bool ok = qs_json_parse_object(&parser, qs_parse_top_level_field, out) && !parser.error;
|
||||
free(buf);
|
||||
if (!ok) {
|
||||
WARN("%s: malformed JSON - ignoring file", QS_CONFIG_FILENAME);
|
||||
memset(out, 0, sizeof(*out));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Video Options: renderer
|
||||
// ===========================================================================
|
||||
|
||||
// Already proven live-safe by the existing Ctrl+G renderer-toggle hotkey
|
||||
// (toggle_opengl(), OpenGL.cc), which does nothing more than flip this same
|
||||
// field - every render-mode check elsewhere in the engine reads
|
||||
// gShockPrefs.doUseOpenGL directly, every frame, so no extra "apply" step is
|
||||
// needed beyond setting it.
|
||||
#define QS_RENDERER_REQUIRES_RESTART false
|
||||
|
||||
static bool qs_apply_renderer(const char *value, bool live) {
|
||||
(void)live;
|
||||
bool want_opengl;
|
||||
if (strcmp(value, "opengl") == 0) {
|
||||
want_opengl = true;
|
||||
} else if (strcmp(value, "software") == 0) {
|
||||
want_opengl = false;
|
||||
} else {
|
||||
WARN("%s: video_options.renderer=\"%s\" is invalid (expected \"software\" or \"opengl\") - ignoring",
|
||||
QS_CONFIG_FILENAME, value);
|
||||
return false;
|
||||
}
|
||||
bool changed = gShockPrefs.doUseOpenGL != want_opengl;
|
||||
gShockPrefs.doUseOpenGL = want_opengl;
|
||||
return changed;
|
||||
}
|
||||
|
||||
static const char *qs_current_renderer(void) { return gShockPrefs.doUseOpenGL ? "opengl" : "software"; }
|
||||
|
||||
// ===========================================================================
|
||||
// Video Options: resolution
|
||||
// ===========================================================================
|
||||
|
||||
// Live-appliable: change_svga_screen_mode() (below) handles a real mode_id
|
||||
// change fully, including reallocating the screen buffer - see
|
||||
// qs_apply_resolution()'s own comment for the one extra step that takes
|
||||
// (screen_draw()) that a naive call site doesn't get for free.
|
||||
#define QS_RESOLUTION_REQUIRES_RESTART false
|
||||
|
||||
// mode_id (fullscrn.c) is the index change_svga_screen_mode() actually
|
||||
// reads - matches the same field Prefs.c's own SavePrefs()/SetShockGlobals()
|
||||
// read/write for gShockPrefs.doVideoMode.
|
||||
extern short mode_id;
|
||||
|
||||
// screen.c - declared locally (game_screen.h's other declarations pull in
|
||||
// UI-region types this file has no other reason to need) rather than
|
||||
// #include "game_screen.h", matching how mode_id above is declared.
|
||||
extern errtype screen_draw(void);
|
||||
|
||||
static const struct {
|
||||
const char *name;
|
||||
short mode;
|
||||
} kResolutions[] = {
|
||||
{"320x200", 0}, {"320x400", 1}, {"640x400", 2}, {"640x480", 3}, {"1024x768", 4},
|
||||
};
|
||||
#define QS_NUM_RESOLUTIONS (sizeof(kResolutions) / sizeof(kResolutions[0]))
|
||||
|
||||
static bool qs_apply_resolution(const char *value, bool live) {
|
||||
for (size_t i = 0; i < QS_NUM_RESOLUTIONS; i++) {
|
||||
if (strcmp(value, kResolutions[i].name) != 0)
|
||||
continue;
|
||||
bool changed = mode_id != kResolutions[i].mode;
|
||||
gShockPrefs.doVideoMode = kResolutions[i].mode;
|
||||
mode_id = kResolutions[i].mode;
|
||||
if (changed && live) {
|
||||
change_svga_screen_mode();
|
||||
// change_svga_screen_mode() only reallocates the screen buffer
|
||||
// (gr_alloc_screen()/gr_set_screen(), fullscrn.c) when mode_id
|
||||
// actually differs from its own last-applied value
|
||||
// (convert_use_mode) - true here, since changed is set above.
|
||||
// screen.c's screen_start() (the normal "enter the 3D game
|
||||
// screen" path, and the only other caller that hits this same
|
||||
// branch) always calls screen_draw() immediately afterward to
|
||||
// repaint the background/MFD/side-icon/inventory/status chrome
|
||||
// onto whatever buffer is now current; nothing else does, so a
|
||||
// mid-session mode-index change like this one otherwise leaves
|
||||
// that chrome blank (all still functional/redrawing correctly
|
||||
// frame to frame - automap content, inventory grid values,
|
||||
// etc. - just missing the static art around them) until the
|
||||
// next full mode transition. screen_draw() itself doesn't
|
||||
// assume any particular UI mode is active (automap/inventory/
|
||||
// main view), so it's safe to call regardless of which one the
|
||||
// resolution change happened during.
|
||||
screen_draw();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
WARN("%s: video_options.resolution=\"%s\" is invalid - ignoring", QS_CONFIG_FILENAME, value);
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char *qs_current_resolution(void) {
|
||||
for (size_t i = 0; i < QS_NUM_RESOLUTIONS; i++)
|
||||
if (kResolutions[i].mode == mode_id)
|
||||
return kResolutions[i].name;
|
||||
return "640x480"; // shouldn't happen - safe fallback for Save
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Apply / Save orchestration
|
||||
// ===========================================================================
|
||||
|
||||
static void qs_append_restart_reason(char *buf, size_t buf_size, const char *name) {
|
||||
if (!buf || buf_size == 0)
|
||||
return;
|
||||
size_t len = strlen(buf);
|
||||
if (len > 0 && len + 2 < buf_size) {
|
||||
strcat(buf, ", ");
|
||||
len += 2;
|
||||
}
|
||||
if (len < buf_size - 1)
|
||||
strncat(buf, name, buf_size - len - 1);
|
||||
}
|
||||
|
||||
// Applies every option present in *parsed (empty string = not present in
|
||||
// the file - left untouched). live=false is the startup path (no restart
|
||||
// distinction applies that early - see qs_config_load_startup()); live=true
|
||||
// is the mid-session "Load Configuration" button path, where restart_needed
|
||||
// (may be NULL) collects a human-readable, comma-separated list of every
|
||||
// option that both actually changed and is flagged *_REQUIRES_RESTART.
|
||||
static void qs_config_apply_parsed(const QsParsedConfig *parsed, bool live, char *restart_needed,
|
||||
size_t restart_needed_size) {
|
||||
if (restart_needed && restart_needed_size > 0)
|
||||
restart_needed[0] = '\0';
|
||||
if (!parsed->has_video_options)
|
||||
return;
|
||||
if (parsed->video_options.renderer[0] != '\0') {
|
||||
bool changed = qs_apply_renderer(parsed->video_options.renderer, live);
|
||||
if (changed && QS_RENDERER_REQUIRES_RESTART)
|
||||
qs_append_restart_reason(restart_needed, restart_needed_size, "renderer");
|
||||
}
|
||||
if (parsed->video_options.resolution[0] != '\0') {
|
||||
bool changed = qs_apply_resolution(parsed->video_options.resolution, live);
|
||||
if (changed && QS_RESOLUTION_REQUIRES_RESTART)
|
||||
qs_append_restart_reason(restart_needed, restart_needed_size, "resolution");
|
||||
}
|
||||
}
|
||||
|
||||
static bool qs_config_write_file(void) {
|
||||
FILE *f = fopen(QS_CONFIG_FILENAME, "w");
|
||||
if (!f) {
|
||||
WARN("%s: failed to open for writing", QS_CONFIG_FILENAME);
|
||||
return false;
|
||||
}
|
||||
fprintf(f, "{\n");
|
||||
fprintf(f, " \"video_options\": {\n");
|
||||
fprintf(f, " \"renderer\": \"%s\",\n", qs_current_renderer());
|
||||
fprintf(f, " \"resolution\": \"%s\"\n", qs_current_resolution());
|
||||
fprintf(f, " }\n");
|
||||
fprintf(f, "}\n");
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Native -> Java: restart-required dialog
|
||||
// ===========================================================================
|
||||
|
||||
// Mirrors xr_overlay.c's own xr_overlay_find_class() helper (also file-local
|
||||
// there) - FindClass() from this thread can't see app classes on its own
|
||||
// since this thread was attached to the JVM via SDL_AndroidGetJNIEnv()
|
||||
// rather than spawned from Java; the activity's own ClassLoader can.
|
||||
static jclass qs_find_class(JNIEnv *env, jobject activity, const char *name) {
|
||||
jclass activityClass = (*env)->GetObjectClass(env, activity);
|
||||
jmethodID getClassLoader =
|
||||
(*env)->GetMethodID(env, activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
|
||||
jobject classLoader = (*env)->CallObjectMethod(env, activity, getClassLoader);
|
||||
jclass classLoaderClass = (*env)->FindClass(env, "java/lang/ClassLoader");
|
||||
jmethodID loadClass =
|
||||
(*env)->GetMethodID(env, classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
jstring className = (*env)->NewStringUTF(env, name);
|
||||
jclass result = (jclass)(*env)->CallObjectMethod(env, classLoader, loadClass, className);
|
||||
if ((*env)->ExceptionCheck(env)) {
|
||||
(*env)->ExceptionClear(env);
|
||||
return NULL;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void qs_show_restart_dialog(const char *restart_needed) {
|
||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||
jobject activity = (jobject)SDL_AndroidGetActivity();
|
||||
if (!env || !activity)
|
||||
return;
|
||||
|
||||
jclass menuOverlayClass = qs_find_class(env, activity, "de.ladkau.questshock.MenuOverlay");
|
||||
if (!menuOverlayClass)
|
||||
return;
|
||||
|
||||
jmethodID showRestartDialog =
|
||||
(*env)->GetStaticMethodID(env, menuOverlayClass, "showRestartDialog", "(Ljava/lang/String;)V");
|
||||
if (!showRestartDialog) {
|
||||
(*env)->ExceptionClear(env);
|
||||
return;
|
||||
}
|
||||
|
||||
char message[256];
|
||||
snprintf(message, sizeof(message),
|
||||
"The following changes require a restart to take effect: %s. Everything else has "
|
||||
"already been applied.",
|
||||
restart_needed);
|
||||
jstring messageStr = (*env)->NewStringUTF(env, message);
|
||||
(*env)->CallStaticVoidMethod(env, menuOverlayClass, showRestartDialog, messageStr);
|
||||
if ((*env)->ExceptionCheck(env))
|
||||
(*env)->ExceptionClear(env);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Public API (see qs_config.h)
|
||||
// ===========================================================================
|
||||
|
||||
static Uint32 s_save_event_type;
|
||||
static Uint32 s_load_event_type;
|
||||
|
||||
void qs_config_load_startup(void) {
|
||||
// Registers the custom SDL event types qs_config_request_save()/_load()
|
||||
// enqueue - done here, once, on the engine's own thread and before the
|
||||
// menu/its buttons could possibly exist yet, rather than lazily from
|
||||
// those functions themselves (which run on Android's UI thread and
|
||||
// could otherwise race each other doing this lazily).
|
||||
Uint32 base = SDL_RegisterEvents(2);
|
||||
s_save_event_type = base;
|
||||
s_load_event_type = base + 1;
|
||||
|
||||
QsParsedConfig parsed;
|
||||
if (qs_config_read_file(&parsed))
|
||||
qs_config_apply_parsed(&parsed, /*live=*/false, NULL, 0);
|
||||
}
|
||||
|
||||
void qs_config_request_save(void) {
|
||||
SDL_Event event;
|
||||
SDL_zero(event);
|
||||
event.type = s_save_event_type;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
void qs_config_request_load(void) {
|
||||
SDL_Event event;
|
||||
SDL_zero(event);
|
||||
event.type = s_load_event_type;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
bool qs_config_handle_event(const SDL_Event *event) {
|
||||
if (event->type == s_save_event_type) {
|
||||
if (qs_config_write_file())
|
||||
message_info("Configuration saved");
|
||||
else
|
||||
message_info("Failed to save configuration");
|
||||
return true;
|
||||
}
|
||||
if (event->type == s_load_event_type) {
|
||||
QsParsedConfig parsed;
|
||||
if (!qs_config_read_file(&parsed)) {
|
||||
message_info("No configuration file to load");
|
||||
return true;
|
||||
}
|
||||
char restart_needed[128];
|
||||
qs_config_apply_parsed(&parsed, /*live=*/true, restart_needed, sizeof(restart_needed));
|
||||
if (restart_needed[0] != '\0')
|
||||
qs_show_restart_dialog(restart_needed);
|
||||
else
|
||||
message_info("Configuration loaded");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef QUESTSHOCK_QS_CONFIG_H
|
||||
#define QUESTSHOCK_QS_CONFIG_H
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Persistent, human-editable JSON config file - see CONFIG.md at the repo
|
||||
// root for the documented schema. Lives at a plain relative path
|
||||
// ("questshock.config.json") since the whole engine already runs with its
|
||||
// cwd chdir()'d to the user-facing /sdcard/questshock/ folder (see
|
||||
// QuestShockActivity.java's nativeChdir()) before main() ever runs - the
|
||||
// same way prefs.txt/keybinds.txt (Prefs.c) already do.
|
||||
//
|
||||
// Deliberately independent of prefs.txt: this file is never read or written
|
||||
// as a side effect of prefs.txt load/save, and vice versa.
|
||||
|
||||
// Parses the config file if it exists (silent no-op if it doesn't - this
|
||||
// never creates the file itself) and applies every recognized option
|
||||
// directly into gShockPrefs/mode_id. Call once, early in main() - see
|
||||
// android/engine-patches/21-android-config.patch - after LoadPrefs() (so the
|
||||
// config file can override prefs.txt) and before init_all() (so there's no
|
||||
// live-vs-restart distinction to worry about: nothing is initialized yet, so
|
||||
// every field takes full effect as part of this same startup).
|
||||
void qs_config_load_startup(void);
|
||||
|
||||
// Called from the "Save Configuration"/"Load Configuration" button JNI
|
||||
// handlers (questshock_native.c), which run on Android's UI thread. These
|
||||
// only enqueue a custom SDL event - the actual file I/O and any
|
||||
// gShockPrefs/mode_id/message_info() work happens on the engine's own SDL
|
||||
// main thread instead (see qs_config_handle_event() below), matching this
|
||||
// codebase's existing convention for crossing this exact thread boundary
|
||||
// (xr_session.c's xr_poll_events(), questshock_native.c's SDL_TEXTINPUT
|
||||
// push for keyboard input).
|
||||
void qs_config_request_save(void);
|
||||
void qs_config_request_load(void);
|
||||
|
||||
// Returns true and handles the event if it's one of the two custom events
|
||||
// qs_config_request_save()/_load() enqueue - called from pump_events()
|
||||
// (sdl_events.c, see android/engine-patches/21-android-config.patch) on the
|
||||
// engine's own thread, once per polled event, before its normal switch on
|
||||
// well-known SDL_* event types. Returns false (no-op) for any other event
|
||||
// type, including before the two custom types have even been registered.
|
||||
bool qs_config_handle_event(const SDL_Event *event);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // QUESTSHOCK_QS_CONFIG_H
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include "qs_config.h"
|
||||
#include "xr_overlay.h"
|
||||
#include "xr_session.h"
|
||||
|
||||
@@ -88,3 +89,17 @@ JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeShowKeyboard(JNIEnv *env, jclass clazz) {
|
||||
xr_overlay_set_visible(xr_session_get_keyboard_overlay(), true);
|
||||
}
|
||||
|
||||
// MenuOverlay's "Save Configuration"/"Load Configuration" buttons - see
|
||||
// qs_config.h for why these only enqueue an event rather than doing the
|
||||
// actual file/engine-state work here directly (this runs on Android's UI
|
||||
// thread, not the engine's own).
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSaveConfig(JNIEnv *env, jclass clazz) {
|
||||
qs_config_request_save();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_de_ladkau_questshock_MenuOverlay_nativeRequestLoadConfig(JNIEnv *env, jclass clazz) {
|
||||
qs_config_request_load();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user