70c43240bc
build / build (push) Successful in 3m42s
The new Cheats tab exposes the engine's own cheat handlers as buttons: give all items and weapons, toggle physics, next level and previous level. Physics shows its state through the button highlight. The buttons need a running game and show a message otherwise. It also has four independent session-only switches for unlimited health, energy, ammo and items (grenades and drugs). While a switch is on, its resource is topped up every frame by qs_cheat_refill() from the new engine patch 26. The switches are not saved to the config.
1637 lines
66 KiB
C
1637 lines
66 KiB
C
// 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 "xr_input.h"
|
|
#include "xr_session.h"
|
|
|
|
#include "Prefs.h"
|
|
#include "fullscrn.h"
|
|
#include "lg.h"
|
|
#include "tools.h"
|
|
|
|
#define QS_CONFIG_FILENAME "questshock.config.json"
|
|
// Bundled with the app (QuestShockActivity.java extracts it fresh on every
|
|
// launch) and read only at startup, only if QS_CONFIG_FILENAME doesn't
|
|
// exist yet - never touched by Save/Load Configuration.
|
|
#define QS_CONFIG_DEFAULT_FILENAME "questshock.config.default.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;
|
|
}
|
|
|
|
// Parses a JSON number at the cursor into *out - unlike qs_json_skip_number()
|
|
// above (which every other option so far has only ever needed, since every
|
|
// existing option is a string enum), this one actually extracts the value.
|
|
// Same scan-then-copy-then-parse approach as qs_json_parse_string(), needed
|
|
// because the source buffer isn't NUL-terminated at an arbitrary offset, so
|
|
// strtof() can't just be pointed at the cursor directly.
|
|
static bool qs_json_parse_number(QsJsonParser *p, float *out) {
|
|
if (p->error)
|
|
return false;
|
|
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++;
|
|
size_t len = (size_t)(p->p - start);
|
|
if (len == 0 || len >= 64) {
|
|
qs_json_fail(p);
|
|
return false;
|
|
}
|
|
char buf[64];
|
|
memcpy(buf, start, len);
|
|
buf[len] = '\0';
|
|
char *endptr = NULL;
|
|
*out = strtof(buf, &endptr);
|
|
if (endptr == buf) {
|
|
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);
|
|
}
|
|
|
|
// Same "skip if wrong type" behavior as qs_json_take_string_field() above,
|
|
// for a numeric field - sets *has to true only on success, since (unlike
|
|
// the string groups, where an empty string means "not present") 0.0f is a
|
|
// perfectly valid value and can't double as an absent-sentinel.
|
|
static void qs_json_take_number_field(QsJsonParser *p, float *out, bool *has) {
|
|
if (!p->error && p->p < p->end && (isdigit((unsigned char)*p->p) || *p->p == '-')) {
|
|
if (qs_json_parse_number(p, out))
|
|
*has = true;
|
|
} 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
|
|
// The virtual screen's physical width (meters) for each of the two
|
|
// screen profiles - see qs_screen_profile_notify_loop_mode()'s own
|
|
// comment. Like QsParsedPlayerPosition below (and unlike renderer/
|
|
// resolution above), these are plain numbers, not string enums, and
|
|
// need their own has_* bools since 0.0f is a valid value.
|
|
float initial_screen_width_m, initial_screen_distance_m;
|
|
float game_screen_width_m, game_screen_distance_m;
|
|
bool has_initial_screen_width_m, has_initial_screen_distance_m;
|
|
bool has_game_screen_width_m, has_game_screen_distance_m;
|
|
// Each of the two options (width/distance) is offered as 5 preset
|
|
// buttons in the Video Options tab, generated as offset + step*index
|
|
// (index 0..4) rather than a fixed table - see qs_preset_value()'s
|
|
// own comment. Shared by both "Initial Screen" and "Game Screen"
|
|
// (both columns' preset buttons show the same 5 values per row) -
|
|
// config-file-only (no UI control): the buttons are for adjusting
|
|
// each profile's own current *value*, this offset/step pair is for
|
|
// reshaping what those buttons themselves offer.
|
|
float screen_width_offset_m, screen_width_step_m;
|
|
float screen_distance_offset_m, screen_distance_step_m;
|
|
bool has_screen_width_offset_m, has_screen_width_step_m;
|
|
bool has_screen_distance_offset_m, has_screen_distance_step_m;
|
|
} QsParsedVideoOptions;
|
|
|
|
typedef struct {
|
|
char language[16]; // "" = not present in the file
|
|
} QsParsedGeneral;
|
|
|
|
typedef struct {
|
|
float x, z, yaw_degrees;
|
|
// Unlike every string-valued option above, 0.0f is a perfectly valid
|
|
// value here and can't double as "not present in the file" - see
|
|
// qs_json_take_number_field()'s own comment.
|
|
bool has_x, has_z, has_yaw_degrees;
|
|
} QsParsedPlayerPosition;
|
|
|
|
// One string slot per remappable action, indexed by
|
|
// xr_input_action_index_from_name(). This file does not list the actions
|
|
// itself; xr_input.c owns them. Sized above the current action count.
|
|
#define QS_MAX_PLAYER_CONTROL_ACTIONS 96
|
|
typedef struct {
|
|
char bindings[QS_MAX_PLAYER_CONTROL_ACTIONS][24]; // "" = not present in the file
|
|
} QsParsedPlayerControls;
|
|
|
|
typedef struct {
|
|
QsParsedVideoOptions video_options;
|
|
bool has_video_options;
|
|
QsParsedGeneral general;
|
|
bool has_general;
|
|
QsParsedPlayerPosition player_position;
|
|
bool has_player_position;
|
|
QsParsedPlayerControls player_controls;
|
|
bool has_player_controls;
|
|
} 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 if (strcmp(key, "initial_screen_width_m") == 0)
|
|
qs_json_take_number_field(p, &out->initial_screen_width_m, &out->has_initial_screen_width_m);
|
|
else if (strcmp(key, "game_screen_width_m") == 0)
|
|
qs_json_take_number_field(p, &out->game_screen_width_m, &out->has_game_screen_width_m);
|
|
else if (strcmp(key, "initial_screen_distance_m") == 0)
|
|
qs_json_take_number_field(p, &out->initial_screen_distance_m,
|
|
&out->has_initial_screen_distance_m);
|
|
else if (strcmp(key, "game_screen_distance_m") == 0)
|
|
qs_json_take_number_field(p, &out->game_screen_distance_m,
|
|
&out->has_game_screen_distance_m);
|
|
else if (strcmp(key, "screen_width_offset_m") == 0)
|
|
qs_json_take_number_field(p, &out->screen_width_offset_m, &out->has_screen_width_offset_m);
|
|
else if (strcmp(key, "screen_width_step_m") == 0)
|
|
qs_json_take_number_field(p, &out->screen_width_step_m, &out->has_screen_width_step_m);
|
|
else if (strcmp(key, "screen_distance_offset_m") == 0)
|
|
qs_json_take_number_field(p, &out->screen_distance_offset_m,
|
|
&out->has_screen_distance_offset_m);
|
|
else if (strcmp(key, "screen_distance_step_m") == 0)
|
|
qs_json_take_number_field(p, &out->screen_distance_step_m,
|
|
&out->has_screen_distance_step_m);
|
|
else
|
|
qs_json_skip_value(p);
|
|
}
|
|
|
|
static void qs_parse_general_field(const char *key, QsJsonParser *p, void *ctx) {
|
|
QsParsedGeneral *out = (QsParsedGeneral *)ctx;
|
|
if (strcmp(key, "language") == 0)
|
|
qs_json_take_string_field(p, out->language, sizeof(out->language));
|
|
else
|
|
qs_json_skip_value(p);
|
|
}
|
|
|
|
static void qs_parse_player_position_field(const char *key, QsJsonParser *p, void *ctx) {
|
|
QsParsedPlayerPosition *out = (QsParsedPlayerPosition *)ctx;
|
|
if (strcmp(key, "x") == 0)
|
|
qs_json_take_number_field(p, &out->x, &out->has_x);
|
|
else if (strcmp(key, "z") == 0)
|
|
qs_json_take_number_field(p, &out->z, &out->has_z);
|
|
else if (strcmp(key, "yaw_degrees") == 0)
|
|
qs_json_take_number_field(p, &out->yaw_degrees, &out->has_yaw_degrees);
|
|
else
|
|
qs_json_skip_value(p);
|
|
}
|
|
|
|
// Looks up `key` in xr_input.c's action-name list instead of a fixed strcmp
|
|
// chain.
|
|
static void qs_parse_player_controls_field(const char *key, QsJsonParser *p, void *ctx) {
|
|
QsParsedPlayerControls *out = (QsParsedPlayerControls *)ctx;
|
|
int index;
|
|
if (xr_input_action_index_from_name(key, &index) && index < QS_MAX_PLAYER_CONTROL_ACTIONS)
|
|
qs_json_take_string_field(p, out->bindings[index], sizeof(out->bindings[index]));
|
|
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 if (strcmp(key, "general") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
|
out->has_general = qs_json_parse_object(p, qs_parse_general_field, &out->general);
|
|
else if (strcmp(key, "player_position") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
|
out->has_player_position =
|
|
qs_json_parse_object(p, qs_parse_player_position_field, &out->player_position);
|
|
else if (strcmp(key, "player_controls") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
|
out->has_player_controls =
|
|
qs_json_parse_object(p, qs_parse_player_controls_field, &out->player_controls);
|
|
else
|
|
qs_json_skip_value(p);
|
|
}
|
|
|
|
// Reads and parses `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_named(const char *filename, QsParsedConfig *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
FILE *f = fopen(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", filename);
|
|
memset(out, 0, sizeof(*out));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool qs_config_read_file(QsParsedConfig *out) {
|
|
return qs_config_read_file_named(QS_CONFIG_FILENAME, out);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// 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
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Video Options: screen profile (initial screen / game screen)
|
|
// ===========================================================================
|
|
|
|
// Live-appliable: xr_session_set_game_screen_shape() (xr_session.c) is a
|
|
// plain state setter with no XR calls, same category as
|
|
// xr_session_set_player_transform() - safe to call live or at startup.
|
|
#define QS_SCREEN_PROFILE_REQUIRES_RESTART false
|
|
|
|
// mainloop.h's own loop-mode #defines - declared locally rather than
|
|
// #include "mainloop.h", which pulls in an `extern LGRegion *_current_view;`
|
|
// this file can't resolve (LGRegion isn't visible via any header this file
|
|
// already includes) - same "avoid a header with declarations this file has
|
|
// no other reason to need" reasoning as screen_draw()/language_change()
|
|
// above. These two values must stay in sync with mainloop.h's own
|
|
// SETUP_LOOP/CUTSCENE_LOOP.
|
|
#define QS_SETUP_LOOP 4
|
|
#define QS_CUTSCENE_LOOP 6
|
|
|
|
// Each of the two options (width/distance) offers 5 preset buttons in
|
|
// the Video Options tab, generated as offset+step*index (index 0..4)
|
|
// rather than a fixed table - see qs_preset_value() below. Shared by
|
|
// both "Initial Screen" and "Game Screen" (both columns' preset buttons
|
|
// show the same 5 values per row), config-file-only (no UI control):
|
|
// unlike the four *_screen_width_m-style current-value statics below
|
|
// (button-driven, live-appliable, one pair per profile), these only
|
|
// reshape what the buttons themselves offer.
|
|
#define QS_NUM_SCREEN_PRESETS 5
|
|
static float s_screen_width_offset_m = 1.0f;
|
|
static float s_screen_width_step_m = 0.6f;
|
|
static float s_screen_distance_offset_m = 1.0f;
|
|
static float s_screen_distance_step_m = 0.5f;
|
|
|
|
// Generates preset index `index`'s value (index 0..QS_NUM_SCREEN_PRESETS-1)
|
|
// for a given offset/step pair - e.g. offset 1.0/step 0.6 generates
|
|
// 1.0, 1.6, 2.2, 2.8, 3.4 (this feature's original width presets).
|
|
static float qs_preset_value(float offset, float step, int index) {
|
|
return offset + step * (float)index;
|
|
}
|
|
|
|
// Inverse of qs_preset_value() - returns -1 (a legitimate result here,
|
|
// unlike kResolutions'-style "shouldn't happen" fallbacks - a hand-edited
|
|
// config file can set any value in between presets) if the current value
|
|
// doesn't exactly match one of the 5 presets this offset/step pair
|
|
// generates.
|
|
static int qs_preset_index(float offset, float step, float value) {
|
|
for (int i = 0; i < QS_NUM_SCREEN_PRESETS; i++)
|
|
if (qs_preset_value(offset, step, i) == value)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
// 1.6m/2.0m (qs_preset_value() index 1/2 of the shared offset/step
|
|
// defaults above) for both profiles - the game screen's own original,
|
|
// pre-this-feature hardcoded shape (QUAD_DISTANCE_METERS in xr_session.c),
|
|
// so a config file/build that never touches these renders identically to
|
|
// before until the player opts in.
|
|
static float s_initial_screen_width_m = 1.6f;
|
|
static float s_initial_screen_distance_m = 2.0f;
|
|
static float s_game_screen_width_m = 1.6f;
|
|
static float s_game_screen_distance_m = 2.0f;
|
|
// Which profile xr_session.c's game-screen shape was last set to - so
|
|
// qs_screen_profile_notify_loop_mode() can skip re-applying anything on a
|
|
// same-profile transition (e.g. game -> automap -> game), and so a preset
|
|
// button tap while the *other* profile is active knows to just update the
|
|
// stored value without touching the live shape (see
|
|
// qs_config_handle_event()'s own screen-profile branches).
|
|
static bool s_screen_profile_is_game = false;
|
|
|
|
static float qs_clamp_screen_width(float width_m) {
|
|
if (width_m < 0.2f)
|
|
return 0.2f;
|
|
if (width_m > 10.0f)
|
|
return 10.0f;
|
|
return width_m;
|
|
}
|
|
|
|
static float qs_clamp_screen_distance(float distance_m) {
|
|
if (distance_m < 0.5f)
|
|
return 0.5f;
|
|
if (distance_m > 10.0f)
|
|
return 10.0f;
|
|
return distance_m;
|
|
}
|
|
|
|
static void qs_screen_profile_reapply(void) {
|
|
if (s_screen_profile_is_game)
|
|
xr_session_set_game_screen_shape(s_game_screen_width_m, 0.0f, s_game_screen_distance_m);
|
|
else
|
|
xr_session_set_game_screen_shape(s_initial_screen_width_m, 0.0f,
|
|
s_initial_screen_distance_m);
|
|
}
|
|
|
|
// Called directly from the engine (see android/engine-patches/
|
|
// 22-android-screen-profile.patch's hook into mainloop.c's
|
|
// loopmode_switch(), and qs_config_load_startup() below for the initial
|
|
// call) - runs on the engine's own thread already, so unlike every other
|
|
// action in this file (all UI-thread-button-triggered), no cross-thread
|
|
// SDL-event dispatch is needed here.
|
|
void qs_screen_profile_notify_loop_mode(short new_mode) {
|
|
bool isGame = new_mode != QS_SETUP_LOOP && new_mode != QS_CUTSCENE_LOOP;
|
|
if (isGame == s_screen_profile_is_game)
|
|
return;
|
|
s_screen_profile_is_game = isGame;
|
|
qs_screen_profile_reapply();
|
|
}
|
|
|
|
static bool qs_apply_initial_screen_width(float width_m) {
|
|
width_m = qs_clamp_screen_width(width_m);
|
|
bool changed = s_initial_screen_width_m != width_m;
|
|
s_initial_screen_width_m = width_m;
|
|
if (changed && !s_screen_profile_is_game)
|
|
qs_screen_profile_reapply();
|
|
return changed;
|
|
}
|
|
|
|
static bool qs_apply_game_screen_width(float width_m) {
|
|
width_m = qs_clamp_screen_width(width_m);
|
|
bool changed = s_game_screen_width_m != width_m;
|
|
s_game_screen_width_m = width_m;
|
|
if (changed && s_screen_profile_is_game)
|
|
qs_screen_profile_reapply();
|
|
return changed;
|
|
}
|
|
|
|
static bool qs_apply_initial_screen_distance(float distance_m) {
|
|
distance_m = qs_clamp_screen_distance(distance_m);
|
|
bool changed = s_initial_screen_distance_m != distance_m;
|
|
s_initial_screen_distance_m = distance_m;
|
|
if (changed && !s_screen_profile_is_game)
|
|
qs_screen_profile_reapply();
|
|
return changed;
|
|
}
|
|
|
|
static bool qs_apply_game_screen_distance(float distance_m) {
|
|
distance_m = qs_clamp_screen_distance(distance_m);
|
|
bool changed = s_game_screen_distance_m != distance_m;
|
|
s_game_screen_distance_m = distance_m;
|
|
if (changed && s_screen_profile_is_game)
|
|
qs_screen_profile_reapply();
|
|
return changed;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// General: language
|
|
// ===========================================================================
|
|
|
|
// Live-appliable: language_change() (below) is the resource-reload half of
|
|
// what the game's own options menu calls when the player changes language
|
|
// there (see wrapper.c's language_dealfunc(), wired up via
|
|
// multi_init(..., REF_STR_Language, ...)) - closes/reopens the
|
|
// language-specific resource files, updates which_lang/QUESTVAR, and
|
|
// refreshes the inventory/MFD/side-icon panels' own language-dependent
|
|
// text/art. Deliberately NOT calling language_dealfunc() itself: its extra
|
|
// render_run()/opanel_redraw() repaint step assumes the game's own
|
|
// in-game options-panel widget is the thing currently on screen, which
|
|
// isn't true when this is triggered from our independent VR overlay menu -
|
|
// confirmed by a device crash (SIGSEGV, null pointer) inside render_run()'s
|
|
// 3D-view frustum-clipping code when called this way. The game's normal
|
|
// per-frame render loop already redraws everything anyway.
|
|
#define QS_LANGUAGE_REQUIRES_RESTART false
|
|
|
|
// wrapper.h declares this one, but including the whole header pulls in
|
|
// other prototypes referencing types this file has no other reason to need
|
|
// - same reasoning as screen_draw() above.
|
|
extern void language_change(uchar lang);
|
|
|
|
// gamestrn.c/init.c - declared locally rather than pulling in their
|
|
// headers, same reasoning as language_change() above. language_change()
|
|
// itself indexes these two by lang (0/1/2) to reopen the string/MFD-art
|
|
// resource files for the new language - but it does so destructively,
|
|
// closing the *current* (working) files first, with no fallback if the
|
|
// reopen then fails. qs_language_resources_available() below checks both
|
|
// files exist *before* ever calling language_change(), so a language whose
|
|
// resource pack wasn't fully installed can't leave the game with no valid
|
|
// resource file open at all.
|
|
extern char *language_files[];
|
|
extern char *mfdart_files[];
|
|
|
|
static const struct {
|
|
const char *name;
|
|
short language;
|
|
} kLanguages[] = {
|
|
{"english", 0},
|
|
{"french", 1},
|
|
{"german", 2},
|
|
};
|
|
#define QS_NUM_LANGUAGES (sizeof(kLanguages) / sizeof(kLanguages[0]))
|
|
|
|
// Confirmed necessary on a real device: this project's own Android data set
|
|
// ships frnstrng.res/gerstrng.res (language_files[]) but not
|
|
// mfdfrn.res/mfdger.res (mfdart_files[]) - selecting French or German used
|
|
// to call language_change(), which closes the working English resource
|
|
// files before discovering the new ones don't exist, leaving no valid
|
|
// resource file open at all and crashing (SIGBUS, null resource pointer)
|
|
// the next time any resource lookup happened.
|
|
static bool qs_language_resources_available(short lang) {
|
|
FILE *f = fopen(language_files[lang], "rb");
|
|
if (!f)
|
|
return false;
|
|
fclose(f);
|
|
f = fopen(mfdart_files[lang], "rb");
|
|
if (!f)
|
|
return false;
|
|
fclose(f);
|
|
return true;
|
|
}
|
|
|
|
// Defined further down, alongside qs_show_restart_dialog() (same
|
|
// find-class-via-classloader JNI plumbing) - forward-declared here since
|
|
// qs_apply_language() above is defined earlier in the file.
|
|
static void qs_show_message_dialog(const char *message);
|
|
|
|
static bool qs_apply_language(const char *value, bool live) {
|
|
for (size_t i = 0; i < QS_NUM_LANGUAGES; i++) {
|
|
if (strcmp(value, kLanguages[i].name) != 0)
|
|
continue;
|
|
if (!qs_language_resources_available(kLanguages[i].language)) {
|
|
WARN("%s: general.language=\"%s\" - resource files for this language aren't installed "
|
|
"- not switching",
|
|
QS_CONFIG_FILENAME, value);
|
|
// Only when there's a menu to show it in - live=false is the
|
|
// startup path (qs_config_load_startup()), before the menu
|
|
// overlay exists; qs_show_message_dialog() would just silently
|
|
// no-op there anyway (MenuOverlay.instance is still null), but
|
|
// there's no point trying.
|
|
if (live) {
|
|
char message[256];
|
|
snprintf(message, sizeof(message),
|
|
"Can't switch to %s: its resource files (%s, %s) aren't installed in "
|
|
"res/data/ - see CONFIG.md.",
|
|
value, language_files[kLanguages[i].language],
|
|
mfdart_files[kLanguages[i].language]);
|
|
qs_show_message_dialog(message);
|
|
}
|
|
return false;
|
|
}
|
|
bool changed = gShockPrefs.goLanguage != kLanguages[i].language;
|
|
gShockPrefs.goLanguage = kLanguages[i].language;
|
|
if (changed && live)
|
|
language_change((uchar)kLanguages[i].language);
|
|
return changed;
|
|
}
|
|
WARN("%s: general.language=\"%s\" is invalid - ignoring", QS_CONFIG_FILENAME, value);
|
|
return false;
|
|
}
|
|
|
|
static const char *qs_current_language_name(void) {
|
|
for (size_t i = 0; i < QS_NUM_LANGUAGES; i++)
|
|
if (kLanguages[i].language == gShockPrefs.goLanguage)
|
|
return kLanguages[i].name;
|
|
return "english"; // shouldn't happen - safe fallback for Save
|
|
}
|
|
|
|
// ===========================================================================
|
|
// General: player position
|
|
// ===========================================================================
|
|
|
|
// Live-appliable: xr_session_set_player_transform() (xr_session.c) is a
|
|
// plain state setter with no XR API calls of its own - safe to call before
|
|
// xr_init() has even run (which is how startup seeding works here, unlike
|
|
// every other option: nothing in xr_session.c exists yet to "apply" this
|
|
// to at startup, so the value is simply stored and picked up once
|
|
// xr_init() creates its own play space - see that function's own comment)
|
|
// as well as live, mid-session.
|
|
#define QS_PLAYER_POSITION_REQUIRES_RESTART false
|
|
|
|
// Applies only the fields actually present in *parsed, leaving the other
|
|
// axis/yaw untouched - e.g. a hand-edited file with just `"yaw_degrees"`
|
|
// shouldn't reset x/z to 0. Always live-appliable (see above) - the `live`
|
|
// parameter argument that qs_apply_renderer()/qs_apply_resolution()/
|
|
// qs_apply_language() take doesn't apply here, so unlike those this takes
|
|
// no such argument.
|
|
static bool qs_apply_player_position(const QsParsedPlayerPosition *parsed) {
|
|
float x, z, yaw_degrees;
|
|
xr_session_get_player_transform(&x, &z, &yaw_degrees);
|
|
bool changed = false;
|
|
if (parsed->has_x && parsed->x != x) {
|
|
x = parsed->x;
|
|
changed = true;
|
|
}
|
|
if (parsed->has_z && parsed->z != z) {
|
|
z = parsed->z;
|
|
changed = true;
|
|
}
|
|
if (parsed->has_yaw_degrees && parsed->yaw_degrees != yaw_degrees) {
|
|
yaw_degrees = parsed->yaw_degrees;
|
|
changed = true;
|
|
}
|
|
if (changed)
|
|
xr_session_set_player_transform(x, z, yaw_degrees);
|
|
return changed;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// General: player controls
|
|
// ===========================================================================
|
|
|
|
// Live-appliable: xr_input_set_action_binding() (xr_input.c) just writes
|
|
// into the per-frame lookup table xr_input_sync_and_draw() already reads
|
|
// every frame - no restart distinction needed, same as player_position.
|
|
#define QS_PLAYER_CONTROLS_REQUIRES_RESTART false
|
|
|
|
// Applies only the actions present in *parsed and leaves the rest untouched.
|
|
// Invalid raw-input names (a typo, or a symmetric form on the wrong action -
|
|
// see xr_input_set_action_binding()) are WARN+skipped per action.
|
|
static bool qs_apply_player_controls(const QsParsedPlayerControls *parsed) {
|
|
bool changed = false;
|
|
int count = xr_input_action_count();
|
|
if (count > QS_MAX_PLAYER_CONTROL_ACTIONS)
|
|
count = QS_MAX_PLAYER_CONTROL_ACTIONS;
|
|
for (int i = 0; i < count; i++) {
|
|
if (parsed->bindings[i][0] == '\0')
|
|
continue;
|
|
int before = xr_input_get_action_binding(i);
|
|
if (!xr_input_set_action_binding(i, parsed->bindings[i])) {
|
|
WARN("%s: player_controls.%s=\"%s\" is invalid - ignoring", QS_CONFIG_FILENAME,
|
|
xr_input_action_name(i), parsed->bindings[i]);
|
|
continue;
|
|
}
|
|
if (xr_input_get_action_binding(i) != before)
|
|
changed = true;
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Read-only queries for the Video Options tab (see qs_config.h)
|
|
// ===========================================================================
|
|
|
|
bool qs_config_current_renderer_is_opengl(void) { return gShockPrefs.doUseOpenGL; }
|
|
|
|
int qs_config_current_resolution_index(void) {
|
|
for (size_t i = 0; i < QS_NUM_RESOLUTIONS; i++)
|
|
if (kResolutions[i].mode == mode_id)
|
|
return (int)i;
|
|
return 3; // shouldn't happen - 640x480, matches qs_current_resolution()'s own fallback
|
|
}
|
|
|
|
int qs_config_current_language(void) { return gShockPrefs.goLanguage; }
|
|
|
|
// -1 (no exact preset match) is a legitimate result for all four of these -
|
|
// see qs_preset_index()'s own comment.
|
|
int qs_config_current_initial_screen_width_index(void) {
|
|
return qs_preset_index(s_screen_width_offset_m, s_screen_width_step_m, s_initial_screen_width_m);
|
|
}
|
|
int qs_config_current_initial_screen_distance_index(void) {
|
|
return qs_preset_index(s_screen_distance_offset_m, s_screen_distance_step_m,
|
|
s_initial_screen_distance_m);
|
|
}
|
|
int qs_config_current_game_screen_width_index(void) {
|
|
return qs_preset_index(s_screen_width_offset_m, s_screen_width_step_m, s_game_screen_width_m);
|
|
}
|
|
int qs_config_current_game_screen_distance_index(void) {
|
|
return qs_preset_index(s_screen_distance_offset_m, s_screen_distance_step_m,
|
|
s_game_screen_distance_m);
|
|
}
|
|
|
|
// Returns the value preset button `index` (0..QS_NUM_SCREEN_PRESETS-1)
|
|
// currently shows for a given option - shared by both profiles' columns
|
|
// (see the offset/step statics' own comment) - so MenuOverlay.java can
|
|
// label its preset buttons correctly even after a config-file offset/step
|
|
// change. Out-of-range index clamps into range rather than returning a
|
|
// sentinel: unlike the index getters above, there's no "not present"
|
|
// concept here - every index 0..4 always has a value.
|
|
static int qs_clamp_preset_index(int index) {
|
|
if (index < 0)
|
|
return 0;
|
|
if (index >= QS_NUM_SCREEN_PRESETS)
|
|
return QS_NUM_SCREEN_PRESETS - 1;
|
|
return index;
|
|
}
|
|
float qs_config_screen_width_preset_value(int index) {
|
|
return qs_preset_value(s_screen_width_offset_m, s_screen_width_step_m, qs_clamp_preset_index(index));
|
|
}
|
|
float qs_config_screen_distance_preset_value(int index) {
|
|
return qs_preset_value(s_screen_distance_offset_m, s_screen_distance_step_m,
|
|
qs_clamp_preset_index(index));
|
|
}
|
|
|
|
// ===========================================================================
|
|
// 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) {
|
|
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");
|
|
}
|
|
if (parsed->video_options.has_initial_screen_width_m) {
|
|
bool changed = qs_apply_initial_screen_width(parsed->video_options.initial_screen_width_m);
|
|
if (changed && QS_SCREEN_PROFILE_REQUIRES_RESTART)
|
|
qs_append_restart_reason(restart_needed, restart_needed_size, "initial_screen_width_m");
|
|
}
|
|
if (parsed->video_options.has_game_screen_width_m) {
|
|
bool changed = qs_apply_game_screen_width(parsed->video_options.game_screen_width_m);
|
|
if (changed && QS_SCREEN_PROFILE_REQUIRES_RESTART)
|
|
qs_append_restart_reason(restart_needed, restart_needed_size, "game_screen_width_m");
|
|
}
|
|
if (parsed->video_options.has_initial_screen_distance_m) {
|
|
bool changed =
|
|
qs_apply_initial_screen_distance(parsed->video_options.initial_screen_distance_m);
|
|
if (changed && QS_SCREEN_PROFILE_REQUIRES_RESTART)
|
|
qs_append_restart_reason(restart_needed, restart_needed_size,
|
|
"initial_screen_distance_m");
|
|
}
|
|
if (parsed->video_options.has_game_screen_distance_m) {
|
|
bool changed =
|
|
qs_apply_game_screen_distance(parsed->video_options.game_screen_distance_m);
|
|
if (changed && QS_SCREEN_PROFILE_REQUIRES_RESTART)
|
|
qs_append_restart_reason(restart_needed, restart_needed_size,
|
|
"game_screen_distance_m");
|
|
}
|
|
// The four offset/step fields below only reshape what the preset
|
|
// buttons offer (see their own statics' comment) - no live-reapply
|
|
// or restart consideration needed, just a direct assignment.
|
|
if (parsed->video_options.has_screen_width_offset_m)
|
|
s_screen_width_offset_m = parsed->video_options.screen_width_offset_m;
|
|
if (parsed->video_options.has_screen_width_step_m)
|
|
s_screen_width_step_m = parsed->video_options.screen_width_step_m;
|
|
if (parsed->video_options.has_screen_distance_offset_m)
|
|
s_screen_distance_offset_m = parsed->video_options.screen_distance_offset_m;
|
|
if (parsed->video_options.has_screen_distance_step_m)
|
|
s_screen_distance_step_m = parsed->video_options.screen_distance_step_m;
|
|
}
|
|
if (parsed->has_general) {
|
|
if (parsed->general.language[0] != '\0') {
|
|
bool changed = qs_apply_language(parsed->general.language, live);
|
|
if (changed && QS_LANGUAGE_REQUIRES_RESTART)
|
|
qs_append_restart_reason(restart_needed, restart_needed_size, "language");
|
|
}
|
|
}
|
|
if (parsed->has_player_position) {
|
|
bool changed = qs_apply_player_position(&parsed->player_position);
|
|
if (changed && QS_PLAYER_POSITION_REQUIRES_RESTART)
|
|
qs_append_restart_reason(restart_needed, restart_needed_size, "player_position");
|
|
}
|
|
if (parsed->has_player_controls) {
|
|
bool changed = qs_apply_player_controls(&parsed->player_controls);
|
|
if (changed && QS_PLAYER_CONTROLS_REQUIRES_RESTART)
|
|
qs_append_restart_reason(restart_needed, restart_needed_size, "player_controls");
|
|
}
|
|
}
|
|
|
|
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, " \"initial_screen_width_m\": %f,\n", s_initial_screen_width_m);
|
|
fprintf(f, " \"initial_screen_distance_m\": %f,\n", s_initial_screen_distance_m);
|
|
fprintf(f, " \"game_screen_width_m\": %f,\n", s_game_screen_width_m);
|
|
fprintf(f, " \"game_screen_distance_m\": %f,\n", s_game_screen_distance_m);
|
|
fprintf(f, " \"screen_width_offset_m\": %f,\n", s_screen_width_offset_m);
|
|
fprintf(f, " \"screen_width_step_m\": %f,\n", s_screen_width_step_m);
|
|
fprintf(f, " \"screen_distance_offset_m\": %f,\n", s_screen_distance_offset_m);
|
|
fprintf(f, " \"screen_distance_step_m\": %f\n", s_screen_distance_step_m);
|
|
fprintf(f, " },\n");
|
|
fprintf(f, " \"general\": {\n");
|
|
fprintf(f, " \"language\": \"%s\"\n", qs_current_language_name());
|
|
fprintf(f, " },\n");
|
|
{
|
|
int count = xr_input_action_count();
|
|
fprintf(f, " \"player_controls\": {\n");
|
|
for (int i = 0; i < count; i++)
|
|
fprintf(f, " \"%s\": \"%s\"%s\n", xr_input_action_name(i),
|
|
xr_input_get_action_binding_name(i), i + 1 < count ? "," : "");
|
|
fprintf(f, " },\n");
|
|
}
|
|
{
|
|
float x, z, yaw_degrees;
|
|
xr_session_get_player_transform(&x, &z, &yaw_degrees);
|
|
fprintf(f, " \"player_position\": {\n");
|
|
fprintf(f, " \"x\": %f,\n", x);
|
|
fprintf(f, " \"z\": %f,\n", z);
|
|
fprintf(f, " \"yaw_degrees\": %f\n", yaw_degrees);
|
|
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;
|
|
}
|
|
|
|
// Shared by qs_show_restart_dialog() and qs_show_message_dialog() below -
|
|
// both call a MenuOverlay static method taking a single String and
|
|
// returning void, differing only in which one.
|
|
static void qs_call_dialog_method(const char *java_method_name, const char *message) {
|
|
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 method =
|
|
(*env)->GetStaticMethodID(env, menuOverlayClass, java_method_name, "(Ljava/lang/String;)V");
|
|
if (!method) {
|
|
(*env)->ExceptionClear(env);
|
|
return;
|
|
}
|
|
|
|
jstring messageStr = (*env)->NewStringUTF(env, message);
|
|
(*env)->CallStaticVoidMethod(env, menuOverlayClass, method, messageStr);
|
|
if ((*env)->ExceptionCheck(env))
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
|
|
static void qs_show_restart_dialog(const char *restart_needed) {
|
|
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);
|
|
qs_call_dialog_method("showRestartDialog", message);
|
|
}
|
|
|
|
// Same restart-confirmation dialog view (MenuOverlay.java's
|
|
// showMessageDialog()), minus its Restart Now button - for a plain
|
|
// informational/warning message with nothing to restart into, currently
|
|
// only qs_apply_language()'s missing-resource-files warning above.
|
|
static void qs_show_message_dialog(const char *message) { qs_call_dialog_method("showMessageDialog", message); }
|
|
|
|
// Non-cancelable, single-"Exit"-button dialog (MenuOverlay.java's
|
|
// showFatalControllerBindingError()) - see qs_config_report_fatal_input_error()'s
|
|
// own comment (qs_config.h) on when/why this fires.
|
|
void qs_config_report_fatal_input_error(const char *profile_display_name) {
|
|
char message[256];
|
|
snprintf(message, sizeof(message),
|
|
"Controller support failed to initialize: the OpenXR runtime rejected the \"%s\" "
|
|
"interaction profile's bindings. This is an app bug, not something you can fix.",
|
|
profile_display_name);
|
|
qs_call_dialog_method("showFatalControllerBindingError", message);
|
|
}
|
|
|
|
// Shared by qs_config_notify_control_capture_result() below - resolves
|
|
// MenuOverlay.onControlCaptureResult(int, String), called once per changed
|
|
// row (up to twice per capture - see this function's own header comment).
|
|
static void qs_call_control_capture_result(int action_index, const char *raw_name) {
|
|
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 method = (*env)->GetStaticMethodID(env, menuOverlayClass, "onControlCaptureResult",
|
|
"(ILjava/lang/String;)V");
|
|
if (!method) {
|
|
(*env)->ExceptionClear(env);
|
|
return;
|
|
}
|
|
|
|
jstring rawNameStr = raw_name ? (*env)->NewStringUTF(env, raw_name) : NULL;
|
|
(*env)->CallStaticVoidMethod(env, menuOverlayClass, method, (jint)action_index, rawNameStr);
|
|
if ((*env)->ExceptionCheck(env))
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
|
|
void qs_config_notify_control_capture_result(int action_index, const char *raw_name,
|
|
int displaced_action_index,
|
|
const char *displaced_raw_name) {
|
|
qs_call_control_capture_result(action_index, raw_name);
|
|
if (displaced_action_index >= 0)
|
|
qs_call_control_capture_result(displaced_action_index, displaced_raw_name);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Public API (see qs_config.h)
|
|
// ===========================================================================
|
|
|
|
static Uint32 s_save_event_type;
|
|
static Uint32 s_load_event_type;
|
|
static Uint32 s_set_renderer_event_type;
|
|
static Uint32 s_set_resolution_event_type;
|
|
static Uint32 s_set_language_event_type;
|
|
static Uint32 s_reset_position_event_type;
|
|
static Uint32 s_set_initial_screen_width_event_type;
|
|
static Uint32 s_set_initial_screen_distance_event_type;
|
|
static Uint32 s_set_game_screen_width_event_type;
|
|
static Uint32 s_set_game_screen_distance_event_type;
|
|
static Uint32 s_begin_control_capture_event_type;
|
|
static Uint32 s_cancel_control_capture_event_type;
|
|
static Uint32 s_reset_player_controls_event_type;
|
|
static Uint32 s_menu_visibility_event_type;
|
|
static Uint32 s_cheat_event_type;
|
|
|
|
void qs_config_load_startup(void) {
|
|
// Registers the custom SDL event types qs_config_request_save()/_load()/
|
|
// _set_renderer()/_set_resolution()/_set_language()/_reset_position()/
|
|
// the four _set_*_screen_*() screen-profile setters/the three
|
|
// control-capture/reset requests 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(15);
|
|
s_save_event_type = base;
|
|
s_load_event_type = base + 1;
|
|
s_set_renderer_event_type = base + 2;
|
|
s_set_resolution_event_type = base + 3;
|
|
s_set_language_event_type = base + 4;
|
|
s_reset_position_event_type = base + 5;
|
|
s_set_initial_screen_width_event_type = base + 6;
|
|
s_set_game_screen_width_event_type = base + 7;
|
|
s_set_initial_screen_distance_event_type = base + 8;
|
|
s_set_game_screen_distance_event_type = base + 9;
|
|
s_begin_control_capture_event_type = base + 10;
|
|
s_cancel_control_capture_event_type = base + 11;
|
|
s_reset_player_controls_event_type = base + 12;
|
|
s_menu_visibility_event_type = base + 13;
|
|
s_cheat_event_type = base + 14;
|
|
|
|
QsParsedConfig parsed;
|
|
bool haveConfig = qs_config_read_file(&parsed);
|
|
if (!haveConfig)
|
|
haveConfig = qs_config_read_file_named(QS_CONFIG_DEFAULT_FILENAME, &parsed);
|
|
if (haveConfig)
|
|
qs_config_apply_parsed(&parsed, /*live=*/false, NULL, 0);
|
|
|
|
// A saved player_position only means anything if all three fields are
|
|
// present (a partial one - e.g. just "yaw_degrees" from a hand-edited
|
|
// file - isn't a complete, sane transform) - xr_input.c's own
|
|
// automatic startup recenter (xr_input_recenter_player_to_screen(),
|
|
// via g_pending_startup_recenter) is the fallback for a missing file,
|
|
// a missing player_position group, or an incomplete one; skip it here
|
|
// once a complete saved position was actually loaded, so it doesn't
|
|
// immediately override what was just applied above.
|
|
if (parsed.has_player_position && parsed.player_position.has_x &&
|
|
parsed.player_position.has_z && parsed.player_position.has_yaw_degrees)
|
|
xr_input_skip_startup_recenter();
|
|
|
|
// The engine's own startup state is always QS_SETUP_LOOP (mainloop.c:
|
|
// `short _current_loop = SETUP_LOOP;`, Shock.c's main() never changes
|
|
// it before entering mainloop()), matching s_screen_profile_is_game's
|
|
// own default (false/"initial") - so calling
|
|
// qs_screen_profile_notify_loop_mode() here would see "no change" and
|
|
// skip actually applying anything (its whole point is to skip
|
|
// redundant re-applies on a same-profile transition, but there's no
|
|
// "previous" profile yet this early). Call the apply step directly
|
|
// instead, unconditionally, so whichever profile's shape was just
|
|
// loaded/defaulted above actually reaches xr_session.c before the
|
|
// first frame renders.
|
|
qs_screen_profile_reapply();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void qs_config_request_set_renderer(bool opengl) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_set_renderer_event_type;
|
|
event.user.code = opengl ? 1 : 0;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_request_set_resolution(int resolution_index) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_set_resolution_event_type;
|
|
event.user.code = resolution_index;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_request_set_language(int language_index) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_set_language_event_type;
|
|
event.user.code = language_index;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_request_reset_position(void) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_reset_position_event_type;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
// Screen profile preset buttons (Video Options tab) - index into the
|
|
// shared width/distance offset+step-generated presets (see
|
|
// qs_preset_value()'s own comment), same enqueue-only reasoning as every
|
|
// other button in this file.
|
|
void qs_config_request_set_initial_screen_width(int preset_index) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_set_initial_screen_width_event_type;
|
|
event.user.code = preset_index;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_request_set_game_screen_width(int preset_index) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_set_game_screen_width_event_type;
|
|
event.user.code = preset_index;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_request_set_initial_screen_distance(int preset_index) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_set_initial_screen_distance_event_type;
|
|
event.user.code = preset_index;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_request_set_game_screen_distance(int preset_index) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_set_game_screen_distance_event_type;
|
|
event.user.code = preset_index;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
// Controller Options tab (see qs_config.h) - same enqueue-only reasoning as
|
|
// every button above.
|
|
void qs_config_request_begin_control_capture(int action_index) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_begin_control_capture_event_type;
|
|
event.user.code = action_index;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_request_cancel_control_capture(void) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_cancel_control_capture_event_type;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_request_cheat(int cheat_id) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_cheat_event_type;
|
|
event.user.code = cheat_id;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_notify_menu_visibility(bool visible) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_menu_visibility_event_type;
|
|
event.user.code = visible ? 1 : 0;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
void qs_config_request_reset_player_controls(void) {
|
|
SDL_Event event;
|
|
SDL_zero(event);
|
|
event.type = s_reset_player_controls_event_type;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
// hkeyfunc.c - the same toggle the game's own "pause" hotkey ('p') calls,
|
|
// which also handles the mouse capture/cursor/sound side of pausing.
|
|
extern uchar game_paused;
|
|
extern uchar pause_game_func(ushort keycode, uint32_t context, intptr_t data);
|
|
|
|
// True only while a pause this file caused is (as far as it knows) still in
|
|
// effect - see qs_config_notify_menu_visibility().
|
|
static bool s_menu_paused_game = false;
|
|
|
|
// s_screen_profile_is_game is "a game is running" (anything but the setup/
|
|
// main-menu and cutscene loops, see qs_screen_profile_notify_loop_mode()).
|
|
static void qs_apply_menu_pause(bool menu_visible) {
|
|
if (menu_visible) {
|
|
if (s_screen_profile_is_game && !game_paused) {
|
|
pause_game_func(0, 0, 0);
|
|
s_menu_paused_game = true;
|
|
}
|
|
} else if (s_menu_paused_game) {
|
|
s_menu_paused_game = false;
|
|
// Only toggle back if it's still paused - the game may have
|
|
// unpaused itself (or the player did) while the menu was open, and
|
|
// toggling then would pause it instead.
|
|
if (game_paused)
|
|
pause_game_func(0, 0, 0);
|
|
}
|
|
}
|
|
|
|
// hkeyfunc.c - the engine's own cheat hotkey handlers, and the refill added by
|
|
// android/engine-patches/26-android-cheat-refill.patch.
|
|
extern uchar toggle_giveall_func(ushort keycode, uint32_t context, intptr_t data);
|
|
extern uchar toggle_physics_func(ushort keycode, uint32_t context, intptr_t data);
|
|
extern uchar toggle_up_level_func(ushort keycode, uint32_t context, intptr_t data);
|
|
extern uchar toggle_down_level_func(ushort keycode, uint32_t context, intptr_t data);
|
|
extern void qs_cheat_refill(int what);
|
|
extern uchar physics_running;
|
|
|
|
#define QS_CHEAT_GIVE_ALL 0
|
|
#define QS_CHEAT_PHYSICS 1
|
|
#define QS_CHEAT_LEVEL_UP 2
|
|
#define QS_CHEAT_LEVEL_DOWN 3
|
|
#define QS_CHEAT_UNLIMITED_HEALTH 4
|
|
#define QS_CHEAT_UNLIMITED_ENERGY 5
|
|
#define QS_CHEAT_UNLIMITED_AMMO 6
|
|
#define QS_CHEAT_UNLIMITED_ITEMS 7
|
|
|
|
// Bits of s_cheat_unlimited; the values match qs_cheat_refill() in patch 26.
|
|
#define QS_CHEAT_MASK_HEALTH 1
|
|
#define QS_CHEAT_MASK_ENERGY 2
|
|
#define QS_CHEAT_MASK_AMMO 4
|
|
#define QS_CHEAT_MASK_ITEMS 8
|
|
|
|
static int s_cheat_unlimited = 0;
|
|
|
|
int qs_config_cheat_unlimited_mask(void) { return s_cheat_unlimited; }
|
|
bool qs_config_cheat_physics_running(void) { return physics_running != 0; }
|
|
|
|
void qs_config_frame_tick(void) {
|
|
if (s_cheat_unlimited && s_screen_profile_is_game)
|
|
qs_cheat_refill(s_cheat_unlimited);
|
|
}
|
|
|
|
static void qs_apply_cheat(int cheat_id) {
|
|
if (cheat_id >= QS_CHEAT_UNLIMITED_HEALTH && cheat_id <= QS_CHEAT_UNLIMITED_ITEMS) {
|
|
static const struct {
|
|
int mask;
|
|
const char *on, *off;
|
|
} kUnlimited[] = {
|
|
{QS_CHEAT_MASK_HEALTH, "Unlimited health on", "Unlimited health off"},
|
|
{QS_CHEAT_MASK_ENERGY, "Unlimited energy on", "Unlimited energy off"},
|
|
{QS_CHEAT_MASK_AMMO, "Unlimited ammo on", "Unlimited ammo off"},
|
|
{QS_CHEAT_MASK_ITEMS, "Unlimited items on", "Unlimited items off"},
|
|
};
|
|
const int i = cheat_id - QS_CHEAT_UNLIMITED_HEALTH;
|
|
s_cheat_unlimited ^= kUnlimited[i].mask;
|
|
message_info((s_cheat_unlimited & kUnlimited[i].mask) ? kUnlimited[i].on : kUnlimited[i].off);
|
|
return;
|
|
}
|
|
// The engine's own handlers assume a running game (player state, levels).
|
|
if (!s_screen_profile_is_game) {
|
|
message_info("Cheats need a running game");
|
|
return;
|
|
}
|
|
switch (cheat_id) {
|
|
case QS_CHEAT_GIVE_ALL:
|
|
toggle_giveall_func(0, 0, 0);
|
|
break;
|
|
case QS_CHEAT_PHYSICS:
|
|
toggle_physics_func(0, 0, 0);
|
|
break;
|
|
case QS_CHEAT_LEVEL_UP:
|
|
toggle_up_level_func(0, 0, 0);
|
|
break;
|
|
case QS_CHEAT_LEVEL_DOWN:
|
|
toggle_down_level_func(0, 0, 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
if (event->type == s_set_renderer_event_type) {
|
|
qs_apply_renderer(event->user.code ? "opengl" : "software", /*live=*/true);
|
|
return true;
|
|
}
|
|
if (event->type == s_set_resolution_event_type) {
|
|
int index = event->user.code;
|
|
if (index >= 0 && (size_t)index < QS_NUM_RESOLUTIONS)
|
|
qs_apply_resolution(kResolutions[index].name, /*live=*/true);
|
|
return true;
|
|
}
|
|
if (event->type == s_set_language_event_type) {
|
|
int index = event->user.code;
|
|
if (index >= 0 && (size_t)index < QS_NUM_LANGUAGES)
|
|
qs_apply_language(kLanguages[index].name, /*live=*/true);
|
|
return true;
|
|
}
|
|
if (event->type == s_reset_position_event_type) {
|
|
// Re-anchors the position onto the player's current real head position
|
|
// and keeps their facing (see xr_input_reset_player_position()). Does
|
|
// not read or write questshock.config.json.
|
|
xr_input_reset_player_position();
|
|
message_info("Recentered");
|
|
return true;
|
|
}
|
|
if (event->type == s_set_initial_screen_width_event_type) {
|
|
int index = event->user.code;
|
|
if (index >= 0 && index < QS_NUM_SCREEN_PRESETS)
|
|
qs_apply_initial_screen_width(
|
|
qs_preset_value(s_screen_width_offset_m, s_screen_width_step_m, index));
|
|
return true;
|
|
}
|
|
if (event->type == s_set_game_screen_width_event_type) {
|
|
int index = event->user.code;
|
|
if (index >= 0 && index < QS_NUM_SCREEN_PRESETS)
|
|
qs_apply_game_screen_width(
|
|
qs_preset_value(s_screen_width_offset_m, s_screen_width_step_m, index));
|
|
return true;
|
|
}
|
|
if (event->type == s_set_initial_screen_distance_event_type) {
|
|
int index = event->user.code;
|
|
if (index >= 0 && index < QS_NUM_SCREEN_PRESETS)
|
|
qs_apply_initial_screen_distance(
|
|
qs_preset_value(s_screen_distance_offset_m, s_screen_distance_step_m, index));
|
|
return true;
|
|
}
|
|
if (event->type == s_set_game_screen_distance_event_type) {
|
|
int index = event->user.code;
|
|
if (index >= 0 && index < QS_NUM_SCREEN_PRESETS)
|
|
qs_apply_game_screen_distance(
|
|
qs_preset_value(s_screen_distance_offset_m, s_screen_distance_step_m, index));
|
|
return true;
|
|
}
|
|
if (event->type == s_begin_control_capture_event_type) {
|
|
xr_input_capture_begin(event->user.code);
|
|
return true;
|
|
}
|
|
if (event->type == s_cancel_control_capture_event_type) {
|
|
xr_input_capture_cancel();
|
|
return true;
|
|
}
|
|
if (event->type == s_cheat_event_type) {
|
|
qs_apply_cheat(event->user.code);
|
|
return true;
|
|
}
|
|
if (event->type == s_menu_visibility_event_type) {
|
|
qs_apply_menu_pause(event->user.code != 0);
|
|
return true;
|
|
}
|
|
if (event->type == s_reset_player_controls_event_type) {
|
|
// Built-in defaults first (they cover every action), then
|
|
// questshock.config.default.json's player_controls group, if it has
|
|
// one, overrides the actions it specifies.
|
|
xr_input_reset_player_controls();
|
|
QsParsedConfig defaultParsed;
|
|
if (qs_config_read_file_named(QS_CONFIG_DEFAULT_FILENAME, &defaultParsed) &&
|
|
defaultParsed.has_player_controls)
|
|
qs_apply_player_controls(&defaultParsed.player_controls);
|
|
message_info("Controller bindings reset to defaults");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|