Add configurable game/initial screen width and distance, with a bundled first-run config
build / build (push) Successful in 2m41s

- Video Options tab offers width and distance presets independently for
  the Initial Screen (splash/intro/menu) and Game Screen (gameplay)
  profiles, with their preset step/offset configurable via
  questshock.config.json.
- Ships res/questshock.config.default.json as the initial config on
  first run (or whenever questshock.config.json doesn't exist yet),
  refreshed from the APK on every launch without touching a user's own
  saved config.
This commit is contained in:
ml
2026-09-06 09:00:45 +02:00
parent 5fe4cdf7f8
commit e1e6a006d4
14 changed files with 1052 additions and 91 deletions
+64 -4
View File
@@ -9,9 +9,11 @@ startup, in addition to (and independent of) the engine's own `prefs.txt`.
- **Format**: plain JSON, grouped by concern. Each top-level key is a
*group*; each key inside a group is one *option*.
- **Loading**: read automatically at app startup, before the splash screen,
if the file exists. If it doesn't exist, nothing changes - the engine
behaves exactly as it did before this file existed (driven by `prefs.txt`
as usual).
if the file exists. If it doesn't, `questshock.config.default.json` (same
folder) is used instead - a copy bundled with the app, refreshed on every
launch, never overwriting your own saved `questshock.config.json`. If
neither file exists, the engine behaves as it did before this file
existed (driven by `prefs.txt` as usual).
- **Saving**: never automatic. In-headset, open the menu (controller menu
button) and press **Save Configuration** to write the file from your
current live settings, overwriting it if present. Press **Load
@@ -47,7 +49,15 @@ startup, in addition to (and independent of) the engine's own `prefs.txt`.
{
"video_options": {
"renderer": "opengl",
"resolution": "1024x768"
"resolution": "1024x768",
"initial_screen_width_m": 1.6,
"initial_screen_distance_m": 2.0,
"game_screen_width_m": 2.8,
"game_screen_distance_m": 2.0,
"screen_width_offset_m": 1.0,
"screen_width_step_m": 0.6,
"screen_distance_offset_m": 1.0,
"screen_distance_step_m": 0.5
},
"general": {
"language": "english"
@@ -78,6 +88,56 @@ The game's internal rendering resolution.
- **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"`
- **Restart required?**: No (live-appliable)
### `initial_screen_width_m`, `initial_screen_distance_m`, `game_screen_width_m`, `game_screen_distance_m`
The virtual screen's physical width and distance from the player (both in
meters) - the game shows two independently configurable screen
"profiles", switched automatically based on what's currently on screen:
- **Initial Screen** (`initial_screen_*`) - the splash screen, intro
cutscenes, and the game's own main menu.
- **Game Screen** (`game_screen_*`) - actual gameplay (the 3D view,
automap, cyberspace, etc.).
Switching happens automatically - no player action needed - the instant
the game itself transitions between these (e.g. starting/loading a game,
or returning to the menu).
- **Type**: number (meters)
- **Possible values**: the in-headset Video Options tab offers 5 presets
each - widths `1.0`, `1.6` (the original, pre-this-feature default),
`2.2`, `2.8`, `3.4`; distances `1.0`, `1.5`, `2.0` (the original,
pre-this-feature default), `2.5`, `3.0` - but a hand-edited value
doesn't need to match a preset exactly (it's clamped to a sane range -
width `[0.2, 10.0]`, distance `[0.5, 10.0]` - rather than snapped or
rejected; the Video Options tab just shows no button selected for an
in-between value).
- **Restart required?**: No (live-appliable)
- **Note**: the screen is always flat - there's no curve option.
### `screen_width_offset_m`, `screen_width_step_m`, `screen_distance_offset_m`, `screen_distance_step_m`
Reshapes the 5 preset buttons the Video Options tab offers for width and
distance (above) - config-file-only, there's no in-headset control for
these. Shared by both **Initial Screen** and **Game Screen** (both
columns' buttons show the same 5 values per row) - this only changes
what the buttons offer, not either profile's current value.
Preset button `index` (0-4) shows `offset + step * index`. For example,
a distance offset of `0` and step of `2` shows buttons `0`, `2`, `4`,
`6`, `8`; the defaults below reproduce this feature's original launch
presets exactly.
- **Type**: number (meters)
- **Default values**: width offset `1.0`, step `0.6` (buttons `1.0`,
`1.6`, `2.2`, `2.8`, `3.4`); distance offset `1.0`, step `0.5` (buttons
`1.0`, `1.5`, `2.0`, `2.5`, `3.0`).
- **Restart required?**: No (live-appliable)
- **Note**: not clamped - an unusual offset/step (e.g. negative step) is
accepted as-is. A resulting preset value outside the current value's
own clamp range (see above) still clamps normally when that preset
button is actually pressed.
## General (`general`)
### `language`
+3
View File
@@ -104,10 +104,13 @@ def stageEngineTask = tasks.register("stageEngine", Exec) {
inputs.dir("${projectDir}/../gl4es-src")
inputs.dir("${projectDir}/../gl4es-patches")
inputs.file("${repoRoot}/build-image/Dockerfile")
// Staged into assets/ by prepare-android-project.sh.
inputs.file("${repoRoot}/res/questshock.config.default.json")
outputs.dir("${repoRoot}/build/android-engine")
outputs.dir("${repoRoot}/build/android-gl4es")
outputs.dir("${repoRoot}/build/android-prebuilt")
outputs.file("${projectDir}/../engine.properties")
outputs.dir("${projectDir}/src/main/assets")
}
// preBuild is what every variant's compile/native-build tasks already
+377 -14
View File
@@ -28,6 +28,10 @@
#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
@@ -330,6 +334,27 @@ static void qs_json_take_number_field(QsJsonParser *p, float *out, bool *has) {
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 {
@@ -359,6 +384,26 @@ static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void
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);
}
@@ -396,13 +441,13 @@ static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx
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
// 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(QsParsedConfig *out) {
static bool qs_config_read_file_named(const char *filename, QsParsedConfig *out) {
memset(out, 0, sizeof(*out));
FILE *f = fopen(QS_CONFIG_FILENAME, "rb");
FILE *f = fopen(filename, "rb");
if (!f)
return false;
fseek(f, 0, SEEK_END);
@@ -425,13 +470,17 @@ static bool qs_config_read_file(QsParsedConfig *out) {
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);
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
// ===========================================================================
@@ -531,6 +580,149 @@ static const char *qs_current_resolution(void) {
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
// ===========================================================================
@@ -697,6 +889,45 @@ int qs_config_current_resolution_index(void) {
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
// ===========================================================================
@@ -734,6 +965,41 @@ static void qs_config_apply_parsed(const QsParsedConfig *parsed, bool live, char
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') {
@@ -758,7 +1024,15 @@ static bool qs_config_write_file(void) {
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, " \"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());
@@ -853,24 +1127,36 @@ 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;
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()
// 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(6);
// _set_renderer()/_set_resolution()/_set_language()/_reset_position()/
// the four _set_*_screen_*() screen-profile setters 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(10);
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;
QsParsedConfig parsed;
if (qs_config_read_file(&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
@@ -884,6 +1170,19 @@ void qs_config_load_startup(void) {
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) {
@@ -931,6 +1230,42 @@ void qs_config_request_reset_position(void) {
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);
}
bool qs_config_handle_event(const SDL_Event *event) {
if (event->type == s_save_event_type) {
if (qs_config_write_file())
@@ -982,5 +1317,33 @@ bool qs_config_handle_event(const SDL_Event *event) {
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;
}
return false;
}
+55 -6
View File
@@ -66,15 +66,42 @@ void qs_config_request_set_language(int language_index);
// player's current real head pose instead.
void qs_config_request_reset_position(void);
// Returns true and handles the event if it's one of the six custom events
// Same cross-thread-dispatch reasoning as the four above - called from the
// Video Options tab's "Initial Screen"/"Game Screen" width/distance
// preset rows (MenuOverlay.java). preset_index indexes qs_config.c's own
// shared, offset+step-generated width/distance preset lists (both in
// meters; the offset/step themselves are config-file-only, not exposed
// as buttons), read back via the four qs_config_current_*_screen_*_index()
// getters below. Applies live immediately if the just-changed profile is
// the currently active one (see CONFIG.md) - otherwise just updates the
// stored preference, invisibly, until that profile becomes active (e.g.
// tweaking "Initial Screen" mid-game).
void qs_config_request_set_initial_screen_width(int preset_index);
void qs_config_request_set_initial_screen_distance(int preset_index);
void qs_config_request_set_game_screen_width(int preset_index);
void qs_config_request_set_game_screen_distance(int preset_index);
// Returns true and handles the event if it's one of the ten custom events
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()/
// _set_language()/_reset_position() 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 six custom types have even been registered.
// _set_language()/_reset_position()/the four _set_*_screen_*() setters
// 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 ten custom types have even been registered.
bool qs_config_handle_event(const SDL_Event *event);
// Called directly (no event-queue dispatch - see
// android/engine-patches/22-android-screen-profile.patch and this
// function's own definition in qs_config.c for why) from the engine's
// mainloop.c whenever the loop mode changes, and once from
// qs_config_load_startup() - picks between the "Initial Screen" (menu/
// intro) and "Game Screen" (actual gameplay) presets based on new_mode
// (mainloop.h's SETUP_LOOP/CUTSCENE_LOOP vs. everything else) and applies
// the newly-active one's shape to xr_session.c, but only if it's actually
// different from what was already active.
void qs_screen_profile_notify_loop_mode(short new_mode);
// Read-only queries for the Video Options tab's own selection highlight
// (MenuOverlay.java re-reads these whenever the tab is (re)shown, so it
// reflects the live value even if it was last changed some other way, e.g.
@@ -87,6 +114,28 @@ bool qs_config_handle_event(const SDL_Event *event);
bool qs_config_current_renderer_is_opengl(void);
int qs_config_current_resolution_index(void);
// -1 (not just an unlikely fallback, a routine, expected result here -
// unlike qs_config_current_resolution_index() above) if the profile's
// current value doesn't exactly match one of qs_config.c's own
// offset+step-generated width/distance presets, e.g. after a hand-edited
// config file sets an in-between value - MenuOverlay.java's own
// setSelected() naturally shows no button selected in that case, no
// special-casing needed.
int qs_config_current_initial_screen_width_index(void);
int qs_config_current_initial_screen_distance_index(void);
int qs_config_current_game_screen_width_index(void);
int qs_config_current_game_screen_distance_index(void);
// Returns the value preset button `index` (0..4) currently shows for the
// width/distance option - shared by both "Initial Screen" and "Game
// Screen" columns (their offset/step is config-file-only, not per-profile
// - see qs_config.c's own comment on the underlying statics), so
// MenuOverlay.java can label its preset buttons correctly even after a
// Load Configuration changes the offset/step. Index is clamped into
// range, not validated - every index 0..4 always has a value.
float qs_config_screen_width_preset_value(int index);
float qs_config_screen_distance_preset_value(int index);
// gShockPrefs.goLanguage (0 English/1 French/2 German, Prefs.h) - read by
// MenuOverlay.java's/KeyboardOverlay.java's periodic header-refresh timer
// to pick which language's chrome text (Strings.java) to display, so a
@@ -132,6 +132,33 @@ Java_de_ladkau_questshock_MenuOverlay_nativeRequestResetPosition(JNIEnv *env, jc
qs_config_request_reset_position();
}
// MenuOverlay's Video Options tab - "Initial Screen"/"Game Screen" width/
// distance preset rows, same enqueue-only reasoning as above.
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetInitialScreenWidth(JNIEnv *env, jclass clazz,
jint presetIndex) {
qs_config_request_set_initial_screen_width(presetIndex);
}
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetGameScreenWidth(JNIEnv *env, jclass clazz,
jint presetIndex) {
qs_config_request_set_game_screen_width(presetIndex);
}
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetInitialScreenDistance(JNIEnv *env,
jclass clazz,
jint presetIndex) {
qs_config_request_set_initial_screen_distance(presetIndex);
}
JNIEXPORT void JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetGameScreenDistance(JNIEnv *env, jclass clazz,
jint presetIndex) {
qs_config_request_set_game_screen_distance(presetIndex);
}
// Read-only queries - safe to call directly (no enqueue needed, see
// qs_config.h's own comment on each of these).
JNIEXPORT jboolean JNICALL
@@ -144,6 +171,42 @@ Java_de_ladkau_questshock_MenuOverlay_nativeGetResolutionIndex(JNIEnv *env, jcla
return qs_config_current_resolution_index();
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetInitialScreenWidthIndex(JNIEnv *env, jclass clazz) {
return qs_config_current_initial_screen_width_index();
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetGameScreenWidthIndex(JNIEnv *env, jclass clazz) {
return qs_config_current_game_screen_width_index();
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetInitialScreenDistanceIndex(JNIEnv *env,
jclass clazz) {
return qs_config_current_initial_screen_distance_index();
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetGameScreenDistanceIndex(JNIEnv *env, jclass clazz) {
return qs_config_current_game_screen_distance_index();
}
// Preset button *labels* (not the current value) - shared by both
// "Initial Screen" and "Game Screen" columns, since their offset/step is
// config-file-only, not per-profile (see qs_config.h's own comment).
JNIEXPORT jfloat JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetScreenWidthPresetValue(JNIEnv *env, jclass clazz,
jint index) {
return qs_config_screen_width_preset_value(index);
}
JNIEXPORT jfloat JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetScreenDistancePresetValue(JNIEnv *env, jclass clazz,
jint index) {
return qs_config_screen_distance_preset_value(index);
}
JNIEXPORT jint JNICALL
Java_de_ladkau_questshock_MenuOverlay_nativeGetLanguage(JNIEnv *env, jclass clazz) {
return qs_config_current_language();
+76 -14
View File
@@ -48,7 +48,7 @@ static const float kHandColor[2][4] = {
// MenuOverlay.HEIGHT).
#define KEYBOARD_TITLE_BAR_V_FRACTION (90.0f / 768.0f)
#define KEYBOARD_CLOSE_BUTTON_U_FRACTION (1.0f - 160.0f / 1024.0f)
#define MENU_TITLE_BAR_V_FRACTION (56.0f / 552.0f)
#define MENU_TITLE_BAR_V_FRACTION (56.0f / 800.0f)
#define MENU_DRAG_HANDLE_U_FRACTION 1.0f
static XrInstance g_instance = XR_NULL_HANDLE;
@@ -642,35 +642,97 @@ static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float
g_beam_quad_valid[hand] = true;
}
// Hit-tests this hand's ray against the game quad (xr_get_game_quad_extent())
// Hit-tests this hand's ray against the game screen (xr_get_game_quad_extent())
// - the lowest-priority target, tried only once neither the keyboard nor
// menu overlay claimed the ray this frame (or, symmetrically, once this
// hand already has a pending mouse-down on the game quad - see
// hand already has a pending mouse-down on the game screen - see
// g_game_touch_active and the keyboard/menu call sites in
// xr_input_sync_and_draw()). outU/outV (only meaningful when the
// returned hit is true) let the caller drive the game's own mouse cursor
// via xr_mouse.h. Returns plain `hit` - drag/touch dispatch for actual
// clicks is the caller's responsibility (xr_mouse_click()), not this
// function's, unlike xr_input_try_overlay().
//
// Two shapes: a flat plane (centralAngle == 0, the default and the only
// shape this function supported before screen curving existed - kept
// byte-for-byte unchanged as its own branch below, so nobody who never
// touches the new Video Options curve setting sees any change at all), or
// a curved cylinder segment. The curve is horizontal-only (like a real
// curved monitor) - the cylinder's vertical axis runs parallel to the
// screen's own local Y at (x=0, z=Zc) for every y, so y drops out of the
// lateral-surface intersection test entirely; halfHeight bounds the
// vertical extent exactly like the flat case, unchanged.
static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, float fx, float fy,
float fz, bool *outPlaneHit, float *outPlaneDistance,
float *outU, float *outV) {
float distance, halfWidth, halfHeight;
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);
float distance, halfWidth, halfHeight, radius, centralAngle;
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight, &radius, &centralAngle);
bool planeHit = false, hit = false;
float u = 0.0f, v = 0.0f, t = 0.0f;
if (fabsf(fz) > 1e-5f) {
t = (-distance - location->pose.position.z) / fz;
if (t > 0.0f) {
planeHit = true;
float cx = (location->pose.position.x + t * fx) / halfWidth;
float cy = (location->pose.position.y + t * fy) / halfHeight;
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
u = (cx + 1.0f) * 0.5f;
v = (1.0f - cy) * 0.5f;
if (centralAngle <= 0.0f) {
if (fabsf(fz) > 1e-5f) {
t = (-distance - location->pose.position.z) / fz;
if (t > 0.0f) {
planeHit = true;
float cx = (location->pose.position.x + t * fx) / halfWidth;
float cy = (location->pose.position.y + t * fy) / halfHeight;
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
u = (cx + 1.0f) * 0.5f;
v = (1.0f - cy) * 0.5f;
}
}
} else {
// Zc: the cylinder's axis position along Z (see xr_session.c's
// own concavity-derivation comment - same offset the render path
// uses to place the actual XrCompositionLayerCylinderKHR, so the
// two can never disagree about where the surface actually is).
float Zc = -distance + radius;
float x0 = location->pose.position.x;
float dz0 = location->pose.position.z - Zc;
// Ray-vs-infinite-cylinder quadratic (only x/z participate, per
// this function's own comment above) - A<=~0 means the ray runs
// parallel to the axis (never crosses the lateral surface),
// mirroring the flat case's fabsf(fz)>1e-5f guard.
float A = fx * fx + fz * fz;
if (A > 1e-8f) {
float B = 2.0f * (fx * x0 + fz * dz0);
float C = x0 * x0 + dz0 * dz0 - radius * radius;
float disc = B * B - 4.0f * A * C;
if (disc >= 0.0f) {
float sq = sqrtf(disc);
// Smallest positive root - the near intersection, correct
// both for the normal case (viewer inside the cylinder,
// so exactly one root is positive) and if the viewer ever
// ends up outside it.
float t1 = (-B - sq) / (2.0f * A);
float t2 = (-B + sq) / (2.0f * A);
if (t1 > 0.0f)
t = t1;
else if (t2 > 0.0f)
t = t2;
if (t > 0.0f) {
planeHit = true;
float px = location->pose.position.x + t * fx;
float py = location->pose.position.y + t * fy;
float pz = location->pose.position.z + t * fz;
// Signed angle from center (angle 0 is dead ahead,
// matching the flat case's cx==0 being dead center) -
// bounded by the configured centralAngle instead of a
// halfWidth.
float phi = atan2f(px, Zc - pz);
float halfAngle = centralAngle * 0.5f;
float cy = py / halfHeight;
hit = fabsf(phi) <= halfAngle && fabsf(cy) <= 1.0f;
u = (phi / halfAngle + 1.0f) * 0.5f;
v = (1.0f - cy) * 0.5f;
}
}
}
}
*outPlaneHit = planeHit;
*outPlaneDistance = t;
*outU = u;
+133 -41
View File
@@ -26,14 +26,18 @@
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
// Fixed pose/size for the single game quad - 2m in front of the local space
// origin, sized to preserve the game's own aspect ratio at a comfortable
// visual angle. No per-eye view/projection work is needed for a quad layer
// at all - the compositor handles reprojecting it per eye itself, which is
// exactly why this milestone doesn't need xrLocateViews or any stereo
// rendering (see the plan's step A notes).
// Original, pre-screen-profile-feature default distance for the game
// screen - 2m in front of the local space origin - now just the initial
// value of g_screen_distance_m (below) rather than a fixed constant.
// Width/curve/distance are all configurable (see g_screen_width_m/
// g_screen_curve_deg/g_screen_distance_m above and CONFIG.md's Video
// Options "Initial Screen"/"Game Screen" settings), sized to preserve the
// game's own pixel aspect ratio at whatever physical width is currently
// configured. No per-eye view/projection work is needed for a quad/
// cylinder layer at all - the compositor handles reprojecting it per eye
// itself, which is exactly why this milestone doesn't need xrLocateViews
// or any stereo rendering (see the plan's step A notes).
#define QUAD_DISTANCE_METERS 2.0f
#define QUAD_WIDTH_METERS 1.6f
// The menu launcher (just a "Keyboard" button - see MenuOverlay.java) is
// small and sits above where the keyboard defaults to; the keyboard keeps
@@ -41,14 +45,16 @@
// xr_overlay.h) so both can be shown/hidden/dragged separately - deliberately
// positioned apart by default so they don't start out overlapping.
#define MENU_JAVA_CLASS "de.ladkau.questshock.MenuOverlay"
// 768x552 (was 512x256, then 768x512) - grown to fit the header/tabs/Video
// Options content added on top of the original single "Keyboard" button,
// then again to fit the stats ribbon below the tab bar; taller in world
// space too since MENU_WIDTH_METERS (the physical size) stays fixed and
// height_m is derived from it by aspect ratio (see xr_overlay.c's
// xr_overlay_init()).
// 768x800 (was 512x256, then 768x512, then 768x552) - grown to fit the
// header/tabs/Video Options content added on top of the original single
// "Keyboard" button, then again to fit the stats ribbon below the tab
// bar, then again to fit the Video Options tab's screen-size grid; taller
// in world space too since MENU_WIDTH_METERS (the physical size) stays
// fixed and height_m is derived from it by aspect ratio (see
// xr_overlay.c's xr_overlay_init()). Must match MenuOverlay.java's own
// WIDTH/HEIGHT, and xr_input.c's MENU_TITLE_BAR_V_FRACTION denominator.
#define MENU_WIDTH 768
#define MENU_HEIGHT 552
#define MENU_HEIGHT 800
#define MENU_DISTANCE_METERS 1.3f
// Physical size - was 0.35m, too small to read comfortably at
// MENU_DISTANCE_METERS given how much text the header/tabs/stats ribbon
@@ -127,6 +133,24 @@ static PFN_xrQueryPerformanceMetricsCounterMETA g_xrQueryPerformanceMetricsCount
static XrPath g_cpu_utilization_path = XR_NULL_PATH;
static XrPath g_gpu_utilization_path = XR_NULL_PATH;
// XR_KHR_composition_layer_cylinder support - same "only if the runtime
// advertised it" reasoning as g_performance_metrics_supported above. If
// unsupported, the curve is forced to 0 (flat) everywhere regardless of
// configuration (see xr_get_game_quad_extent()) so the game screen just
// silently stays flat instead of failing to submit a layer at all.
static bool g_cylinder_layer_supported = false;
// The game screen's currently-configured shape (see qs_config.c's
// qs_screen_profile_notify_loop_mode(), which is what actually calls
// xr_session_set_game_screen_shape() below - this file has no notion of
// "which profile" is active, only "what shape is current"). Defaults
// match the game quad's own original, pre-this-feature hardcoded width
// (1.6m) and lack of curve, so a build that never touches the new config
// options renders identically to before.
static float g_screen_width_m = 1.6f;
static float g_screen_curve_deg = 0.0f;
static float g_screen_distance_m = QUAD_DISTANCE_METERS;
static bool xr_check(XrResult result, const char *what) {
if (XR_SUCCEEDED(result))
return true;
@@ -355,12 +379,24 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
g_performance_metrics_supported =
xr_instance_extension_supported(XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
g_cylinder_layer_supported =
xr_instance_extension_supported(XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME);
const char *extensions[3] = {
// The two KHR extensions here are always required; the other two are
// each independently optional device capabilities (see
// g_performance_metrics_supported/g_cylinder_layer_supported's own
// comments) - appended only if actually advertised, since listing an
// unsupported extension fails xrCreateInstance entirely, not just that
// one feature.
const char *extensions[4] = {
XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME,
XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME,
XR_META_PERFORMANCE_METRICS_EXTENSION_NAME,
};
uint32_t extensionCount = 2;
if (g_performance_metrics_supported)
extensions[extensionCount++] = XR_META_PERFORMANCE_METRICS_EXTENSION_NAME;
if (g_cylinder_layer_supported)
extensions[extensionCount++] = XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME;
XrInstanceCreateInfoAndroidKHR androidInfo = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
androidInfo.applicationVM = vm;
@@ -368,7 +404,7 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO};
createInfo.next = &androidInfo;
createInfo.enabledExtensionCount = g_performance_metrics_supported ? 3 : 2;
createInfo.enabledExtensionCount = extensionCount;
createInfo.enabledExtensionNames = extensions;
strncpy(createInfo.applicationInfo.applicationName, "QuestShock",
XR_MAX_APPLICATION_NAME_SIZE - 1);
@@ -663,30 +699,54 @@ void xr_frame_end(void) {
if (!xr_is_session_running())
return;
// Up to 5 layers: the game quad (if a frame was actually rendered),
// the menu launcher and keyboard overlays while visible - each gets
// its own acquire/render/release cycle against its own swapchain (see
// xr_overlay_render_and_build_layer()) any time before xrEndFrame,
// unlike the game quad there's no per-frame engine rendering to wrap
// around here, just each overlay's own blit - and, per hand, a
// laser-beam quad while that hand's ray is aimed at the keyboard,
// menu, or the game quad itself (see xr_input_get_beam_layer()).
// Up to 5 layers: the game screen (if a frame was actually rendered) -
// a flat quad, or a curved cylinder if the currently-active screen
// profile has a nonzero curve (see xr_get_game_quad_extent()'s own
// comment; xr_input.c's xr_input_try_game_quad() hit-tests whichever
// shape is actually submitted here, always agreeing since both read
// the same getter) - the menu launcher and keyboard overlays while
// visible - each gets its own acquire/render/release cycle against
// its own swapchain (see xr_overlay_render_and_build_layer()) any
// time before xrEndFrame, unlike the game screen there's no per-frame
// engine rendering to wrap around here, just each overlay's own blit -
// and, per hand, a laser-beam quad while that hand's ray is aimed at
// the keyboard, menu, or the game screen itself (see
// xr_input_get_beam_layer()).
float gameDistance, gameHalfWidth, gameHalfHeight, gameRadius, gameCentralAngle;
xr_get_game_quad_extent(&gameDistance, &gameHalfWidth, &gameHalfHeight, &gameRadius,
&gameCentralAngle);
bool gameScreenCurved = gameCentralAngle > 0.0f;
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
gameQuad.space = g_play_space;
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height;
gameQuad.pose.orientation.w = 1.0f;
gameQuad.pose.position.z = -QUAD_DISTANCE_METERS;
gameQuad.size.width = QUAD_WIDTH_METERS;
gameQuad.size.height =
QUAD_WIDTH_METERS * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
XrCompositionLayerCylinderKHR gameCylinder = {XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR};
if (gameScreenCurved) {
gameCylinder.space = g_play_space;
gameCylinder.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
gameCylinder.subImage.swapchain = g_game_swapchain.swapchain;
gameCylinder.subImage.imageRect.extent.width = g_game_swapchain.width;
gameCylinder.subImage.imageRect.extent.height = g_game_swapchain.height;
gameCylinder.pose.orientation.w = 1.0f;
gameCylinder.pose.position.z = -gameDistance;
gameCylinder.radius = gameRadius;
gameCylinder.centralAngle = gameCentralAngle;
gameCylinder.aspectRatio = (float)g_game_swapchain.width / (float)g_game_swapchain.height;
} else {
gameQuad.space = g_play_space;
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height;
gameQuad.pose.orientation.w = 1.0f;
gameQuad.pose.position.z = -gameDistance;
gameQuad.size.width = gameHalfWidth * 2.0f;
gameQuad.size.height = gameHalfHeight * 2.0f;
}
const XrCompositionLayerBaseHeader *layers[5];
uint32_t layerCount = 0;
if (g_have_acquired_game_image)
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad;
layers[layerCount++] = gameScreenCurved ? (XrCompositionLayerBaseHeader *)&gameCylinder
: (XrCompositionLayerBaseHeader *)&gameQuad;
XrCompositionLayerQuad menuQuad;
if (g_frame_should_render && xr_overlay_render_and_build_layer(g_menu_overlay, &menuQuad)) {
@@ -770,13 +830,45 @@ void xr_shutdown(void) {
g_xrQueryPerformanceMetricsCounterMETA = NULL;
g_cpu_utilization_path = XR_NULL_PATH;
g_gpu_utilization_path = XR_NULL_PATH;
g_cylinder_layer_supported = false;
// g_screen_width_m/g_screen_curve_deg/g_screen_distance_m deliberately
// NOT reset here - same reasoning as xr_session_set_player_transform()'s
// own state: it's meant to persist across a shutdown/reinit within the
// same process (e.g. an Android lifecycle pause/resume), not just
// within one XR session.
}
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m) {
*distance_m = QUAD_DISTANCE_METERS;
*half_width_m = QUAD_WIDTH_METERS * 0.5f;
*half_height_m =
QUAD_WIDTH_METERS * 0.5f * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
void xr_session_set_game_screen_shape(float width_m, float curve_deg, float distance_m) {
g_screen_width_m = width_m;
g_screen_curve_deg = curve_deg;
g_screen_distance_m = distance_m;
}
// Single source of truth for the game screen's current shape, read by
// both xr_frame_end() (to build/submit the actual layer) and
// xr_input_try_game_quad() (xr_input.c, to hit-test the laser pointer
// against it). half_width_m is meaningful only when *central_angle_rad is
// 0 (flat); a curved screen's horizontal extent is bounded by
// *central_angle_rad instead, with *half_height_m always the vertical
// bound either way.
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m,
float *radius_m, float *central_angle_rad) {
float curveDeg = g_cylinder_layer_supported ? g_screen_curve_deg : 0.0f;
float aspect = (float)g_game_swapchain.height / (float)g_game_swapchain.width;
*distance_m = g_screen_distance_m;
*half_width_m = g_screen_width_m * 0.5f;
*half_height_m = g_screen_width_m * 0.5f * aspect;
if (curveDeg > 0.0f) {
float theta = curveDeg * QS_DEG_TO_RAD;
*central_angle_rad = theta;
*radius_m = g_screen_width_m / theta;
} else {
*central_angle_rad = 0.0f;
*radius_m = 0.0f;
}
}
void xr_get_game_resolution(int *width, int *height) {
+22 -3
View File
@@ -68,11 +68,30 @@ void xr_frame_end(void);
void xr_shutdown(void);
// Play-space geometry of the single game quad xr_frame_end() submits
// Play-space geometry of the single game screen xr_frame_end() submits
// (distance in front of the play space origin, half-width/half-height,
// all in meters) - shared with xr_input.c's ray/quad hit-testing so the
// laser pointer always matches whatever's actually visible.
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m);
// laser pointer always matches whatever's actually visible. *radius_m/
// *central_angle_rad are 0 when flat, in which case *half_width_m is the
// horizontal extent; otherwise the screen is a curved cylinder segment
// and the caller uses *central_angle_rad instead of *half_width_m for the
// horizontal bound (*half_height_m stays meaningful either way). Curving
// is currently unused - qs_config.c has no config surface for it - but
// kept working so it can be re-enabled later.
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m,
float *radius_m, float *central_angle_rad);
// Sets the game screen's current physical width (meters) and curve (wrap
// angle in degrees, 0 = flat) - a plain state setter, no XR calls, same
// category as xr_session_set_player_transform() above (safe to call
// before xr_init() has even run). Read by both xr_frame_end() (to build/
// submit the actual layer) and, via xr_get_game_quad_extent() above,
// xr_input.c's hit-testing. Driven by qs_config.c's
// qs_screen_profile_notify_loop_mode(), which decides which of the
// "Initial Screen"/"Game Screen" config profiles (see CONFIG.md) is
// currently active - this function itself has no notion of "profiles",
// only "what shape is current".
void xr_session_set_game_screen_shape(float width_m, float curve_deg, float distance_m);
// The engine's current screen resolution (gScreenWide/gScreenHigh,
// InitMac.h) - not the game quad's own swapchain size, which is fixed at
@@ -46,17 +46,28 @@ import java.util.Locale;
public class MenuOverlay extends OverlayPanel {
// Grown from the original 512x256 (still just a single "Keyboard"
// button back then) to fit the header/tabs/Video Options content added
// since, then again to fit the stats ribbon below the tab bar - see
// since, then again to fit the stats ribbon below the tab bar, then
// again to fit the screen-size grid (width/distance rows) - see
// xr_session.c's matching MENU_WIDTH/MENU_HEIGHT and xr_input.c's
// matching MENU_TITLE_BAR_V_FRACTION.
private static final int WIDTH = 768;
private static final int HEIGHT = 552;
private static final int HEIGHT = 800;
private static final int HEADER_HEIGHT = 56;
private static final int TAB_BAR_HEIGHT = 48;
private static final int STATS_BAR_HEIGHT = 40;
private static final long STATS_REFRESH_MS = 500;
private static final String[] RESOLUTION_LABELS = {"320x200", "320x400", "640x400", "640x480", "1024x768"};
// Construction-time placeholder text only, matching qs_config.c's own
// default offset/step - buildVideoOptionsTab() below uses these just
// to give the screen-profile preset buttons *some* label before the
// tab is ever shown; refreshVideoOptionsSelection()'s refreshPresetRow()
// immediately overwrites them with the real, possibly config-file-
// adjusted values once the tab is (re)shown (switchTab() always calls
// it first), so these never need to change even if the corresponding
// qs_config.c defaults ever do.
private static final String[] SCREEN_WIDTH_LABELS = {"1.0m", "1.6m", "2.2m", "2.8m", "3.4m"};
private static final String[] SCREEN_DISTANCE_LABELS = {"1.0m", "1.5m", "2.0m", "2.5m", "3.0m"};
// Order must match qs_config.c's kLanguages[]/gShockPrefs.goLanguage's
// own numbering (Prefs.h: 0 English/1 French/2 German) - also the keys
// this same language's own Strings.java translation is filed under.
@@ -110,6 +121,19 @@ public class MenuOverlay extends OverlayPanel {
private Button openglButton;
private Button[] resolutionButtons;
// Video Options tab's screen-size grid (two columns - Initial Screen/
// Game Screen - each with a Width row and a Distance row)
private TextView initialScreenTitleView;
private TextView initialScreenWidthLabel;
private TextView initialScreenDistanceLabel;
private Button[] initialScreenWidthButtons;
private Button[] initialScreenDistanceButtons;
private TextView gameScreenTitleView;
private TextView gameScreenWidthLabel;
private TextView gameScreenDistanceLabel;
private Button[] gameScreenWidthButtons;
private Button[] gameScreenDistanceButtons;
private int currentLanguage = Strings.ENGLISH;
private final Handler handler = new Handler(Looper.getMainLooper());
@@ -306,6 +330,108 @@ public class MenuOverlay extends OverlayPanel {
}
tab.addView(resolutionRow);
// Two columns side by side (Initial Screen / Game Screen) rather
// than 4 fully-stacked rows - keeps the added vertical space to
// roughly 2 row-heights instead of 4. See CONFIG.md's Video
// Options "Initial Screen"/"Game Screen" settings.
LinearLayout screenColumns = new LinearLayout(activity);
screenColumns.setOrientation(LinearLayout.HORIZONTAL);
screenColumns.setPadding(0, 16, 0, 0);
LinearLayout initialColumn = new LinearLayout(activity);
initialColumn.setOrientation(LinearLayout.VERTICAL);
initialScreenTitleView = new TextView(activity);
initialScreenTitleView.setTextColor(0xFFFFFFFF);
initialScreenTitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
initialScreenTitleView.setText(Strings.get("initial_screen", currentLanguage));
initialColumn.addView(initialScreenTitleView);
initialScreenWidthLabel = new TextView(activity);
initialScreenWidthLabel.setTextColor(0xFFFFFFFF);
initialScreenWidthLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
initialScreenWidthLabel.setPadding(0, 8, 0, 0);
initialScreenWidthLabel.setText(Strings.get("width", currentLanguage));
initialColumn.addView(initialScreenWidthLabel);
LinearLayout initialWidthRow = new LinearLayout(activity);
initialWidthRow.setOrientation(LinearLayout.HORIZONTAL);
initialScreenWidthButtons = new Button[SCREEN_WIDTH_LABELS.length];
for (int i = 0; i < SCREEN_WIDTH_LABELS.length; i++) {
final int index = i;
initialScreenWidthButtons[i] = addSelectableButton(initialWidthRow, SCREEN_WIDTH_LABELS[i],
v -> nativeRequestSetInitialScreenWidth(index));
}
initialColumn.addView(initialWidthRow);
initialScreenDistanceLabel = new TextView(activity);
initialScreenDistanceLabel.setTextColor(0xFFFFFFFF);
initialScreenDistanceLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
initialScreenDistanceLabel.setPadding(0, 8, 0, 0);
initialScreenDistanceLabel.setText(Strings.get("distance", currentLanguage));
initialColumn.addView(initialScreenDistanceLabel);
LinearLayout initialDistanceRow = new LinearLayout(activity);
initialDistanceRow.setOrientation(LinearLayout.HORIZONTAL);
initialScreenDistanceButtons = new Button[SCREEN_DISTANCE_LABELS.length];
for (int i = 0; i < SCREEN_DISTANCE_LABELS.length; i++) {
final int index = i;
initialScreenDistanceButtons[i] = addSelectableButton(initialDistanceRow,
SCREEN_DISTANCE_LABELS[i], v -> nativeRequestSetInitialScreenDistance(index));
}
initialColumn.addView(initialDistanceRow);
screenColumns.addView(initialColumn, new LinearLayout.LayoutParams(
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
LinearLayout gameColumn = new LinearLayout(activity);
gameColumn.setOrientation(LinearLayout.VERTICAL);
gameScreenTitleView = new TextView(activity);
gameScreenTitleView.setTextColor(0xFFFFFFFF);
gameScreenTitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
gameScreenTitleView.setText(Strings.get("game_screen", currentLanguage));
gameColumn.addView(gameScreenTitleView);
gameScreenWidthLabel = new TextView(activity);
gameScreenWidthLabel.setTextColor(0xFFFFFFFF);
gameScreenWidthLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
gameScreenWidthLabel.setPadding(0, 8, 0, 0);
gameScreenWidthLabel.setText(Strings.get("width", currentLanguage));
gameColumn.addView(gameScreenWidthLabel);
LinearLayout gameWidthRow = new LinearLayout(activity);
gameWidthRow.setOrientation(LinearLayout.HORIZONTAL);
gameScreenWidthButtons = new Button[SCREEN_WIDTH_LABELS.length];
for (int i = 0; i < SCREEN_WIDTH_LABELS.length; i++) {
final int index = i;
gameScreenWidthButtons[i] = addSelectableButton(gameWidthRow, SCREEN_WIDTH_LABELS[i],
v -> nativeRequestSetGameScreenWidth(index));
}
gameColumn.addView(gameWidthRow);
gameScreenDistanceLabel = new TextView(activity);
gameScreenDistanceLabel.setTextColor(0xFFFFFFFF);
gameScreenDistanceLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
gameScreenDistanceLabel.setPadding(0, 8, 0, 0);
gameScreenDistanceLabel.setText(Strings.get("distance", currentLanguage));
gameColumn.addView(gameScreenDistanceLabel);
LinearLayout gameDistanceRow = new LinearLayout(activity);
gameDistanceRow.setOrientation(LinearLayout.HORIZONTAL);
gameScreenDistanceButtons = new Button[SCREEN_DISTANCE_LABELS.length];
for (int i = 0; i < SCREEN_DISTANCE_LABELS.length; i++) {
final int index = i;
gameScreenDistanceButtons[i] = addSelectableButton(gameDistanceRow,
SCREEN_DISTANCE_LABELS[i], v -> nativeRequestSetGameScreenDistance(index));
}
gameColumn.addView(gameDistanceRow);
screenColumns.addView(gameColumn, new LinearLayout.LayoutParams(
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
tab.addView(screenColumns);
return tab;
}
@@ -438,6 +564,41 @@ public class MenuOverlay extends OverlayPanel {
for (int i = 0; i < resolutionButtons.length; i++) {
resolutionButtons[i].setSelected(i == index);
}
// -1 (no exact preset match, e.g. a hand-edited config value) just
// means no button in the row ends up selected - setSelected(false)
// for every entry, nothing special-cased. Each row's *labels* are
// also refreshed here, not just fixed at construction time - the
// offset/step generating them is config-file-only (see
// qs_config.h's own comment) and can change under Load
// Configuration while this tab is open.
refreshPresetRow(initialScreenWidthButtons, nativeGetInitialScreenWidthIndex(),
MenuOverlay::nativeGetScreenWidthPresetValue);
refreshPresetRow(initialScreenDistanceButtons, nativeGetInitialScreenDistanceIndex(),
MenuOverlay::nativeGetScreenDistancePresetValue);
refreshPresetRow(gameScreenWidthButtons, nativeGetGameScreenWidthIndex(),
MenuOverlay::nativeGetScreenWidthPresetValue);
refreshPresetRow(gameScreenDistanceButtons, nativeGetGameScreenDistanceIndex(),
MenuOverlay::nativeGetScreenDistancePresetValue);
}
// Functional interface for the two nativeGetScreen*PresetValue()
// getters below - each takes a preset index (0..4) and returns that
// preset's current value, shared by both "Initial Screen" and "Game
// Screen" columns (only which button is selected differs per column).
private interface PresetValueGetter {
float get(int index);
}
private void refreshPresetRow(Button[] buttons, int selectedIndex, PresetValueGetter valueGetter) {
for (int i = 0; i < buttons.length; i++) {
buttons[i].setText(formatMeters(valueGetter.get(i)));
buttons[i].setSelected(i == selectedIndex);
}
}
private static String formatMeters(float meters) {
return String.format(Locale.US, "%.1fm", meters);
}
// Same reasoning as refreshVideoOptionsSelection() above, for the Main
@@ -531,6 +692,16 @@ public class MenuOverlay extends OverlayPanel {
for (int i = 0; i < languageButtons.length; i++) {
languageButtons[i].setText(Strings.get(LANGUAGE_KEYS[i], currentLanguage));
}
// The screen-profile preset buttons' own labels are never
// translated (plain measurements, like RESOLUTION_LABELS above) -
// refreshVideoOptionsSelection()'s refreshPresetRow() keeps them
// current independently of language.
initialScreenTitleView.setText(Strings.get("initial_screen", currentLanguage));
initialScreenWidthLabel.setText(Strings.get("width", currentLanguage));
initialScreenDistanceLabel.setText(Strings.get("distance", currentLanguage));
gameScreenTitleView.setText(Strings.get("game_screen", currentLanguage));
gameScreenWidthLabel.setText(Strings.get("width", currentLanguage));
gameScreenDistanceLabel.setText(Strings.get("distance", currentLanguage));
}
// "Dismiss" on the restart dialog - the live-appliable subset was
@@ -584,11 +755,31 @@ public class MenuOverlay extends OverlayPanel {
// xr_input_recenter_player_to_screen()) - a live-only action, unrelated
// to questshock.config.json.
private static native void nativeRequestResetPosition();
// Video Options tab's "Initial Screen"/"Game Screen" width/distance
// preset rows - same enqueue-only reasoning as above. presetIndex
// (0..4) selects one of qs_config.c's own offset+step-generated
// presets (nativeGetScreen*PresetValue() below returns the actual
// value, for the button's label).
private static native void nativeRequestSetInitialScreenWidth(int presetIndex);
private static native void nativeRequestSetInitialScreenDistance(int presetIndex);
private static native void nativeRequestSetGameScreenWidth(int presetIndex);
private static native void nativeRequestSetGameScreenDistance(int presetIndex);
// Read-only queries - safe to call directly, see qs_config.h's own
// comment on each of these.
private static native boolean nativeGetRendererIsOpenGl();
private static native int nativeGetResolutionIndex();
// -1 = no exact preset match (see refreshVideoOptionsSelection()).
private static native int nativeGetInitialScreenWidthIndex();
private static native int nativeGetInitialScreenDistanceIndex();
private static native int nativeGetGameScreenWidthIndex();
private static native int nativeGetGameScreenDistanceIndex();
// Preset button *labels* (not the current value) - shared by both
// "Initial Screen" and "Game Screen" columns, since their offset/step
// is config-file-only, not per-profile (see qs_config.h's own
// comment). Index is clamped into range, not validated.
private static native float nativeGetScreenWidthPresetValue(int index);
private static native float nativeGetScreenDistancePresetValue(int index);
// Package-visible (not private): KeyboardOverlay also calls this
// directly, once, at its own construction time, to show the right
// language from the start rather than waiting for this class's own
@@ -23,7 +23,7 @@ import org.libsdl.app.SDLSurface;
* SDL_RWops, so Android's asset-manager fallback for SDL_RWFromFile
* doesn't apply here) expects to find it - see android/engine-patches/ for
* why this is done from Java instead of patching engine/ itself, which is
* never modified.
* never modified.q
*
* super.onCreate() (SDLActivity's) must always run first and unconditionally
* - Android throws SuperNotCalledException otherwise, checked right after
@@ -144,10 +144,17 @@ public class QuestShockActivity extends SDLActivity {
File marker = new File(gameDir, "GET_ASSETS_QUEST.txt");
if (!marker.exists()) {
copyAssetDir("shaders", new File(gameDir, "shaders"));
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"));
copyAssetFile("GET_ASSETS_QUEST.txt", marker);
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"), false);
copyAssetFile("GET_ASSETS_QUEST.txt", marker, false);
}
// Refreshed on every launch (unlike the marker-gated block above) so
// it always matches the installed build - qs_config.c reads this as
// the initial config only when questshock.config.json doesn't exist
// yet, never overwriting a user's own saved config.
copyAssetFile("questshock.config.default.json",
new File(gameDir, "questshock.config.default.json"), true);
// Shockolate's engine/ is never patched to check this itself (see the
// class doc above) - it just does plain fopen("res/data/...", ...)
// and, on a fresh install with no game data copied in yet, that
@@ -211,8 +218,8 @@ public class QuestShockActivity extends SDLActivity {
.show();
}
private void copyAssetFile(String assetPath, File dest) {
if (dest.exists()) {
private void copyAssetFile(String assetPath, File dest, boolean overwrite) {
if (dest.exists() && !overwrite) {
return;
}
File parent = dest.getParentFile();
@@ -246,7 +253,7 @@ public class QuestShockActivity extends SDLActivity {
}
destDir.mkdirs();
for (String entry : entries) {
copyAssetFile(assetDir + "/" + entry, new File(destDir, entry));
copyAssetFile(assetDir + "/" + entry, new File(destDir, entry), false);
}
}
}
@@ -50,6 +50,10 @@ final class Strings {
put("english", "English", "Anglais", "Englisch");
put("french", "French", "Français", "Französisch");
put("german", "German", "Allemand", "Deutsch");
put("initial_screen", "Initial Screen", "Écran initial", "Startbildschirm");
put("game_screen", "Game Screen", "Écran de jeu", "Spielbildschirm");
put("width", "Width", "Largeur", "Breite");
put("distance", "Distance", "Distance", "Abstand");
// "OpenGL" is a proper/brand name - not translated, same in every
// language, so it has no entry here; call sites just use it as a
// literal.
@@ -0,0 +1,30 @@
--- a/src/GameSrc/mainloop.c
+++ b/src/GameSrc/mainloop.c
@@ -57,6 +57,10 @@
#include "tools.h"
#include "wrapper.h"
+#ifdef __ANDROID__
+#include "qs_config.h"
+#endif
+
// how is the game doing, anyway, set to true at end of time
uchar cit_success = FALSE;
@@ -103,6 +107,16 @@
_last_mode = *cmode;
(*exit_modes[_last_mode])();
*cmode = _new_mode;
+#ifdef __ANDROID__
+ // Notifies qs_config.c of every loop-mode transition so it can pick
+ // between the "Initial Screen" and "Game Screen" virtual-screen
+ // presets - see qs_config.h/CONFIG.md. Called synchronously, directly,
+ // on this thread (mainloop()'s own) - unlike the UI-thread-triggered
+ // config actions elsewhere in qs_config.c, this doesn't need the
+ // cross-thread SDL-event dance since it already originates on the
+ // right thread.
+ qs_screen_profile_notify_loop_mode(*cmode);
+#endif
_static_change = 0;
if (*cmode >= 0)
(*enter_modes[*cmode])();
+2 -1
View File
@@ -81,7 +81,7 @@ fi
echo "prebuiltDir=$PREBUILT_DIR_PROP"
} > "$ANDROID_DIR/engine.properties"
echo "== Staging bundled assets (shaders, soundfont, get-assets text) =="
echo "== Staging bundled assets (shaders, soundfont, get-assets text, default config) =="
ASSETS_DIR="$ANDROID_DIR/app/src/main/assets"
rm -rf "$ASSETS_DIR"
mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res"
@@ -91,6 +91,7 @@ mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res"
cp -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/"
cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/soundfont.sf2"
cp "$REPO_ROOT/res/assets/GET_ASSETS_QUEST.txt" "$ASSETS_DIR/GET_ASSETS_QUEST.txt"
cp "$REPO_ROOT/res/questshock.config.default.json" "$ASSETS_DIR/questshock.config.default.json"
echo "== Staging prebuilt Android SDL2/SDL2_mixer/fluidsynth-lite/openxr .so's into jniLibs =="
# gl4es is deliberately absent here - it's no longer prebuilt, it's compiled
+17
View File
@@ -0,0 +1,17 @@
{
"video_options": {
"renderer": "opengl",
"resolution": "1024x768",
"initial_screen_width_m": 1.600000,
"initial_screen_distance_m": 2.000000,
"game_screen_width_m": 3.400000,
"game_screen_distance_m": 2.000000,
"screen_width_offset_m": 1.000000,
"screen_width_step_m": 0.600000,
"screen_distance_offset_m": 1.000000,
"screen_distance_step_m": 0.500000
},
"general": {
"language": "english"
}
}