Add configurable game/initial screen width and distance, with a bundled first-run config
build / build (push) Successful in 2m41s
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:
@@ -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
|
- **Format**: plain JSON, grouped by concern. Each top-level key is a
|
||||||
*group*; each key inside a group is one *option*.
|
*group*; each key inside a group is one *option*.
|
||||||
- **Loading**: read automatically at app startup, before the splash screen,
|
- **Loading**: read automatically at app startup, before the splash screen,
|
||||||
if the file exists. If it doesn't exist, nothing changes - the engine
|
if the file exists. If it doesn't, `questshock.config.default.json` (same
|
||||||
behaves exactly as it did before this file existed (driven by `prefs.txt`
|
folder) is used instead - a copy bundled with the app, refreshed on every
|
||||||
as usual).
|
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
|
- **Saving**: never automatic. In-headset, open the menu (controller menu
|
||||||
button) and press **Save Configuration** to write the file from your
|
button) and press **Save Configuration** to write the file from your
|
||||||
current live settings, overwriting it if present. Press **Load
|
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": {
|
"video_options": {
|
||||||
"renderer": "opengl",
|
"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": {
|
"general": {
|
||||||
"language": "english"
|
"language": "english"
|
||||||
@@ -78,6 +88,56 @@ The game's internal rendering resolution.
|
|||||||
- **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"`
|
- **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"`
|
||||||
- **Restart required?**: No (live-appliable)
|
- **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`)
|
## General (`general`)
|
||||||
|
|
||||||
### `language`
|
### `language`
|
||||||
|
|||||||
@@ -104,10 +104,13 @@ def stageEngineTask = tasks.register("stageEngine", Exec) {
|
|||||||
inputs.dir("${projectDir}/../gl4es-src")
|
inputs.dir("${projectDir}/../gl4es-src")
|
||||||
inputs.dir("${projectDir}/../gl4es-patches")
|
inputs.dir("${projectDir}/../gl4es-patches")
|
||||||
inputs.file("${repoRoot}/build-image/Dockerfile")
|
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-engine")
|
||||||
outputs.dir("${repoRoot}/build/android-gl4es")
|
outputs.dir("${repoRoot}/build/android-gl4es")
|
||||||
outputs.dir("${repoRoot}/build/android-prebuilt")
|
outputs.dir("${repoRoot}/build/android-prebuilt")
|
||||||
outputs.file("${projectDir}/../engine.properties")
|
outputs.file("${projectDir}/../engine.properties")
|
||||||
|
outputs.dir("${projectDir}/src/main/assets")
|
||||||
}
|
}
|
||||||
|
|
||||||
// preBuild is what every variant's compile/native-build tasks already
|
// preBuild is what every variant's compile/native-build tasks already
|
||||||
|
|||||||
@@ -28,6 +28,10 @@
|
|||||||
#include "tools.h"
|
#include "tools.h"
|
||||||
|
|
||||||
#define QS_CONFIG_FILENAME "questshock.config.json"
|
#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
|
// Minimal JSON parser
|
||||||
@@ -330,6 +334,27 @@ static void qs_json_take_number_field(QsJsonParser *p, float *out, bool *has) {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
char renderer[16]; // "" = not present in the file
|
char renderer[16]; // "" = not present in the file
|
||||||
char resolution[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;
|
} QsParsedVideoOptions;
|
||||||
|
|
||||||
typedef struct {
|
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));
|
qs_json_take_string_field(p, out->renderer, sizeof(out->renderer));
|
||||||
else if (strcmp(key, "resolution") == 0)
|
else if (strcmp(key, "resolution") == 0)
|
||||||
qs_json_take_string_field(p, out->resolution, sizeof(out->resolution));
|
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
|
else
|
||||||
qs_json_skip_value(p);
|
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);
|
qs_json_skip_value(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads and parses QS_CONFIG_FILENAME into *out. Returns false (with *out
|
// Reads and parses `filename` into *out. Returns false (with *out zeroed)
|
||||||
// zeroed) if the file doesn't exist, is empty, or fails to parse as valid
|
// if the file doesn't exist, is empty, or fails to parse as valid JSON -
|
||||||
// JSON - logging a WARN only for the last case, since a missing file is the
|
// logging a WARN only for the last case, since a missing file is the
|
||||||
// normal/expected state, not an error.
|
// 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));
|
memset(out, 0, sizeof(*out));
|
||||||
FILE *f = fopen(QS_CONFIG_FILENAME, "rb");
|
FILE *f = fopen(filename, "rb");
|
||||||
if (!f)
|
if (!f)
|
||||||
return false;
|
return false;
|
||||||
fseek(f, 0, SEEK_END);
|
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;
|
bool ok = qs_json_parse_object(&parser, qs_parse_top_level_field, out) && !parser.error;
|
||||||
free(buf);
|
free(buf);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
WARN("%s: malformed JSON - ignoring file", QS_CONFIG_FILENAME);
|
WARN("%s: malformed JSON - ignoring file", filename);
|
||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool qs_config_read_file(QsParsedConfig *out) {
|
||||||
|
return qs_config_read_file_named(QS_CONFIG_FILENAME, out);
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Video Options: renderer
|
// Video Options: renderer
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -531,6 +580,149 @@ static const char *qs_current_resolution(void) {
|
|||||||
return "640x480"; // shouldn't happen - safe fallback for Save
|
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
|
// General: language
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -697,6 +889,45 @@ int qs_config_current_resolution_index(void) {
|
|||||||
|
|
||||||
int qs_config_current_language(void) { return gShockPrefs.goLanguage; }
|
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
|
// 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)
|
if (changed && QS_RESOLUTION_REQUIRES_RESTART)
|
||||||
qs_append_restart_reason(restart_needed, restart_needed_size, "resolution");
|
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->has_general) {
|
||||||
if (parsed->general.language[0] != '\0') {
|
if (parsed->general.language[0] != '\0') {
|
||||||
@@ -758,7 +1024,15 @@ static bool qs_config_write_file(void) {
|
|||||||
fprintf(f, "{\n");
|
fprintf(f, "{\n");
|
||||||
fprintf(f, " \"video_options\": {\n");
|
fprintf(f, " \"video_options\": {\n");
|
||||||
fprintf(f, " \"renderer\": \"%s\",\n", qs_current_renderer());
|
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, " },\n");
|
||||||
fprintf(f, " \"general\": {\n");
|
fprintf(f, " \"general\": {\n");
|
||||||
fprintf(f, " \"language\": \"%s\"\n", qs_current_language_name());
|
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_resolution_event_type;
|
||||||
static Uint32 s_set_language_event_type;
|
static Uint32 s_set_language_event_type;
|
||||||
static Uint32 s_reset_position_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) {
|
void qs_config_load_startup(void) {
|
||||||
// Registers the custom SDL event types qs_config_request_save()/_load()/
|
// Registers the custom SDL event types qs_config_request_save()/_load()/
|
||||||
// _set_renderer()/_set_resolution()/_set_language()/_reset_position()
|
// _set_renderer()/_set_resolution()/_set_language()/_reset_position()/
|
||||||
// enqueue - done here, once, on the engine's own thread and before the
|
// the four _set_*_screen_*() screen-profile setters enqueue - done
|
||||||
// menu/its buttons could possibly exist yet, rather than lazily from
|
// here, once, on the engine's own thread and before the menu/its
|
||||||
// those functions themselves (which run on Android's UI thread and
|
// buttons could possibly exist yet, rather than lazily from those
|
||||||
// could otherwise race each other doing this lazily).
|
// functions themselves (which run on Android's UI thread and could
|
||||||
Uint32 base = SDL_RegisterEvents(6);
|
// otherwise race each other doing this lazily).
|
||||||
|
Uint32 base = SDL_RegisterEvents(10);
|
||||||
s_save_event_type = base;
|
s_save_event_type = base;
|
||||||
s_load_event_type = base + 1;
|
s_load_event_type = base + 1;
|
||||||
s_set_renderer_event_type = base + 2;
|
s_set_renderer_event_type = base + 2;
|
||||||
s_set_resolution_event_type = base + 3;
|
s_set_resolution_event_type = base + 3;
|
||||||
s_set_language_event_type = base + 4;
|
s_set_language_event_type = base + 4;
|
||||||
s_reset_position_event_type = base + 5;
|
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;
|
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);
|
qs_config_apply_parsed(&parsed, /*live=*/false, NULL, 0);
|
||||||
|
|
||||||
// A saved player_position only means anything if all three fields are
|
// 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 &&
|
if (parsed.has_player_position && parsed.player_position.has_x &&
|
||||||
parsed.player_position.has_z && parsed.player_position.has_yaw_degrees)
|
parsed.player_position.has_z && parsed.player_position.has_yaw_degrees)
|
||||||
xr_input_skip_startup_recenter();
|
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) {
|
void qs_config_request_save(void) {
|
||||||
@@ -931,6 +1230,42 @@ void qs_config_request_reset_position(void) {
|
|||||||
SDL_PushEvent(&event);
|
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) {
|
bool qs_config_handle_event(const SDL_Event *event) {
|
||||||
if (event->type == s_save_event_type) {
|
if (event->type == s_save_event_type) {
|
||||||
if (qs_config_write_file())
|
if (qs_config_write_file())
|
||||||
@@ -982,5 +1317,33 @@ bool qs_config_handle_event(const SDL_Event *event) {
|
|||||||
message_info("Recentered");
|
message_info("Recentered");
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,15 +66,42 @@ void qs_config_request_set_language(int language_index);
|
|||||||
// player's current real head pose instead.
|
// player's current real head pose instead.
|
||||||
void qs_config_request_reset_position(void);
|
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()/
|
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()/
|
||||||
// _set_language()/_reset_position() enqueue - called from pump_events()
|
// _set_language()/_reset_position()/the four _set_*_screen_*() setters
|
||||||
// (sdl_events.c, see android/engine-patches/21-android-config.patch) on the
|
// enqueue - called from pump_events() (sdl_events.c, see
|
||||||
// engine's own thread, once per polled event, before its normal switch on
|
// android/engine-patches/21-android-config.patch) on the engine's own
|
||||||
// well-known SDL_* event types. Returns false (no-op) for any other event
|
// thread, once per polled event, before its normal switch on well-known
|
||||||
// type, including before the six custom types have even been registered.
|
// 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);
|
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
|
// 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
|
// (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.
|
// 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);
|
bool qs_config_current_renderer_is_opengl(void);
|
||||||
int qs_config_current_resolution_index(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
|
// gShockPrefs.goLanguage (0 English/1 French/2 German, Prefs.h) - read by
|
||||||
// MenuOverlay.java's/KeyboardOverlay.java's periodic header-refresh timer
|
// MenuOverlay.java's/KeyboardOverlay.java's periodic header-refresh timer
|
||||||
// to pick which language's chrome text (Strings.java) to display, so a
|
// 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();
|
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
|
// Read-only queries - safe to call directly (no enqueue needed, see
|
||||||
// qs_config.h's own comment on each of these).
|
// qs_config.h's own comment on each of these).
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
@@ -144,6 +171,42 @@ Java_de_ladkau_questshock_MenuOverlay_nativeGetResolutionIndex(JNIEnv *env, jcla
|
|||||||
return qs_config_current_resolution_index();
|
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
|
JNIEXPORT jint JNICALL
|
||||||
Java_de_ladkau_questshock_MenuOverlay_nativeGetLanguage(JNIEnv *env, jclass clazz) {
|
Java_de_ladkau_questshock_MenuOverlay_nativeGetLanguage(JNIEnv *env, jclass clazz) {
|
||||||
return qs_config_current_language();
|
return qs_config_current_language();
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ static const float kHandColor[2][4] = {
|
|||||||
// MenuOverlay.HEIGHT).
|
// MenuOverlay.HEIGHT).
|
||||||
#define KEYBOARD_TITLE_BAR_V_FRACTION (90.0f / 768.0f)
|
#define KEYBOARD_TITLE_BAR_V_FRACTION (90.0f / 768.0f)
|
||||||
#define KEYBOARD_CLOSE_BUTTON_U_FRACTION (1.0f - 160.0f / 1024.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
|
#define MENU_DRAG_HANDLE_U_FRACTION 1.0f
|
||||||
|
|
||||||
static XrInstance g_instance = XR_NULL_HANDLE;
|
static XrInstance g_instance = XR_NULL_HANDLE;
|
||||||
@@ -642,24 +642,36 @@ static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float
|
|||||||
g_beam_quad_valid[hand] = true;
|
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
|
// - the lowest-priority target, tried only once neither the keyboard nor
|
||||||
// menu overlay claimed the ray this frame (or, symmetrically, once this
|
// 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
|
// g_game_touch_active and the keyboard/menu call sites in
|
||||||
// xr_input_sync_and_draw()). outU/outV (only meaningful when the
|
// 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
|
// 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
|
// via xr_mouse.h. Returns plain `hit` - drag/touch dispatch for actual
|
||||||
// clicks is the caller's responsibility (xr_mouse_click()), not this
|
// clicks is the caller's responsibility (xr_mouse_click()), not this
|
||||||
// function's, unlike xr_input_try_overlay().
|
// 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,
|
static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, float fx, float fy,
|
||||||
float fz, bool *outPlaneHit, float *outPlaneDistance,
|
float fz, bool *outPlaneHit, float *outPlaneDistance,
|
||||||
float *outU, float *outV) {
|
float *outU, float *outV) {
|
||||||
float distance, halfWidth, halfHeight;
|
float distance, halfWidth, halfHeight, radius, centralAngle;
|
||||||
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);
|
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight, &radius, ¢ralAngle);
|
||||||
|
|
||||||
bool planeHit = false, hit = false;
|
bool planeHit = false, hit = false;
|
||||||
float u = 0.0f, v = 0.0f, t = 0.0f;
|
float u = 0.0f, v = 0.0f, t = 0.0f;
|
||||||
|
|
||||||
|
if (centralAngle <= 0.0f) {
|
||||||
if (fabsf(fz) > 1e-5f) {
|
if (fabsf(fz) > 1e-5f) {
|
||||||
t = (-distance - location->pose.position.z) / fz;
|
t = (-distance - location->pose.position.z) / fz;
|
||||||
if (t > 0.0f) {
|
if (t > 0.0f) {
|
||||||
@@ -671,6 +683,56 @@ static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, fl
|
|||||||
v = (1.0f - cy) * 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;
|
*outPlaneHit = planeHit;
|
||||||
*outPlaneDistance = t;
|
*outPlaneDistance = t;
|
||||||
*outU = u;
|
*outU = u;
|
||||||
|
|||||||
@@ -26,14 +26,18 @@
|
|||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, 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
|
// Original, pre-screen-profile-feature default distance for the game
|
||||||
// origin, sized to preserve the game's own aspect ratio at a comfortable
|
// screen - 2m in front of the local space origin - now just the initial
|
||||||
// visual angle. No per-eye view/projection work is needed for a quad layer
|
// value of g_screen_distance_m (below) rather than a fixed constant.
|
||||||
// at all - the compositor handles reprojecting it per eye itself, which is
|
// Width/curve/distance are all configurable (see g_screen_width_m/
|
||||||
// exactly why this milestone doesn't need xrLocateViews or any stereo
|
// g_screen_curve_deg/g_screen_distance_m above and CONFIG.md's Video
|
||||||
// rendering (see the plan's step A notes).
|
// 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_DISTANCE_METERS 2.0f
|
||||||
#define QUAD_WIDTH_METERS 1.6f
|
|
||||||
|
|
||||||
// The menu launcher (just a "Keyboard" button - see MenuOverlay.java) is
|
// The menu launcher (just a "Keyboard" button - see MenuOverlay.java) is
|
||||||
// small and sits above where the keyboard defaults to; the keyboard keeps
|
// 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
|
// xr_overlay.h) so both can be shown/hidden/dragged separately - deliberately
|
||||||
// positioned apart by default so they don't start out overlapping.
|
// positioned apart by default so they don't start out overlapping.
|
||||||
#define MENU_JAVA_CLASS "de.ladkau.questshock.MenuOverlay"
|
#define MENU_JAVA_CLASS "de.ladkau.questshock.MenuOverlay"
|
||||||
// 768x552 (was 512x256, then 768x512) - grown to fit the header/tabs/Video
|
// 768x800 (was 512x256, then 768x512, then 768x552) - grown to fit the
|
||||||
// Options content added on top of the original single "Keyboard" button,
|
// header/tabs/Video Options content added on top of the original single
|
||||||
// then again to fit the stats ribbon below the tab bar; taller in world
|
// "Keyboard" button, then again to fit the stats ribbon below the tab
|
||||||
// space too since MENU_WIDTH_METERS (the physical size) stays fixed and
|
// bar, then again to fit the Video Options tab's screen-size grid; taller
|
||||||
// height_m is derived from it by aspect ratio (see xr_overlay.c's
|
// in world space too since MENU_WIDTH_METERS (the physical size) stays
|
||||||
// xr_overlay_init()).
|
// 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_WIDTH 768
|
||||||
#define MENU_HEIGHT 552
|
#define MENU_HEIGHT 800
|
||||||
#define MENU_DISTANCE_METERS 1.3f
|
#define MENU_DISTANCE_METERS 1.3f
|
||||||
// Physical size - was 0.35m, too small to read comfortably at
|
// Physical size - was 0.35m, too small to read comfortably at
|
||||||
// MENU_DISTANCE_METERS given how much text the header/tabs/stats ribbon
|
// 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_cpu_utilization_path = XR_NULL_PATH;
|
||||||
static XrPath g_gpu_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) {
|
static bool xr_check(XrResult result, const char *what) {
|
||||||
if (XR_SUCCEEDED(result))
|
if (XR_SUCCEEDED(result))
|
||||||
return true;
|
return true;
|
||||||
@@ -355,12 +379,24 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
|||||||
|
|
||||||
g_performance_metrics_supported =
|
g_performance_metrics_supported =
|
||||||
xr_instance_extension_supported(XR_META_PERFORMANCE_METRICS_EXTENSION_NAME);
|
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_ANDROID_CREATE_INSTANCE_EXTENSION_NAME,
|
||||||
XR_KHR_OPENGL_ES_ENABLE_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};
|
XrInstanceCreateInfoAndroidKHR androidInfo = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
|
||||||
androidInfo.applicationVM = vm;
|
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};
|
XrInstanceCreateInfo createInfo = {XR_TYPE_INSTANCE_CREATE_INFO};
|
||||||
createInfo.next = &androidInfo;
|
createInfo.next = &androidInfo;
|
||||||
createInfo.enabledExtensionCount = g_performance_metrics_supported ? 3 : 2;
|
createInfo.enabledExtensionCount = extensionCount;
|
||||||
createInfo.enabledExtensionNames = extensions;
|
createInfo.enabledExtensionNames = extensions;
|
||||||
strncpy(createInfo.applicationInfo.applicationName, "QuestShock",
|
strncpy(createInfo.applicationInfo.applicationName, "QuestShock",
|
||||||
XR_MAX_APPLICATION_NAME_SIZE - 1);
|
XR_MAX_APPLICATION_NAME_SIZE - 1);
|
||||||
@@ -663,30 +699,54 @@ void xr_frame_end(void) {
|
|||||||
if (!xr_is_session_running())
|
if (!xr_is_session_running())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Up to 5 layers: the game quad (if a frame was actually rendered),
|
// Up to 5 layers: the game screen (if a frame was actually rendered) -
|
||||||
// the menu launcher and keyboard overlays while visible - each gets
|
// a flat quad, or a curved cylinder if the currently-active screen
|
||||||
// its own acquire/render/release cycle against its own swapchain (see
|
// profile has a nonzero curve (see xr_get_game_quad_extent()'s own
|
||||||
// xr_overlay_render_and_build_layer()) any time before xrEndFrame,
|
// comment; xr_input.c's xr_input_try_game_quad() hit-tests whichever
|
||||||
// unlike the game quad there's no per-frame engine rendering to wrap
|
// shape is actually submitted here, always agreeing since both read
|
||||||
// around here, just each overlay's own blit - and, per hand, a
|
// the same getter) - the menu launcher and keyboard overlays while
|
||||||
// laser-beam quad while that hand's ray is aimed at the keyboard,
|
// visible - each gets its own acquire/render/release cycle against
|
||||||
// menu, or the game quad itself (see xr_input_get_beam_layer()).
|
// 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};
|
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||||
|
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.space = g_play_space;
|
||||||
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||||
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
|
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
|
||||||
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
|
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
|
||||||
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height;
|
gameQuad.subImage.imageRect.extent.height = g_game_swapchain.height;
|
||||||
gameQuad.pose.orientation.w = 1.0f;
|
gameQuad.pose.orientation.w = 1.0f;
|
||||||
gameQuad.pose.position.z = -QUAD_DISTANCE_METERS;
|
gameQuad.pose.position.z = -gameDistance;
|
||||||
gameQuad.size.width = QUAD_WIDTH_METERS;
|
gameQuad.size.width = gameHalfWidth * 2.0f;
|
||||||
gameQuad.size.height =
|
gameQuad.size.height = gameHalfHeight * 2.0f;
|
||||||
QUAD_WIDTH_METERS * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
|
}
|
||||||
|
|
||||||
const XrCompositionLayerBaseHeader *layers[5];
|
const XrCompositionLayerBaseHeader *layers[5];
|
||||||
uint32_t layerCount = 0;
|
uint32_t layerCount = 0;
|
||||||
if (g_have_acquired_game_image)
|
if (g_have_acquired_game_image)
|
||||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad;
|
layers[layerCount++] = gameScreenCurved ? (XrCompositionLayerBaseHeader *)&gameCylinder
|
||||||
|
: (XrCompositionLayerBaseHeader *)&gameQuad;
|
||||||
|
|
||||||
XrCompositionLayerQuad menuQuad;
|
XrCompositionLayerQuad menuQuad;
|
||||||
if (g_frame_should_render && xr_overlay_render_and_build_layer(g_menu_overlay, &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_xrQueryPerformanceMetricsCounterMETA = NULL;
|
||||||
g_cpu_utilization_path = XR_NULL_PATH;
|
g_cpu_utilization_path = XR_NULL_PATH;
|
||||||
g_gpu_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) {
|
void xr_session_set_game_screen_shape(float width_m, float curve_deg, float distance_m) {
|
||||||
*distance_m = QUAD_DISTANCE_METERS;
|
g_screen_width_m = width_m;
|
||||||
*half_width_m = QUAD_WIDTH_METERS * 0.5f;
|
g_screen_curve_deg = curve_deg;
|
||||||
*half_height_m =
|
g_screen_distance_m = distance_m;
|
||||||
QUAD_WIDTH_METERS * 0.5f * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
|
}
|
||||||
|
|
||||||
|
// 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) {
|
void xr_get_game_resolution(int *width, int *height) {
|
||||||
|
|||||||
@@ -68,11 +68,30 @@ void xr_frame_end(void);
|
|||||||
|
|
||||||
void xr_shutdown(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,
|
// (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
|
// all in meters) - shared with xr_input.c's ray/quad hit-testing so the
|
||||||
// laser pointer always matches whatever's actually visible.
|
// laser pointer always matches whatever's actually visible. *radius_m/
|
||||||
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_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,
|
// The engine's current screen resolution (gScreenWide/gScreenHigh,
|
||||||
// InitMac.h) - not the game quad's own swapchain size, which is fixed at
|
// 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 {
|
public class MenuOverlay extends OverlayPanel {
|
||||||
// Grown from the original 512x256 (still just a single "Keyboard"
|
// Grown from the original 512x256 (still just a single "Keyboard"
|
||||||
// button back then) to fit the header/tabs/Video Options content added
|
// 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
|
// xr_session.c's matching MENU_WIDTH/MENU_HEIGHT and xr_input.c's
|
||||||
// matching MENU_TITLE_BAR_V_FRACTION.
|
// matching MENU_TITLE_BAR_V_FRACTION.
|
||||||
private static final int WIDTH = 768;
|
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 HEADER_HEIGHT = 56;
|
||||||
private static final int TAB_BAR_HEIGHT = 48;
|
private static final int TAB_BAR_HEIGHT = 48;
|
||||||
private static final int STATS_BAR_HEIGHT = 40;
|
private static final int STATS_BAR_HEIGHT = 40;
|
||||||
private static final long STATS_REFRESH_MS = 500;
|
private static final long STATS_REFRESH_MS = 500;
|
||||||
|
|
||||||
private static final String[] RESOLUTION_LABELS = {"320x200", "320x400", "640x400", "640x480", "1024x768"};
|
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
|
// Order must match qs_config.c's kLanguages[]/gShockPrefs.goLanguage's
|
||||||
// own numbering (Prefs.h: 0 English/1 French/2 German) - also the keys
|
// own numbering (Prefs.h: 0 English/1 French/2 German) - also the keys
|
||||||
// this same language's own Strings.java translation is filed under.
|
// 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 openglButton;
|
||||||
private Button[] resolutionButtons;
|
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 int currentLanguage = Strings.ENGLISH;
|
||||||
|
|
||||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
@@ -306,6 +330,108 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
}
|
}
|
||||||
tab.addView(resolutionRow);
|
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;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,6 +564,41 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
for (int i = 0; i < resolutionButtons.length; i++) {
|
for (int i = 0; i < resolutionButtons.length; i++) {
|
||||||
resolutionButtons[i].setSelected(i == index);
|
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
|
// 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++) {
|
for (int i = 0; i < languageButtons.length; i++) {
|
||||||
languageButtons[i].setText(Strings.get(LANGUAGE_KEYS[i], currentLanguage));
|
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
|
// "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
|
// xr_input_recenter_player_to_screen()) - a live-only action, unrelated
|
||||||
// to questshock.config.json.
|
// to questshock.config.json.
|
||||||
private static native void nativeRequestResetPosition();
|
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
|
// Read-only queries - safe to call directly, see qs_config.h's own
|
||||||
// comment on each of these.
|
// comment on each of these.
|
||||||
private static native boolean nativeGetRendererIsOpenGl();
|
private static native boolean nativeGetRendererIsOpenGl();
|
||||||
private static native int nativeGetResolutionIndex();
|
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
|
// Package-visible (not private): KeyboardOverlay also calls this
|
||||||
// directly, once, at its own construction time, to show the right
|
// directly, once, at its own construction time, to show the right
|
||||||
// language from the start rather than waiting for this class's own
|
// 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
|
* SDL_RWops, so Android's asset-manager fallback for SDL_RWFromFile
|
||||||
* doesn't apply here) expects to find it - see android/engine-patches/ for
|
* 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
|
* 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
|
* super.onCreate() (SDLActivity's) must always run first and unconditionally
|
||||||
* - Android throws SuperNotCalledException otherwise, checked right after
|
* - 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");
|
File marker = new File(gameDir, "GET_ASSETS_QUEST.txt");
|
||||||
if (!marker.exists()) {
|
if (!marker.exists()) {
|
||||||
copyAssetDir("shaders", new File(gameDir, "shaders"));
|
copyAssetDir("shaders", new File(gameDir, "shaders"));
|
||||||
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"));
|
copyAssetFile("res/soundfont.sf2", new File(gameDir, "res/soundfont.sf2"), false);
|
||||||
copyAssetFile("GET_ASSETS_QUEST.txt", marker);
|
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
|
// Shockolate's engine/ is never patched to check this itself (see the
|
||||||
// class doc above) - it just does plain fopen("res/data/...", ...)
|
// class doc above) - it just does plain fopen("res/data/...", ...)
|
||||||
// and, on a fresh install with no game data copied in yet, that
|
// and, on a fresh install with no game data copied in yet, that
|
||||||
@@ -211,8 +218,8 @@ public class QuestShockActivity extends SDLActivity {
|
|||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyAssetFile(String assetPath, File dest) {
|
private void copyAssetFile(String assetPath, File dest, boolean overwrite) {
|
||||||
if (dest.exists()) {
|
if (dest.exists() && !overwrite) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
File parent = dest.getParentFile();
|
File parent = dest.getParentFile();
|
||||||
@@ -246,7 +253,7 @@ public class QuestShockActivity extends SDLActivity {
|
|||||||
}
|
}
|
||||||
destDir.mkdirs();
|
destDir.mkdirs();
|
||||||
for (String entry : entries) {
|
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("english", "English", "Anglais", "Englisch");
|
||||||
put("french", "French", "Français", "Französisch");
|
put("french", "French", "Français", "Französisch");
|
||||||
put("german", "German", "Allemand", "Deutsch");
|
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
|
// "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
|
// language, so it has no entry here; call sites just use it as a
|
||||||
// literal.
|
// 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])();
|
||||||
@@ -81,7 +81,7 @@ fi
|
|||||||
echo "prebuiltDir=$PREBUILT_DIR_PROP"
|
echo "prebuiltDir=$PREBUILT_DIR_PROP"
|
||||||
} > "$ANDROID_DIR/engine.properties"
|
} > "$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"
|
ASSETS_DIR="$ANDROID_DIR/app/src/main/assets"
|
||||||
rm -rf "$ASSETS_DIR"
|
rm -rf "$ASSETS_DIR"
|
||||||
mkdir -p "$ASSETS_DIR/shaders" "$ASSETS_DIR/res"
|
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 -a "$ANDROID_DIR/gles-shaders/." "$ASSETS_DIR/shaders/"
|
||||||
cp "/opt/prebuilt/soundfont/default.sf2" "$ASSETS_DIR/res/soundfont.sf2"
|
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/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 =="
|
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
|
# gl4es is deliberately absent here - it's no longer prebuilt, it's compiled
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user