Add a language option to the VR menu, and fix a crash switching to it
build / build (push) Successful in 2m22s
build / build (push) Successful in 2m22s
- New "Language" row on the Main Menu tab (English/French/German), backed by a new "general" group in questshock.config.json - Fixes a device-reproduced crash: switching language used to call language_dealfunc(), whose render_run()/opanel_redraw() repaint assumes the game's own in-game options panel is on screen - crashes (SIGSEGV) when triggered from our independent VR overlay instead. Now calls language_change() directly, which is all that's actually needed to reload resources and refresh the inventory/MFD/side-icon UI - Fixes a second crash: this data set ships frnstrng.res/gerstrng.res but not mfdfrn.res/mfdger.res, and language_change() closes the current resource files before discovering the new ones don't exist - leaving no valid resource file open and crashing on the next lookup. Now checks both files exist before ever switching, refusing the change (with an in-headset popup) instead of corrupting resource state - extract_assets.sh can now also pull mfdfrn.res/mfdger.res out of a System Shock: Classic installer (which includes them, unlike Enhanced Edition) if one is present in res/assets/ - entirely optional - Documented all of the above in README.md/CONFIG.md
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
# Game assets are not included in the repo
|
# Game assets are not included in the repo
|
||||||
/res/assets/setup_system_shock_enhanced*
|
/res/assets/setup_system_shock_enhanced*
|
||||||
|
/res/assets/setup_system_shock_classic*
|
||||||
/res/assets/*.zip
|
/res/assets/*.zip
|
||||||
/res/assets/ss_ee/
|
/res/assets/ss_ee/
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ startup, in addition to (and independent of) the engine's own `prefs.txt`.
|
|||||||
load and so applies every group in the file, including the
|
load and so applies every group in the file, including the
|
||||||
restart-required ones.
|
restart-required ones.
|
||||||
|
|
||||||
(Both options currently documented below are live-appliable, so this
|
(Every option currently documented below is live-appliable, so this
|
||||||
dialog won't actually appear yet - it's built in for options added in a
|
dialog won't actually appear yet - it's built in for options added in a
|
||||||
future version that do need a restart.)
|
future version that do need a restart.)
|
||||||
- Unrecognized groups/keys are ignored (harmless if you're editing the file
|
- Unrecognized groups/keys are ignored (harmless if you're editing the file
|
||||||
@@ -48,6 +48,9 @@ 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"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"language": "english"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -69,3 +72,25 @@ The game's internal rendering resolution.
|
|||||||
- **Type**: enum
|
- **Type**: enum
|
||||||
- **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"`
|
- **Possible values**: `"320x200"`, `"320x400"`, `"640x400"`, `"640x480"`, `"1024x768"`
|
||||||
- **Restart required?**: No (live-appliable)
|
- **Restart required?**: No (live-appliable)
|
||||||
|
|
||||||
|
## General (`general`)
|
||||||
|
|
||||||
|
### `language`
|
||||||
|
|
||||||
|
The game's UI/text language - both the game's own resource strings and this
|
||||||
|
project's own menu/keyboard chrome text (title bar, buttons, etc.).
|
||||||
|
|
||||||
|
- **Type**: enum
|
||||||
|
- **Possible values**: `"english"`, `"french"`, `"german"`
|
||||||
|
- **Restart required?**: No (live-appliable) - applies the same way the
|
||||||
|
game's own in-game options menu applies a language change (reloads the
|
||||||
|
language-specific resource files and refreshes the inventory/MFD/side-icon
|
||||||
|
UI immediately).
|
||||||
|
- **Note**: French and German both need their own full resource pack
|
||||||
|
(`frnstrng.res`/`mfdfrn.res`, `gerstrng.res`/`mfdger.res` under
|
||||||
|
`res/data/`) - if any of those files aren't present, the switch is
|
||||||
|
refused (logged, `questshock.config.json` and the current language are
|
||||||
|
left unchanged) rather than leaving the game with no valid resource file
|
||||||
|
open. See README.md section 3 ("Game assets") for where `mfdfrn.res`/
|
||||||
|
`mfdger.res` come from - the GOG Enhanced Edition installer alone doesn't
|
||||||
|
include them.
|
||||||
|
|||||||
@@ -90,6 +90,21 @@ package`/`make apk` pick it up the same way either way. This one copy of
|
|||||||
game assets feeds both the desktop build and the Android/Quest build
|
game assets feeds both the desktop build and the Android/Quest build
|
||||||
below.
|
below.
|
||||||
|
|
||||||
|
**Note on French/German**: the GOG Enhanced Edition installer only ships
|
||||||
|
English MFD (Multi-Function Display - the small in-game screens showing
|
||||||
|
the map, inventory, etc.) art resources (`mfdart.res`) - it doesn't include
|
||||||
|
`mfdfrn.res`/`mfdger.res`, which French/German need in addition to their
|
||||||
|
own text resources (`frnstrng.res`/`gerstrng.res`, which the installer
|
||||||
|
*does* include). Without those two files, switching to French/German (via
|
||||||
|
`questshock.config.json`'s `general.language` or the in-headset menu's
|
||||||
|
Language row - see [CONFIG.md](CONFIG.md)) is refused with an in-headset
|
||||||
|
popup rather than applied, since doing so anyway used to crash the game. If
|
||||||
|
you also own **System Shock: Classic** on gog.com (which does include both
|
||||||
|
files), drop its offline installer into `res/assets/` alongside the
|
||||||
|
Enhanced Edition one and `extract_assets.sh` pulls them out of it too,
|
||||||
|
entirely optionally - it still runs fine with just the Enhanced Edition
|
||||||
|
installer.
|
||||||
|
|
||||||
## 4. Desktop build
|
## 4. Desktop build
|
||||||
|
|
||||||
Builds and runs Questshock natively on the desktop (your dev machine, or
|
Builds and runs Questshock natively on the desktop (your dev machine, or
|
||||||
|
|||||||
@@ -284,9 +284,15 @@ typedef struct {
|
|||||||
char resolution[16]; // "" = not present in the file
|
char resolution[16]; // "" = not present in the file
|
||||||
} QsParsedVideoOptions;
|
} QsParsedVideoOptions;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char language[16]; // "" = not present in the file
|
||||||
|
} QsParsedGeneral;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
QsParsedVideoOptions video_options;
|
QsParsedVideoOptions video_options;
|
||||||
bool has_video_options;
|
bool has_video_options;
|
||||||
|
QsParsedGeneral general;
|
||||||
|
bool has_general;
|
||||||
} QsParsedConfig;
|
} QsParsedConfig;
|
||||||
|
|
||||||
static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void *ctx) {
|
static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void *ctx) {
|
||||||
@@ -299,10 +305,20 @@ static void qs_parse_video_options_field(const char *key, QsJsonParser *p, void
|
|||||||
qs_json_skip_value(p);
|
qs_json_skip_value(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void qs_parse_general_field(const char *key, QsJsonParser *p, void *ctx) {
|
||||||
|
QsParsedGeneral *out = (QsParsedGeneral *)ctx;
|
||||||
|
if (strcmp(key, "language") == 0)
|
||||||
|
qs_json_take_string_field(p, out->language, sizeof(out->language));
|
||||||
|
else
|
||||||
|
qs_json_skip_value(p);
|
||||||
|
}
|
||||||
|
|
||||||
static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx) {
|
static void qs_parse_top_level_field(const char *key, QsJsonParser *p, void *ctx) {
|
||||||
QsParsedConfig *out = (QsParsedConfig *)ctx;
|
QsParsedConfig *out = (QsParsedConfig *)ctx;
|
||||||
if (strcmp(key, "video_options") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
if (strcmp(key, "video_options") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
||||||
out->has_video_options = qs_json_parse_object(p, qs_parse_video_options_field, &out->video_options);
|
out->has_video_options = qs_json_parse_object(p, qs_parse_video_options_field, &out->video_options);
|
||||||
|
else if (strcmp(key, "general") == 0 && !p->error && p->p < p->end && *p->p == '{')
|
||||||
|
out->has_general = qs_json_parse_object(p, qs_parse_general_field, &out->general);
|
||||||
else
|
else
|
||||||
qs_json_skip_value(p);
|
qs_json_skip_value(p);
|
||||||
}
|
}
|
||||||
@@ -442,6 +458,117 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// General: language
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
// Live-appliable: language_change() (below) is the resource-reload half of
|
||||||
|
// what the game's own options menu calls when the player changes language
|
||||||
|
// there (see wrapper.c's language_dealfunc(), wired up via
|
||||||
|
// multi_init(..., REF_STR_Language, ...)) - closes/reopens the
|
||||||
|
// language-specific resource files, updates which_lang/QUESTVAR, and
|
||||||
|
// refreshes the inventory/MFD/side-icon panels' own language-dependent
|
||||||
|
// text/art. Deliberately NOT calling language_dealfunc() itself: its extra
|
||||||
|
// render_run()/opanel_redraw() repaint step assumes the game's own
|
||||||
|
// in-game options-panel widget is the thing currently on screen, which
|
||||||
|
// isn't true when this is triggered from our independent VR overlay menu -
|
||||||
|
// confirmed by a device crash (SIGSEGV, null pointer) inside render_run()'s
|
||||||
|
// 3D-view frustum-clipping code when called this way. The game's normal
|
||||||
|
// per-frame render loop already redraws everything anyway.
|
||||||
|
#define QS_LANGUAGE_REQUIRES_RESTART false
|
||||||
|
|
||||||
|
// wrapper.h declares this one, but including the whole header pulls in
|
||||||
|
// other prototypes referencing types this file has no other reason to need
|
||||||
|
// - same reasoning as screen_draw() above.
|
||||||
|
extern void language_change(uchar lang);
|
||||||
|
|
||||||
|
// gamestrn.c/init.c - declared locally rather than pulling in their
|
||||||
|
// headers, same reasoning as language_change() above. language_change()
|
||||||
|
// itself indexes these two by lang (0/1/2) to reopen the string/MFD-art
|
||||||
|
// resource files for the new language - but it does so destructively,
|
||||||
|
// closing the *current* (working) files first, with no fallback if the
|
||||||
|
// reopen then fails. qs_language_resources_available() below checks both
|
||||||
|
// files exist *before* ever calling language_change(), so a language whose
|
||||||
|
// resource pack wasn't fully installed can't leave the game with no valid
|
||||||
|
// resource file open at all.
|
||||||
|
extern char *language_files[];
|
||||||
|
extern char *mfdart_files[];
|
||||||
|
|
||||||
|
static const struct {
|
||||||
|
const char *name;
|
||||||
|
short language;
|
||||||
|
} kLanguages[] = {
|
||||||
|
{"english", 0},
|
||||||
|
{"french", 1},
|
||||||
|
{"german", 2},
|
||||||
|
};
|
||||||
|
#define QS_NUM_LANGUAGES (sizeof(kLanguages) / sizeof(kLanguages[0]))
|
||||||
|
|
||||||
|
// Confirmed necessary on a real device: this project's own Android data set
|
||||||
|
// ships frnstrng.res/gerstrng.res (language_files[]) but not
|
||||||
|
// mfdfrn.res/mfdger.res (mfdart_files[]) - selecting French or German used
|
||||||
|
// to call language_change(), which closes the working English resource
|
||||||
|
// files before discovering the new ones don't exist, leaving no valid
|
||||||
|
// resource file open at all and crashing (SIGBUS, null resource pointer)
|
||||||
|
// the next time any resource lookup happened.
|
||||||
|
static bool qs_language_resources_available(short lang) {
|
||||||
|
FILE *f = fopen(language_files[lang], "rb");
|
||||||
|
if (!f)
|
||||||
|
return false;
|
||||||
|
fclose(f);
|
||||||
|
f = fopen(mfdart_files[lang], "rb");
|
||||||
|
if (!f)
|
||||||
|
return false;
|
||||||
|
fclose(f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined further down, alongside qs_show_restart_dialog() (same
|
||||||
|
// find-class-via-classloader JNI plumbing) - forward-declared here since
|
||||||
|
// qs_apply_language() above is defined earlier in the file.
|
||||||
|
static void qs_show_message_dialog(const char *message);
|
||||||
|
|
||||||
|
static bool qs_apply_language(const char *value, bool live) {
|
||||||
|
for (size_t i = 0; i < QS_NUM_LANGUAGES; i++) {
|
||||||
|
if (strcmp(value, kLanguages[i].name) != 0)
|
||||||
|
continue;
|
||||||
|
if (!qs_language_resources_available(kLanguages[i].language)) {
|
||||||
|
WARN("%s: general.language=\"%s\" - resource files for this language aren't installed "
|
||||||
|
"- not switching",
|
||||||
|
QS_CONFIG_FILENAME, value);
|
||||||
|
// Only when there's a menu to show it in - live=false is the
|
||||||
|
// startup path (qs_config_load_startup()), before the menu
|
||||||
|
// overlay exists; qs_show_message_dialog() would just silently
|
||||||
|
// no-op there anyway (MenuOverlay.instance is still null), but
|
||||||
|
// there's no point trying.
|
||||||
|
if (live) {
|
||||||
|
char message[256];
|
||||||
|
snprintf(message, sizeof(message),
|
||||||
|
"Can't switch to %s: its resource files (%s, %s) aren't installed in "
|
||||||
|
"res/data/ - see CONFIG.md.",
|
||||||
|
value, language_files[kLanguages[i].language],
|
||||||
|
mfdart_files[kLanguages[i].language]);
|
||||||
|
qs_show_message_dialog(message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool changed = gShockPrefs.goLanguage != kLanguages[i].language;
|
||||||
|
gShockPrefs.goLanguage = kLanguages[i].language;
|
||||||
|
if (changed && live)
|
||||||
|
language_change((uchar)kLanguages[i].language);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
WARN("%s: general.language=\"%s\" is invalid - ignoring", QS_CONFIG_FILENAME, value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *qs_current_language_name(void) {
|
||||||
|
for (size_t i = 0; i < QS_NUM_LANGUAGES; i++)
|
||||||
|
if (kLanguages[i].language == gShockPrefs.goLanguage)
|
||||||
|
return kLanguages[i].name;
|
||||||
|
return "english"; // shouldn't happen - safe fallback for Save
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Read-only queries for the Video Options tab (see qs_config.h)
|
// Read-only queries for the Video Options tab (see qs_config.h)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -483,17 +610,24 @@ static void qs_config_apply_parsed(const QsParsedConfig *parsed, bool live, char
|
|||||||
size_t restart_needed_size) {
|
size_t restart_needed_size) {
|
||||||
if (restart_needed && restart_needed_size > 0)
|
if (restart_needed && restart_needed_size > 0)
|
||||||
restart_needed[0] = '\0';
|
restart_needed[0] = '\0';
|
||||||
if (!parsed->has_video_options)
|
if (parsed->has_video_options) {
|
||||||
return;
|
if (parsed->video_options.renderer[0] != '\0') {
|
||||||
if (parsed->video_options.renderer[0] != '\0') {
|
bool changed = qs_apply_renderer(parsed->video_options.renderer, live);
|
||||||
bool changed = qs_apply_renderer(parsed->video_options.renderer, live);
|
if (changed && QS_RENDERER_REQUIRES_RESTART)
|
||||||
if (changed && QS_RENDERER_REQUIRES_RESTART)
|
qs_append_restart_reason(restart_needed, restart_needed_size, "renderer");
|
||||||
qs_append_restart_reason(restart_needed, restart_needed_size, "renderer");
|
}
|
||||||
|
if (parsed->video_options.resolution[0] != '\0') {
|
||||||
|
bool changed = qs_apply_resolution(parsed->video_options.resolution, live);
|
||||||
|
if (changed && QS_RESOLUTION_REQUIRES_RESTART)
|
||||||
|
qs_append_restart_reason(restart_needed, restart_needed_size, "resolution");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (parsed->video_options.resolution[0] != '\0') {
|
if (parsed->has_general) {
|
||||||
bool changed = qs_apply_resolution(parsed->video_options.resolution, live);
|
if (parsed->general.language[0] != '\0') {
|
||||||
if (changed && QS_RESOLUTION_REQUIRES_RESTART)
|
bool changed = qs_apply_language(parsed->general.language, live);
|
||||||
qs_append_restart_reason(restart_needed, restart_needed_size, "resolution");
|
if (changed && QS_LANGUAGE_REQUIRES_RESTART)
|
||||||
|
qs_append_restart_reason(restart_needed, restart_needed_size, "language");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,6 +641,9 @@ static bool qs_config_write_file(void) {
|
|||||||
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, " },\n");
|
||||||
|
fprintf(f, " \"general\": {\n");
|
||||||
|
fprintf(f, " \"language\": \"%s\"\n", qs_current_language_name());
|
||||||
fprintf(f, " }\n");
|
fprintf(f, " }\n");
|
||||||
fprintf(f, "}\n");
|
fprintf(f, "}\n");
|
||||||
fclose(f);
|
fclose(f);
|
||||||
@@ -538,7 +675,10 @@ static jclass qs_find_class(JNIEnv *env, jobject activity, const char *name) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void qs_show_restart_dialog(const char *restart_needed) {
|
// Shared by qs_show_restart_dialog() and qs_show_message_dialog() below -
|
||||||
|
// both call a MenuOverlay static method taking a single String and
|
||||||
|
// returning void, differing only in which one.
|
||||||
|
static void qs_call_dialog_method(const char *java_method_name, const char *message) {
|
||||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||||
jobject activity = (jobject)SDL_AndroidGetActivity();
|
jobject activity = (jobject)SDL_AndroidGetActivity();
|
||||||
if (!env || !activity)
|
if (!env || !activity)
|
||||||
@@ -548,24 +688,34 @@ static void qs_show_restart_dialog(const char *restart_needed) {
|
|||||||
if (!menuOverlayClass)
|
if (!menuOverlayClass)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
jmethodID showRestartDialog =
|
jmethodID method =
|
||||||
(*env)->GetStaticMethodID(env, menuOverlayClass, "showRestartDialog", "(Ljava/lang/String;)V");
|
(*env)->GetStaticMethodID(env, menuOverlayClass, java_method_name, "(Ljava/lang/String;)V");
|
||||||
if (!showRestartDialog) {
|
if (!method) {
|
||||||
(*env)->ExceptionClear(env);
|
(*env)->ExceptionClear(env);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jstring messageStr = (*env)->NewStringUTF(env, message);
|
||||||
|
(*env)->CallStaticVoidMethod(env, menuOverlayClass, method, messageStr);
|
||||||
|
if ((*env)->ExceptionCheck(env))
|
||||||
|
(*env)->ExceptionClear(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void qs_show_restart_dialog(const char *restart_needed) {
|
||||||
char message[256];
|
char message[256];
|
||||||
snprintf(message, sizeof(message),
|
snprintf(message, sizeof(message),
|
||||||
"The following changes require a restart to take effect: %s. Everything else has "
|
"The following changes require a restart to take effect: %s. Everything else has "
|
||||||
"already been applied.",
|
"already been applied.",
|
||||||
restart_needed);
|
restart_needed);
|
||||||
jstring messageStr = (*env)->NewStringUTF(env, message);
|
qs_call_dialog_method("showRestartDialog", message);
|
||||||
(*env)->CallStaticVoidMethod(env, menuOverlayClass, showRestartDialog, messageStr);
|
|
||||||
if ((*env)->ExceptionCheck(env))
|
|
||||||
(*env)->ExceptionClear(env);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same restart-confirmation dialog view (MenuOverlay.java's
|
||||||
|
// showMessageDialog()), minus its Restart Now button - for a plain
|
||||||
|
// informational/warning message with nothing to restart into, currently
|
||||||
|
// only qs_apply_language()'s missing-resource-files warning above.
|
||||||
|
static void qs_show_message_dialog(const char *message) { qs_call_dialog_method("showMessageDialog", message); }
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Public API (see qs_config.h)
|
// Public API (see qs_config.h)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -574,19 +724,21 @@ static Uint32 s_save_event_type;
|
|||||||
static Uint32 s_load_event_type;
|
static Uint32 s_load_event_type;
|
||||||
static Uint32 s_set_renderer_event_type;
|
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;
|
||||||
|
|
||||||
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() enqueue - done here, once, on the
|
// _set_renderer()/_set_resolution()/_set_language() enqueue - done here,
|
||||||
// engine's own thread and before the menu/its buttons could possibly
|
// once, on the engine's own thread and before the menu/its buttons could
|
||||||
// exist yet, rather than lazily from those functions themselves (which
|
// possibly exist yet, rather than lazily from those functions themselves
|
||||||
// run on Android's UI thread and could otherwise race each other doing
|
// (which run on Android's UI thread and could otherwise race each other
|
||||||
// this lazily).
|
// doing this lazily).
|
||||||
Uint32 base = SDL_RegisterEvents(4);
|
Uint32 base = SDL_RegisterEvents(5);
|
||||||
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;
|
||||||
|
|
||||||
QsParsedConfig parsed;
|
QsParsedConfig parsed;
|
||||||
if (qs_config_read_file(&parsed))
|
if (qs_config_read_file(&parsed))
|
||||||
@@ -623,6 +775,14 @@ void qs_config_request_set_resolution(int resolution_index) {
|
|||||||
SDL_PushEvent(&event);
|
SDL_PushEvent(&event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void qs_config_request_set_language(int language_index) {
|
||||||
|
SDL_Event event;
|
||||||
|
SDL_zero(event);
|
||||||
|
event.type = s_set_language_event_type;
|
||||||
|
event.user.code = language_index;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
}
|
||||||
|
|
||||||
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())
|
||||||
@@ -655,5 +815,11 @@ bool qs_config_handle_event(const SDL_Event *event) {
|
|||||||
qs_apply_resolution(kResolutions[index].name, /*live=*/true);
|
qs_apply_resolution(kResolutions[index].name, /*live=*/true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (event->type == s_set_language_event_type) {
|
||||||
|
int index = event->user.code;
|
||||||
|
if (index >= 0 && (size_t)index < QS_NUM_LANGUAGES)
|
||||||
|
qs_apply_language(kLanguages[index].name, /*live=*/true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,13 +49,20 @@ void qs_config_request_load(void);
|
|||||||
void qs_config_request_set_renderer(bool opengl);
|
void qs_config_request_set_renderer(bool opengl);
|
||||||
void qs_config_request_set_resolution(int resolution_index);
|
void qs_config_request_set_resolution(int resolution_index);
|
||||||
|
|
||||||
// Returns true and handles the event if it's one of the four custom events
|
// Same cross-thread-dispatch reasoning as the two above - called from the
|
||||||
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()
|
// Main Menu tab's Language row (MenuOverlay.java). language_index is 0
|
||||||
// enqueue - called from pump_events() (sdl_events.c, see
|
// English/1 French/2 German (gShockPrefs.goLanguage's own numbering,
|
||||||
|
// Prefs.h). Applies live immediately, the same way the game's own options
|
||||||
|
// menu applies a language change (see qs_config.c's qs_apply_language()).
|
||||||
|
void qs_config_request_set_language(int language_index);
|
||||||
|
|
||||||
|
// Returns true and handles the event if it's one of the five custom events
|
||||||
|
// qs_config_request_save()/_load()/_set_renderer()/_set_resolution()/
|
||||||
|
// _set_language() enqueue - called from pump_events() (sdl_events.c, see
|
||||||
// android/engine-patches/21-android-config.patch) on the engine's own
|
// android/engine-patches/21-android-config.patch) on the engine's own
|
||||||
// thread, once per polled event, before its normal switch on well-known
|
// thread, once per polled event, before its normal switch on well-known
|
||||||
// SDL_* event types. Returns false (no-op) for any other event type,
|
// SDL_* event types. Returns false (no-op) for any other event type,
|
||||||
// including before the four custom types have even been registered.
|
// including before the five custom types have even been registered.
|
||||||
bool qs_config_handle_event(const SDL_Event *event);
|
bool qs_config_handle_event(const SDL_Event *event);
|
||||||
|
|
||||||
// Read-only queries for the Video Options tab's own selection highlight
|
// Read-only queries for the Video Options tab's own selection highlight
|
||||||
@@ -73,9 +80,12 @@ int qs_config_current_resolution_index(void);
|
|||||||
// 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
|
||||||
// language change made mid-session via the game's own options menu is
|
// language change made mid-session via the game's own options menu (or our
|
||||||
// picked up without needing to reopen either panel. Same
|
// own Language row, or Load Configuration) is picked up without needing to
|
||||||
// off-thread-scalar-read reasoning as the two getters above.
|
// reopen either panel; also doubles as the Main Menu tab's Language row's
|
||||||
|
// own selection highlight, same as qs_config_current_renderer_is_opengl()/
|
||||||
|
// qs_config_current_resolution_index() above. Same off-thread-scalar-read
|
||||||
|
// reasoning as those two getters.
|
||||||
int qs_config_current_language(void);
|
int qs_config_current_language(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetResolution(JNIEnv *env, jc
|
|||||||
qs_config_request_set_resolution(resolutionIndex);
|
qs_config_request_set_resolution(resolutionIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MenuOverlay's Main Menu tab's Language row - same enqueue-only reasoning as above.
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_de_ladkau_questshock_MenuOverlay_nativeRequestSetLanguage(JNIEnv *env, jclass clazz,
|
||||||
|
jint languageIndex) {
|
||||||
|
qs_config_request_set_language(languageIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ import java.util.Locale;
|
|||||||
* version, FPS/frame-time stats, refreshed on a periodic timer), a content
|
* version, FPS/frame-time stats, refreshed on a periodic timer), a content
|
||||||
* area holding one tab's content at a time, and a bottom tab bar. Tab 1
|
* area holding one tab's content at a time, and a bottom tab bar. Tab 1
|
||||||
* ("Main Menu") is the original "Keyboard"/"Save Configuration"/"Load
|
* ("Main Menu") is the original "Keyboard"/"Save Configuration"/"Load
|
||||||
* Configuration" row plus the restart-confirmation view (shown instead of
|
* Configuration" row plus a Language row, with a dialog view (shown instead
|
||||||
* that row - see CONFIG.md and qs_config.c's *_REQUIRES_RESTART comments).
|
* of that content) doubling as both the restart-confirmation prompt (see
|
||||||
|
* CONFIG.md and qs_config.c's *_REQUIRES_RESTART comments) and a plain
|
||||||
|
* informational/warning popup (showMessageDialog() below).
|
||||||
* Tab 2 ("Video Options") has live-appliable controls for the config file's
|
* Tab 2 ("Video Options") has live-appliable controls for the config file's
|
||||||
* video_options group, mirroring the same buttons' selection to whatever's
|
* video_options group, mirroring the same buttons' selection to whatever's
|
||||||
* actually active (queried fresh whenever the tab is shown, and on the same
|
* actually active (queried fresh whenever the tab is shown, and on the same
|
||||||
@@ -55,6 +57,10 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
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"};
|
||||||
|
// 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.
|
||||||
|
private static final String[] LANGUAGE_KEYS = {"english", "french", "german"};
|
||||||
|
|
||||||
private static MenuOverlay instance;
|
private static MenuOverlay instance;
|
||||||
|
|
||||||
@@ -79,7 +85,10 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
private Button videoOptionsTabButton;
|
private Button videoOptionsTabButton;
|
||||||
|
|
||||||
// Main Menu tab's own two states (only one VISIBLE at a time, within
|
// Main Menu tab's own two states (only one VISIBLE at a time, within
|
||||||
// mainMenuTab)
|
// mainMenuTab) - mainMenuContent is the normal state (button row +
|
||||||
|
// Language row); restartDialog replaces it when Load Configuration
|
||||||
|
// finds a restart-required change.
|
||||||
|
private LinearLayout mainMenuContent;
|
||||||
private LinearLayout buttonRow;
|
private LinearLayout buttonRow;
|
||||||
private LinearLayout restartDialog;
|
private LinearLayout restartDialog;
|
||||||
private TextView restartMessageView;
|
private TextView restartMessageView;
|
||||||
@@ -89,6 +98,10 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
private Button dismissButton;
|
private Button dismissButton;
|
||||||
private Button restartNowButton;
|
private Button restartNowButton;
|
||||||
|
|
||||||
|
// Main Menu tab's Language row
|
||||||
|
private TextView languageLabel;
|
||||||
|
private Button[] languageButtons;
|
||||||
|
|
||||||
// Video Options tab controls
|
// Video Options tab controls
|
||||||
private TextView rendererLabel;
|
private TextView rendererLabel;
|
||||||
private TextView resolutionLabel;
|
private TextView resolutionLabel;
|
||||||
@@ -187,6 +200,10 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
private View buildMainMenuTab() {
|
private View buildMainMenuTab() {
|
||||||
FrameLayout tab = new FrameLayout(activity);
|
FrameLayout tab = new FrameLayout(activity);
|
||||||
|
|
||||||
|
mainMenuContent = new LinearLayout(activity);
|
||||||
|
mainMenuContent.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mainMenuContent.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
buttonRow = new LinearLayout(activity);
|
buttonRow = new LinearLayout(activity);
|
||||||
buttonRow.setOrientation(LinearLayout.HORIZONTAL);
|
buttonRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
buttonRow.setGravity(Gravity.CENTER);
|
buttonRow.setGravity(Gravity.CENTER);
|
||||||
@@ -196,10 +213,32 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
v -> nativeRequestSaveConfig());
|
v -> nativeRequestSaveConfig());
|
||||||
loadButton = addRowButton(buttonRow, Strings.get("load_configuration", currentLanguage),
|
loadButton = addRowButton(buttonRow, Strings.get("load_configuration", currentLanguage),
|
||||||
v -> nativeRequestLoadConfig());
|
v -> nativeRequestLoadConfig());
|
||||||
FrameLayout.LayoutParams buttonRowParams = new FrameLayout.LayoutParams(
|
mainMenuContent.addView(buttonRow, new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
languageLabel = new TextView(activity);
|
||||||
|
languageLabel.setTextColor(0xFFFFFFFF);
|
||||||
|
languageLabel.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
|
||||||
|
languageLabel.setGravity(Gravity.CENTER);
|
||||||
|
languageLabel.setPadding(0, 24, 0, 0);
|
||||||
|
languageLabel.setText(Strings.get("language", currentLanguage));
|
||||||
|
mainMenuContent.addView(languageLabel);
|
||||||
|
|
||||||
|
LinearLayout languageRow = new LinearLayout(activity);
|
||||||
|
languageRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
languageButtons = new Button[LANGUAGE_KEYS.length];
|
||||||
|
for (int i = 0; i < LANGUAGE_KEYS.length; i++) {
|
||||||
|
final int index = i;
|
||||||
|
languageButtons[i] = addSelectableButton(languageRow, Strings.get(LANGUAGE_KEYS[i], currentLanguage),
|
||||||
|
v -> nativeRequestSetLanguage(index));
|
||||||
|
}
|
||||||
|
mainMenuContent.addView(languageRow, new LinearLayout.LayoutParams(
|
||||||
|
600, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams mainMenuContentParams = new FrameLayout.LayoutParams(
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
|
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
|
||||||
buttonRowParams.gravity = Gravity.CENTER;
|
mainMenuContentParams.gravity = Gravity.CENTER;
|
||||||
tab.addView(buttonRow, buttonRowParams);
|
tab.addView(mainMenuContent, mainMenuContentParams);
|
||||||
|
|
||||||
restartMessageView = new TextView(activity);
|
restartMessageView = new TextView(activity);
|
||||||
restartMessageView.setTextColor(0xFFFFFFFF);
|
restartMessageView.setTextColor(0xFFFFFFFF);
|
||||||
@@ -272,12 +311,17 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
tabBar.setOrientation(LinearLayout.HORIZONTAL);
|
tabBar.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
tabBar.setBackgroundColor(0xFF000080);
|
tabBar.setBackgroundColor(0xFF000080);
|
||||||
|
|
||||||
|
// Selectable background (like the Video Options tab's own Renderer/
|
||||||
|
// Resolution buttons) so the currently-active tab stays visibly
|
||||||
|
// highlighted, not just hovered/pressed - see the setSelected()
|
||||||
|
// calls in switchTab() below.
|
||||||
mainMenuTabButton = new Button(activity);
|
mainMenuTabButton = new Button(activity);
|
||||||
mainMenuTabButton.setText(Strings.get("main_menu", currentLanguage));
|
mainMenuTabButton.setText(Strings.get("main_menu", currentLanguage));
|
||||||
mainMenuTabButton.setBackground(buttonBackground());
|
mainMenuTabButton.setBackground(selectableButtonBackground());
|
||||||
mainMenuTabButton.setTextColor(buttonTextColors());
|
mainMenuTabButton.setTextColor(buttonTextColors());
|
||||||
mainMenuTabButton.setOnClickListener(v -> switchTab(mainMenuTab));
|
mainMenuTabButton.setOnClickListener(v -> switchTab(mainMenuTab));
|
||||||
mainMenuTabButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
mainMenuTabButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||||
|
mainMenuTabButton.setSelected(true);
|
||||||
registerHoverable(mainMenuTabButton);
|
registerHoverable(mainMenuTabButton);
|
||||||
tabBar.addView(mainMenuTabButton,
|
tabBar.addView(mainMenuTabButton,
|
||||||
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||||
@@ -288,7 +332,7 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
|
|
||||||
videoOptionsTabButton = new Button(activity);
|
videoOptionsTabButton = new Button(activity);
|
||||||
videoOptionsTabButton.setText(Strings.get("video_options", currentLanguage));
|
videoOptionsTabButton.setText(Strings.get("video_options", currentLanguage));
|
||||||
videoOptionsTabButton.setBackground(buttonBackground());
|
videoOptionsTabButton.setBackground(selectableButtonBackground());
|
||||||
videoOptionsTabButton.setTextColor(buttonTextColors());
|
videoOptionsTabButton.setTextColor(buttonTextColors());
|
||||||
videoOptionsTabButton.setOnClickListener(v -> switchTab(videoOptionsTab));
|
videoOptionsTabButton.setOnClickListener(v -> switchTab(videoOptionsTab));
|
||||||
videoOptionsTabButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
videoOptionsTabButton.setOnTouchListener(CLICK_ON_TOUCH_UP);
|
||||||
@@ -306,9 +350,13 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
currentTab.setVisibility(View.GONE);
|
currentTab.setVisibility(View.GONE);
|
||||||
tab.setVisibility(View.VISIBLE);
|
tab.setVisibility(View.VISIBLE);
|
||||||
currentTab = tab;
|
currentTab = tab;
|
||||||
|
mainMenuTabButton.setSelected(tab == mainMenuTab);
|
||||||
|
videoOptionsTabButton.setSelected(tab == videoOptionsTab);
|
||||||
resetHover();
|
resetHover();
|
||||||
if (tab == videoOptionsTab) {
|
if (tab == videoOptionsTab) {
|
||||||
refreshVideoOptionsSelection();
|
refreshVideoOptionsSelection();
|
||||||
|
} else if (tab == mainMenuTab) {
|
||||||
|
refreshLanguageSelection();
|
||||||
}
|
}
|
||||||
relayoutAndRedraw();
|
relayoutAndRedraw();
|
||||||
}
|
}
|
||||||
@@ -389,6 +437,18 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same reasoning as refreshVideoOptionsSelection() above, for the Main
|
||||||
|
// Menu tab's Language row - nativeGetLanguage() is the same getter
|
||||||
|
// refreshStats() already polls to pick the menu/keyboard chrome text's
|
||||||
|
// own language, so this reflects a language change made any way (our
|
||||||
|
// own Language row, the game's own options menu, or Load Configuration).
|
||||||
|
private void refreshLanguageSelection() {
|
||||||
|
int language = nativeGetLanguage();
|
||||||
|
for (int i = 0; i < languageButtons.length; i++) {
|
||||||
|
languageButtons[i].setSelected(i == language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Polled every STATS_REFRESH_MS while this instance exists (started
|
// Polled every STATS_REFRESH_MS while this instance exists (started
|
||||||
// from the constructor) rather than only while the menu is actually
|
// from the constructor) rather than only while the menu is actually
|
||||||
// visible - simpler than plumbing a new native->Java "menu became
|
// visible - simpler than plumbing a new native->Java "menu became
|
||||||
@@ -423,6 +483,8 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
|
|
||||||
if (currentTab == videoOptionsTab) {
|
if (currentTab == videoOptionsTab) {
|
||||||
refreshVideoOptionsSelection();
|
refreshVideoOptionsSelection();
|
||||||
|
} else if (currentTab == mainMenuTab) {
|
||||||
|
refreshLanguageSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The stats text's width changes every tick (digit count, "ms"
|
// The stats text's width changes every tick (digit count, "ms"
|
||||||
@@ -461,6 +523,10 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
softwareButton.setText(Strings.get("software", currentLanguage));
|
softwareButton.setText(Strings.get("software", currentLanguage));
|
||||||
// openglButton's "OpenGL" label is never translated - see
|
// openglButton's "OpenGL" label is never translated - see
|
||||||
// buildVideoOptionsTab()'s own comment on it.
|
// buildVideoOptionsTab()'s own comment on it.
|
||||||
|
languageLabel.setText(Strings.get("language", currentLanguage));
|
||||||
|
for (int i = 0; i < languageButtons.length; i++) {
|
||||||
|
languageButtons[i].setText(Strings.get(LANGUAGE_KEYS[i], currentLanguage));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Dismiss" on the restart dialog - the live-appliable subset was
|
// "Dismiss" on the restart dialog - the live-appliable subset was
|
||||||
@@ -468,7 +534,7 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
// shown - see qs_config.c's qs_config_handle_event()), so this is
|
// shown - see qs_config.c's qs_config_handle_event()), so this is
|
||||||
// purely a local UI action, no native call needed.
|
// purely a local UI action, no native call needed.
|
||||||
private void showButtonRow() {
|
private void showButtonRow() {
|
||||||
buttonRow.setVisibility(View.VISIBLE);
|
mainMenuContent.setVisibility(View.VISIBLE);
|
||||||
restartDialog.setVisibility(View.GONE);
|
restartDialog.setVisibility(View.GONE);
|
||||||
resetHover();
|
resetHover();
|
||||||
relayoutAndRedraw();
|
relayoutAndRedraw();
|
||||||
@@ -506,6 +572,8 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
// Video Options tab's controls - same enqueue-only reasoning as above.
|
// Video Options tab's controls - same enqueue-only reasoning as above.
|
||||||
private static native void nativeRequestSetRenderer(boolean opengl);
|
private static native void nativeRequestSetRenderer(boolean opengl);
|
||||||
private static native void nativeRequestSetResolution(int resolutionIndex);
|
private static native void nativeRequestSetResolution(int resolutionIndex);
|
||||||
|
// Main Menu tab's Language row - same enqueue-only reasoning as above.
|
||||||
|
private static native void nativeRequestSetLanguage(int languageIndex);
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -545,7 +613,27 @@ public class MenuOverlay extends OverlayPanel {
|
|||||||
}
|
}
|
||||||
instance.activity.runOnUiThread(() -> {
|
instance.activity.runOnUiThread(() -> {
|
||||||
instance.restartMessageView.setText(message);
|
instance.restartMessageView.setText(message);
|
||||||
instance.buttonRow.setVisibility(View.GONE);
|
instance.restartNowButton.setVisibility(View.VISIBLE);
|
||||||
|
instance.mainMenuContent.setVisibility(View.GONE);
|
||||||
|
instance.restartDialog.setVisibility(View.VISIBLE);
|
||||||
|
instance.resetHover();
|
||||||
|
instance.relayoutAndRedraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from qs_config.c's qs_show_message_dialog(), same thread/
|
||||||
|
// dispatch reasoning as showRestartDialog() above - a plain
|
||||||
|
// informational/warning popup with nothing to restart into (currently
|
||||||
|
// only qs_apply_language()'s missing-resource-files warning), so it
|
||||||
|
// reuses the same dialog view minus the Restart Now button.
|
||||||
|
public static void showMessageDialog(final String message) {
|
||||||
|
if (instance == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
instance.activity.runOnUiThread(() -> {
|
||||||
|
instance.restartMessageView.setText(message);
|
||||||
|
instance.restartNowButton.setVisibility(View.GONE);
|
||||||
|
instance.mainMenuContent.setVisibility(View.GONE);
|
||||||
instance.restartDialog.setVisibility(View.VISIBLE);
|
instance.restartDialog.setVisibility(View.VISIBLE);
|
||||||
instance.resetHover();
|
instance.resetHover();
|
||||||
instance.relayoutAndRedraw();
|
instance.relayoutAndRedraw();
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ final class Strings {
|
|||||||
put("renderer", "Renderer", "Moteur de rendu", "Renderer");
|
put("renderer", "Renderer", "Moteur de rendu", "Renderer");
|
||||||
put("resolution", "Resolution", "Résolution", "Auflösung");
|
put("resolution", "Resolution", "Résolution", "Auflösung");
|
||||||
put("software", "Software", "Logiciel", "Software");
|
put("software", "Software", "Logiciel", "Software");
|
||||||
|
put("language", "Language", "Langue", "Sprache");
|
||||||
|
put("english", "English", "Anglais", "Englisch");
|
||||||
|
put("french", "French", "Français", "Französisch");
|
||||||
|
put("german", "German", "Allemand", "Deutsch");
|
||||||
// "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.
|
||||||
|
|||||||
@@ -13,10 +13,23 @@
|
|||||||
# inside it in sshock.kpf, which is just a zip file. Its res/pc/hd/ and
|
# inside it in sshock.kpf, which is just a zip file. Its res/pc/hd/ and
|
||||||
# res/pc/cdrom/ trees together hold every classic-game data/sound file
|
# res/pc/cdrom/ trees together hold every classic-game data/sound file
|
||||||
# Shockolate's res/ folder expects (res/pc/hd wins the couple of
|
# Shockolate's res/ folder expects (res/pc/hd wins the couple of
|
||||||
# filenames present in both: intro.res, objprop.dat). Needs innoextract
|
# filenames present in both: intro.res, objprop.dat), EXCEPT
|
||||||
# and unzip; if they're not installed locally, this script does the
|
# mfdfrn.res/mfdger.res (French/German MFD art - see CONFIG.md/README.md),
|
||||||
# extraction inside a throwaway Ubuntu container instead (needs docker in
|
# which that installer doesn't include at all - see "Optional: French/
|
||||||
# that case, nothing else).
|
# German MFD art" below for where those instead come from. Needs
|
||||||
|
# innoextract and unzip; if they're not installed locally, this script
|
||||||
|
# does the extraction inside a throwaway Ubuntu container instead (needs
|
||||||
|
# docker in that case, nothing else).
|
||||||
|
#
|
||||||
|
# Optional: French/German MFD art
|
||||||
|
# The Enhanced Edition installer's data set doesn't include
|
||||||
|
# mfdfrn.res/mfdger.res, so French/German are otherwise missing their
|
||||||
|
# MFD (Multi-Function Display) art and the game refuses to switch to
|
||||||
|
# them (see CONFIG.md). System Shock: Classic (also gog.com) does
|
||||||
|
# include both files. If you also own it, drop its offline installer
|
||||||
|
# (e.g. setup_system_shock_classic_2.0.0.3.exe) in this directory too -
|
||||||
|
# entirely optional, this script still runs fine without it - and this
|
||||||
|
# script pulls just those two files out of it into ss_ee/data/ as well.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
@@ -27,6 +40,7 @@ fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
|||||||
|
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
installers=(setup_system_shock_enhanced_edition*.exe)
|
installers=(setup_system_shock_enhanced_edition*.exe)
|
||||||
|
classic_installers=(setup_system_shock_classic*.exe)
|
||||||
shopt -u nullglob
|
shopt -u nullglob
|
||||||
case "${#installers[@]}" in
|
case "${#installers[@]}" in
|
||||||
0) fail "no setup_system_shock_enhanced_edition*.exe found in $ASSETS_DIR - buy System Shock: Enhanced Edition on gog.com, download the offline installer, and drop it here" ;;
|
0) fail "no setup_system_shock_enhanced_edition*.exe found in $ASSETS_DIR - buy System Shock: Enhanced Edition on gog.com, download the offline installer, and drop it here" ;;
|
||||||
@@ -35,6 +49,15 @@ case "${#installers[@]}" in
|
|||||||
esac
|
esac
|
||||||
INSTALLER="${installers[0]}"
|
INSTALLER="${installers[0]}"
|
||||||
|
|
||||||
|
# Optional - see "Optional: French/German MFD art" above. Empty (not
|
||||||
|
# missing/failed) when absent; only a genuine ambiguity (more than one
|
||||||
|
# candidate file) is a hard failure.
|
||||||
|
case "${#classic_installers[@]}" in
|
||||||
|
0) CLASSIC_INSTALLER="" ;;
|
||||||
|
1) CLASSIC_INSTALLER="${classic_installers[0]}" ;;
|
||||||
|
*) fail "multiple setup_system_shock_classic*.exe files found in $ASSETS_DIR - keep only one: ${classic_installers[*]}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Extracts sshock.kpf from $1 (an installer .exe) and assembles $2 (an
|
# Extracts sshock.kpf from $1 (an installer .exe) and assembles $2 (an
|
||||||
# ss_ee output directory) from its res/pc/{hd,cdrom} trees. Defined as a
|
# ss_ee output directory) from its res/pc/{hd,cdrom} trees. Defined as a
|
||||||
# function so the exact same logic can run either natively or, via
|
# function so the exact same logic can run either natively or, via
|
||||||
@@ -69,9 +92,38 @@ run_extraction() {
|
|||||||
cp -a "$work/kpf/res/pc/hd/sound/." "$out_dir/sound/"
|
cp -a "$work/kpf/res/pc/hd/sound/." "$out_dir/sound/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Extracts mfdfrn.res/mfdger.res from $1 (a System Shock: Classic installer
|
||||||
|
# .exe) into $2 (an existing ss_ee output directory's data/ subfolder) -
|
||||||
|
# see "Optional: French/German MFD art" above. Unlike the Enhanced Edition
|
||||||
|
# installer, Classic's data files sit directly in the Inno Setup package
|
||||||
|
# (no intermediate zip), so this is just a targeted innoextract --include,
|
||||||
|
# no unzip step needed. Same declare -f-into-docker structure as
|
||||||
|
# run_extraction() above.
|
||||||
|
run_classic_mfd_extraction() {
|
||||||
|
set -euo pipefail
|
||||||
|
local installer="$1" out_dir="$2"
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${work:-}"' EXIT
|
||||||
|
|
||||||
|
echo "== Extracting mfdfrn.res/mfdger.res from classic installer =="
|
||||||
|
innoextract --silent \
|
||||||
|
--include 'app/SSHOCK/DATA/MFDFRN.RES' --include 'app/SSHOCK/DATA/MFDGER.RES' \
|
||||||
|
-d "$work/classic" "$installer"
|
||||||
|
|
||||||
|
# Original DOS-era filenames are uppercase; Shockolate's resource
|
||||||
|
# loader expects the lowercase names already used for every other
|
||||||
|
# res/data/*.res file (case-sensitive on Linux, unlike the DOS/Windows
|
||||||
|
# filesystems these came from).
|
||||||
|
cp "$work/classic/app/SSHOCK/DATA/MFDFRN.RES" "$out_dir/data/mfdfrn.res"
|
||||||
|
cp "$work/classic/app/SSHOCK/DATA/MFDGER.RES" "$out_dir/data/mfdger.res"
|
||||||
|
}
|
||||||
|
|
||||||
if command -v innoextract >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1; then
|
if command -v innoextract >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1; then
|
||||||
echo "== Using local innoextract/unzip =="
|
echo "== Using local innoextract/unzip =="
|
||||||
run_extraction "$ASSETS_DIR/$INSTALLER" "$OUT_DIR"
|
run_extraction "$ASSETS_DIR/$INSTALLER" "$OUT_DIR"
|
||||||
|
if [ -n "$CLASSIC_INSTALLER" ]; then
|
||||||
|
run_classic_mfd_extraction "$ASSETS_DIR/$CLASSIC_INSTALLER" "$OUT_DIR"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "== innoextract/unzip not found locally - using a throwaway docker container =="
|
echo "== innoextract/unzip not found locally - using a throwaway docker container =="
|
||||||
command -v docker >/dev/null 2>&1 \
|
command -v docker >/dev/null 2>&1 \
|
||||||
@@ -86,12 +138,18 @@ else
|
|||||||
apt-get update -qq >/dev/null
|
apt-get update -qq >/dev/null
|
||||||
apt-get install -y -qq innoextract unzip >/dev/null
|
apt-get install -y -qq innoextract unzip >/dev/null
|
||||||
$(declare -f run_extraction)
|
$(declare -f run_extraction)
|
||||||
|
$(declare -f run_classic_mfd_extraction)
|
||||||
run_extraction '/assets/$INSTALLER' '/assets/ss_ee'
|
run_extraction '/assets/$INSTALLER' '/assets/ss_ee'
|
||||||
|
$([ -n "$CLASSIC_INSTALLER" ] && echo "run_classic_mfd_extraction '/assets/$CLASSIC_INSTALLER' '/assets/ss_ee'")
|
||||||
chown -R \"\$HOST_UID:\$HOST_GID\" /assets/ss_ee
|
chown -R \"\$HOST_UID:\$HOST_GID\" /assets/ss_ee
|
||||||
"
|
"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== Done =="
|
echo "== Done =="
|
||||||
echo "Assets extracted to $OUT_DIR"
|
echo "Assets extracted to $OUT_DIR"
|
||||||
echo "Note: mfdfrn.res/mfdger.res (French/German MFD art) are not present in this"
|
if [ -n "$CLASSIC_INSTALLER" ]; then
|
||||||
echo "installer and are only needed for those localizations - not for English play."
|
echo "Also extracted mfdfrn.res/mfdger.res (French/German MFD art) from $CLASSIC_INSTALLER."
|
||||||
|
else
|
||||||
|
echo "Note: mfdfrn.res/mfdger.res (French/German MFD art) are not present - only needed for"
|
||||||
|
echo "French/German (see CONFIG.md); drop a System Shock: Classic installer here too to get them."
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user