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