diff --git a/CMakeLists.txt b/CMakeLists.txt index 6956574919..02eaeb183e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,12 +109,7 @@ add_subdirectory(libs/freeverb) option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)") option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.") option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON) -if (ANDROID) - set(DUSK_ENABLE_UPDATE_CHECKER_DEFAULT OFF) -else () - set(DUSK_ENABLE_UPDATE_CHECKER_DEFAULT ON) -endif () -option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ${DUSK_ENABLE_UPDATE_CHECKER_DEFAULT}) +option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON) if(ANDROID) set(DUSK_MOVIE_SUPPORT OFF) @@ -328,6 +323,10 @@ if (DUSK_ENABLE_UPDATE_CHECKER) list(APPEND GAME_LIBS winhttp) list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_WINHTTP=1) message(STATUS "dusk: Enabled update checker (WinHTTP)") + elseif (ANDROID) + set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/android.cpp) + list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_ANDROID=1) + message(STATUS "dusk: Enabled update checker (Android)") elseif (APPLE) find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED) set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/url_session.mm) diff --git a/extern/aurora b/extern/aurora index 17be93f0ae..1eeff98783 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 17be93f0ae011fc3202e87e3f2efda4aae250fa5 +Subproject commit 1eeff98783c36fba83970c011783a4fd9deac018 diff --git a/include/dusk/main.h b/include/dusk/main.h index d6b9c9927f..787c2541e2 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -11,7 +11,6 @@ namespace dusk { extern bool IsRunning; extern bool IsShuttingDown; extern bool IsGameLaunched; - extern bool IsFocusPaused; extern bool RestartRequested; extern std::filesystem::path ConfigPath; diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 78c5540e58..cb4945801a 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -135,6 +135,7 @@ struct UserSettings { ConfigVar freeCameraSensitivity; ConfigVar debugFlyCam; ConfigVar debugFlyCamLockEvents; + ConfigVar allowBackgroundInput; // Cheats ConfigVar infiniteHearts; diff --git a/platforms/android/app/build.gradle b/platforms/android/app/build.gradle index 9375d06910..01e706a382 100644 --- a/platforms/android/app/build.gradle +++ b/platforms/android/app/build.gradle @@ -2,6 +2,16 @@ plugins { id 'com.android.application' } +def duskRepoDir = rootProject.projectDir.parentFile.parentFile +def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusk').get().asFile +def syncDuskAssets = tasks.register('syncDuskAssets', Sync) { + from(new File(duskRepoDir, 'res')) { + into 'res' + exclude '**/.DS_Store' + } + into duskGeneratedAssetsDir +} + android { namespace 'com.twilitrealm.dusk' compileSdk 36 @@ -27,7 +37,7 @@ android { sourceSets { main { jniLibs.srcDirs = ['src/main/jniLibs'] - assets.srcDirs = ['../../assets'] + assets.srcDirs = [duskGeneratedAssetsDir] } } @@ -48,3 +58,9 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) } + +tasks.configureEach { task -> + if (task.name.startsWith('merge') && task.name.endsWith('Assets')) { + task.dependsOn(syncDuskAssets) + } +} diff --git a/platforms/android/app/proguard-rules.pro b/platforms/android/app/proguard-rules.pro index 952161ca8b..8f2cf4a4e2 100644 --- a/platforms/android/app/proguard-rules.pro +++ b/platforms/android/app/proguard-rules.pro @@ -1,2 +1,4 @@ # Keep SDL activity and related JNI bridge methods. -keep class org.libsdl.app.** { *; } +-keep class com.twilitrealm.dusk.DuskHttpClient { *; } +-keep class com.twilitrealm.dusk.DuskHttpClient$Response { *; } diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml index e8c2bb29a8..a902065475 100644 --- a/platforms/android/app/src/main/AndroidManifest.xml +++ b/platforms/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + out = new ArrayList<>(); StringBuilder current = new StringBuilder(); @@ -63,7 +72,10 @@ public class DuskActivity extends SDLActivity { super.onCreate(savedInstanceState); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); + WindowInsetsController ctrl = getWindow().getDecorView().getWindowInsetsController(); + if (ctrl != null) { + ctrl.hide(WindowInsets.Type.systemBars()); + } }else { View decorView = getWindow().getDecorView(); // Hide the status bar. @@ -72,7 +84,9 @@ public class DuskActivity extends SDLActivity { // Remember that you should never show the action bar if the // status bar is hidden, so hide that too if necessary. ActionBar actionBar = getActionBar(); - actionBar.hide(); + if (actionBar != null) { + actionBar.hide(); + } } } @@ -108,4 +122,93 @@ public class DuskActivity extends SDLActivity { } return new String[0]; } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + persistUriPermissions(data); + } + super.onActivityResult(requestCode, resultCode, data); + } + + private void persistUriPermissions(Intent data) { + if (data == null) { + return; + } + + int permissionFlags = + data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (permissionFlags == 0) { + return; + } + + Uri uri = data.getData(); + if (uri != null) { + persistUriPermission(uri, permissionFlags); + } + + ClipData clipData = data.getClipData(); + if (clipData == null) { + return; + } + for (int i = 0; i < clipData.getItemCount(); ++i) { + Uri itemUri = clipData.getItemAt(i).getUri(); + if (itemUri != null) { + persistUriPermission(itemUri, permissionFlags); + } + } + } + + private void persistUriPermission(Uri uri, int permissionFlags) { + if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) { + persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read"); + } + if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { + persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write"); + } + } + + private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) { + try { + getContentResolver().takePersistableUriPermission(uri, permissionFlag); + } catch (SecurityException | IllegalArgumentException e) { + Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e); + } + } + + public String getDisplayNameForUri(String uriString) { + if (uriString == null || uriString.isEmpty()) { + return ""; + } + + Uri uri = Uri.parse(uriString); + if ("content".equals(uri.getScheme())) { + try (Cursor cursor = getContentResolver().query( + uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null)) + { + if (cursor != null && cursor.moveToFirst()) { + int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (displayNameColumn >= 0) { + String displayName = cursor.getString(displayNameColumn); + if (displayName != null && !displayName.isEmpty()) { + return displayName; + } + } + } + } catch (SecurityException | IllegalArgumentException e) { + Log.w(TAG, "Unable to query display name for " + uri, e); + } + } else if ("file".equals(uri.getScheme())) { + String path = uri.getPath(); + if (path != null && !path.isEmpty()) { + String name = new File(path).getName(); + if (!name.isEmpty()) { + return name; + } + } + } + + String lastSegment = uri.getLastPathSegment(); + return lastSegment != null ? lastSegment : ""; + } } diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java new file mode 100644 index 0000000000..6e804e506f --- /dev/null +++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java @@ -0,0 +1,237 @@ +package com.twilitrealm.dusk; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; + +public final class DuskHttpClient { + public static final int ERROR_NONE = 0; + public static final int ERROR_INVALID_URL = 1; + public static final int ERROR_UNSUPPORTED_SCHEME = 2; + public static final int ERROR_TIMEOUT = 3; + public static final int ERROR_TOO_LARGE = 4; + public static final int ERROR_NETWORK = 5; + + private static final int MAX_REDIRECTS = 5; + + public static final class Response { + public int error; + public String message; + public int statusCode; + public String[] headerNames; + public String[] headerValues; + public byte[] body; + + Response(int error, String message, int statusCode, String[] headerNames, + String[] headerValues, byte[] body) { + this.error = error; + this.message = message; + this.statusCode = statusCode; + this.headerNames = headerNames != null ? headerNames : new String[0]; + this.headerValues = headerValues != null ? headerValues : new String[0]; + this.body = body != null ? body : new byte[0]; + } + } + + private DuskHttpClient() { + } + + public static Response get(String url, String[] headerNames, String[] headerValues, + int timeoutMs, long maxBodyBytes) { + if (url == null || url.isEmpty()) { + return fail(ERROR_INVALID_URL, "URL is empty"); + } + + try { + URL currentUrl = new URL(url); + if (!isHttps(currentUrl)) { + return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported"); + } + + for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) { + HttpsURLConnection connection = + (HttpsURLConnection) currentUrl.openConnection(); + try { + connection.setRequestMethod("GET"); + connection.setConnectTimeout(timeoutMs); + connection.setReadTimeout(timeoutMs); + connection.setUseCaches(false); + connection.setInstanceFollowRedirects(false); + applyHeaders(connection, headerNames, headerValues); + + int statusCode = connection.getResponseCode(); + if (isRedirect(statusCode)) { + String location = connection.getHeaderField("Location"); + if (location == null || location.isEmpty()) { + return fail(ERROR_NETWORK, "Redirect response did not include Location", + statusCode, connection, new byte[0]); + } + + URL nextUrl = new URL(currentUrl, location); + if (!isHttps(nextUrl)) { + return fail(ERROR_UNSUPPORTED_SCHEME, + "Only https:// redirects are supported", statusCode, + connection, new byte[0]); + } + currentUrl = nextUrl; + continue; + } + + byte[] body = readBody(connection, statusCode, maxBodyBytes); + return success(statusCode, connection, body); + } catch (ResponseTooLargeException e) { + return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit", + safeStatusCode(connection), connection, e.partialBody); + } finally { + connection.disconnect(); + } + } + + return fail(ERROR_NETWORK, "Too many redirects"); + } catch (MalformedURLException e) { + return fail(ERROR_INVALID_URL, "Failed to parse URL"); + } catch (SocketTimeoutException e) { + return fail(ERROR_TIMEOUT, "Request timed out"); + } catch (IOException e) { + String message = e.getMessage(); + return fail(ERROR_NETWORK, message != null ? message : e.toString()); + } catch (ClassCastException e) { + return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported"); + } + } + + private static void applyHeaders(HttpsURLConnection connection, String[] names, + String[] values) { + if (names == null || values == null) { + return; + } + + int count = Math.min(names.length, values.length); + for (int i = 0; i < count; ++i) { + if (names[i] != null && values[i] != null) { + connection.setRequestProperty(names[i], values[i]); + } + } + } + + private static boolean isHttps(URL url) { + return "https".equalsIgnoreCase(url.getProtocol()); + } + + private static boolean isRedirect(int statusCode) { + return statusCode == HttpURLConnection.HTTP_MOVED_PERM || + statusCode == HttpURLConnection.HTTP_MOVED_TEMP || + statusCode == HttpURLConnection.HTTP_SEE_OTHER || + statusCode == 307 || + statusCode == 308; + } + + private static byte[] readBody(HttpsURLConnection connection, int statusCode, + long maxBodyBytes) throws IOException, + ResponseTooLargeException { + InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ? + connection.getErrorStream() : connection.getInputStream(); + if (stream == null) { + return new byte[0]; + } + + try (InputStream bodyStream = stream; + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + byte[] buffer = new byte[8192]; + long total = 0; + while (true) { + int read = bodyStream.read(buffer); + if (read < 0) { + return out.toByteArray(); + } + if (read == 0) { + continue; + } + if (read > maxBodyBytes || total > maxBodyBytes - read) { + throw new ResponseTooLargeException(out.toByteArray()); + } + out.write(buffer, 0, read); + total += read; + } + } + } + + private static int safeStatusCode(HttpsURLConnection connection) { + try { + return connection.getResponseCode(); + } catch (IOException e) { + return 0; + } + } + + private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) { + HeaderLists headers = readHeaders(connection); + return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body); + } + + private static Response fail(int error, String message) { + return new Response(error, message, 0, null, null, null); + } + + private static Response fail(int error, String message, int statusCode, + HttpsURLConnection connection, byte[] body) { + HeaderLists headers = readHeaders(connection); + return new Response(error, message, statusCode, headers.names, headers.values, body); + } + + private static HeaderLists readHeaders(HttpsURLConnection connection) { + List names = new ArrayList<>(); + List values = new ArrayList<>(); + + Map> headerFields = connection.getHeaderFields(); + if (headerFields == null) { + return new HeaderLists(new String[0], new String[0]); + } + + for (Map.Entry> entry : headerFields.entrySet()) { + String name = entry.getKey(); + if (name == null) { + continue; + } + List entryValues = entry.getValue(); + if (entryValues == null || entryValues.isEmpty()) { + names.add(name); + values.add(""); + continue; + } + for (String value : entryValues) { + names.add(name); + values.add(value != null ? value : ""); + } + } + + return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0])); + } + + private static final class HeaderLists { + final String[] names; + final String[] values; + + HeaderLists(String[] names, String[] values) { + this.names = names; + this.values = values; + } + } + + private static final class ResponseTooLargeException extends Exception { + final byte[] partialBody; + + ResponseTooLargeException(byte[] partialBody) { + this.partialBody = partialBody; + } + } +} diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java index b91a8211b1..1fb2bfb4a7 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -256,6 +256,7 @@ public class HIDDeviceManager { 0x24c6, // PowerA 0x2c22, // Qanba 0x2dc8, // 8BitDo + 0x37d7, // Flydigi 0x9886, // ASTRO Gaming }; diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java index 9548c529c0..42f5a911f7 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh private static final String TAG = "SDL"; private static final int SDL_MAJOR_VERSION = 3; private static final int SDL_MINOR_VERSION = 4; - private static final int SDL_MICRO_VERSION = 2; + private static final int SDL_MICRO_VERSION = 4; /* // Display InputType.SOURCE/CLASS of events and devices // @@ -2032,7 +2032,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh try { ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode); return pfd != null ? pfd.detachFd() : -1; - } catch (FileNotFoundException e) { + } catch (FileNotFoundException | SecurityException e) { e.printStackTrace(); return -1; } @@ -2227,4 +2227,3 @@ class SDLClipboardHandler implements SDLActivity.onNativeClipboardChanged(); } } - diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java index 027b8fd3e5..fdc2994c15 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java @@ -65,17 +65,15 @@ class SDLInputConnection extends BaseInputConnection @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { - if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) { - // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection - // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 - if (beforeLength > 0 && afterLength == 0) { - // backspace(s) - while (beforeLength-- > 0) { - nativeGenerateScancodeForUnichar('\b'); - } - return true; - } - } + // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection + // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 + if (beforeLength > 0 && afterLength == 0) { + // backspace(s) + while (beforeLength-- > 0) { + nativeGenerateScancodeForUnichar('\b'); + } + return true; + } if (!super.deleteSurroundingText(beforeLength, afterLength)) { return false; diff --git a/platforms/android/scripts/stage-jni-libs.sh b/platforms/android/scripts/stage-jni-libs.sh old mode 100644 new mode 100755 index ae25522e4d..42b08c13e6 --- a/platforms/android/scripts/stage-jni-libs.sh +++ b/platforms/android/scripts/stage-jni-libs.sh @@ -14,9 +14,22 @@ if [[ -z "$ANDROID_NDK_VER" ]] && [[ -d "$ANDROID_HOME_DIR/ndk" ]]; then fi if [[ -n "$ANDROID_NDK_VER" ]]; then - TOOLCHAIN_BIN="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/bin" - if [[ -x "$TOOLCHAIN_BIN/llvm-strip" ]]; then - STRIP_TOOL="$TOOLCHAIN_BIN/llvm-strip" + case "$(uname -s)" in + Darwin) HOST_TAG="darwin-x86_64" ;; + Linux) HOST_TAG="linux-x86_64" ;; + *) HOST_TAG="" ;; + esac + + PREBUILT_DIR="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt" + if [[ -n "$HOST_TAG" && -x "$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" ]]; then + STRIP_TOOL="$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" + else + for candidate in "$PREBUILT_DIR"/*/bin/llvm-strip; do + if [[ -x "$candidate" ]]; then + STRIP_TOOL="$candidate" + break + fi + done fi fi @@ -25,29 +38,35 @@ copy_lib() { local src="$2" local dst_dir="$APP_DIR/$abi" local dst="$dst_dir/libmain.so" + local tmp="$dst_dir/.libmain.so.$$" + if [[ ! -f "$src" ]]; then + echo "Missing native library for $abi: $src" >&2 + exit 1 + fi + mkdir -p "$dst_dir" - cp -f "$src" "$dst" + cp -f "$src" "$tmp" if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then - "$STRIP_TOOL" --strip-debug "$dst" - echo "Staged and stripped $src -> $dst" + "$STRIP_TOOL" --strip-unneeded "$tmp" + mv -f "$tmp" "$dst" + echo "Stripped and staged $src -> $dst" else + mv -f "$tmp" "$dst" echo "Staged $src -> $dst (strip disabled or strip tool unavailable)" fi } -declare -A ABI_TO_LIB=( - ["arm64-v8a"]="$ROOT_DIR/build/android-arm64/libmain.so" - ["x86_64"]="$ROOT_DIR/build/android-x86_64/libmain.so" -) - # Drop any previously staged ABI directories to avoid stale APK contents. rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64" for abi in $ANDROID_STAGE_ABIS; do - src="${ABI_TO_LIB[$abi]:-}" - if [[ -z "$src" ]]; then - echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2 - exit 1 - fi + case "$abi" in + arm64-v8a) src="$ROOT_DIR/build/android-arm64/libmain.so" ;; + x86_64) src="$ROOT_DIR/build/android-x86_64/libmain.so" ;; + *) + echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2 + exit 1 + ;; + esac copy_lib "$abi" "$src" done diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index 05c8440b55..3ce4d51068 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -163,30 +163,30 @@ icon { } icon.arrow-forward { - width: 24dp; - height: 24dp; - font-size: 24dp; + width: 1.2em; + height: 1.2em; + font-size: 1.2em; decorator: text("" center center); } icon.trophy { - width: 24dp; - height: 24dp; - font-size: 24dp; + width: 1.2em; + height: 1.2em; + font-size: 1.2em; decorator: text("" center center); } icon.controller { - width: 24dp; - height: 24dp; - font-size: 24dp; + width: 1.2em; + height: 1.2em; + font-size: 1.2em; decorator: text("" center center); } icon.warning { - width: 24dp; - height: 24dp; - font-size: 24dp; + width: 1.2em; + height: 1.2em; + font-size: 1.2em; decorator: text("" center center); } @@ -274,3 +274,18 @@ logo img.outer { transform: rotate(360deg); } } + +@media (max-height: 640dp) { + toast { + top: 20dp; + right: 20dp; + } + + toast.controller-warning { + bottom: 20dp; + } + + toast.menu-notification { + top: 20dp; + } +} diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss index 9b83db4660..707d8b8234 100644 --- a/res/rml/prelaunch.rcss +++ b/res/rml/prelaunch.rcss @@ -275,7 +275,6 @@ body.mirrored version-info { .update { display: none; - font-size: 16dp; color: #A6A09B; align-items: center; justify-content: flex-end; @@ -375,8 +374,8 @@ body.animate-in .intro-item { } menu { - left: 20dp; - right: 20dp; + left: 32dp; + right: 32dp; width: auto; min-width: 0; max-width: none; @@ -387,8 +386,8 @@ body.animate-in .intro-item { } body.mirrored menu { - left: 20dp; - right: 20dp; + left: 32dp; + right: 32dp; flex-direction: row-reverse; } @@ -396,7 +395,7 @@ body.animate-in .intro-item { flex: 1 1 0; min-width: 0; max-width: 48%; - + margin-left: 32dp; } body.mirrored hero { @@ -438,9 +437,61 @@ body.animate-in .intro-item { decorator: horizontal-gradient(#FEE685FF #FEE68500); } - .eyebrow, - disc-info, - version-info { + .eyebrow { display: none; } + + disc-info { + right: 32dp; + left: auto; + bottom: 32dp; + top: auto; + text-align: right; + font-size: 16dp; + } + + #disc-status { + justify-content: flex-end; + } + + #disc-status icon { + font-size: 20dp; + } + + #disc-version { + font-size: 16dp; + } + + version-info { + right: 32dp; + left: auto; + bottom: auto; + top: 32dp; + text-align: right; + font-size: 16dp; + } + + .update { + font-size: 16dp; + } + + body.mirrored disc-info { + right: auto; + left: 32dp; + bottom: 32dp; + top: auto; + text-align: left; + } + + body.mirrored version-info { + right: auto; + left: 32dp; + bottom: auto; + top: 32dp; + text-align: left; + } + + body.mirrored #disc-status { + justify-content: flex-start; + } } diff --git a/src/dusk/file_select.cpp b/src/dusk/file_select.cpp index fcda233c56..188d491bf3 100644 --- a/src/dusk/file_select.cpp +++ b/src/dusk/file_select.cpp @@ -1,9 +1,16 @@ #include "file_select.hpp" #include +#include #include #include +#include + +#if defined(__ANDROID__) || defined(ANDROID) +#include +#include +#endif #if defined(__APPLE__) #include @@ -19,6 +26,92 @@ namespace dusk { namespace { +std::string fallback_display_name(std::string_view path) { + if (path.empty()) { + return {}; + } + + std::string pathString(path); + const std::size_t slash = pathString.find_last_of("/\\"); + if (slash == std::string::npos || slash + 1 >= pathString.size()) { + return pathString; + } + return pathString.substr(slash + 1); +} + +#if defined(__ANDROID__) || defined(ANDROID) +bool clear_pending_exception(JNIEnv* env) { + if (env == nullptr || !env->ExceptionCheck()) { + return false; + } + env->ExceptionClear(); + return true; +} + +std::string to_string(JNIEnv* env, jstring value) { + if (env == nullptr || value == nullptr) { + return {}; + } + + const char* utf8 = env->GetStringUTFChars(value, nullptr); + if (utf8 == nullptr) { + clear_pending_exception(env); + return {}; + } + + std::string result(utf8); + env->ReleaseStringUTFChars(value, utf8); + return result; +} + +std::string android_display_name(std::string_view path) { + auto* env = static_cast(SDL_GetAndroidJNIEnv()); + if (env == nullptr) { + return {}; + } + + jobject activity = static_cast(SDL_GetAndroidActivity()); + if (activity == nullptr || clear_pending_exception(env)) { + if (activity != nullptr) { + env->DeleteLocalRef(activity); + } + return {}; + } + + jclass activityClass = env->GetObjectClass(activity); + if (activityClass == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return {}; + } + + jmethodID getDisplayName = env->GetMethodID( + activityClass, "getDisplayNameForUri", "(Ljava/lang/String;)Ljava/lang/String;"); + env->DeleteLocalRef(activityClass); + if (getDisplayName == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return {}; + } + + jstring uri = env->NewStringUTF(std::string(path).c_str()); + if (uri == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return {}; + } + + auto* displayName = + static_cast(env->CallObjectMethod(activity, getDisplayName, uri)); + env->DeleteLocalRef(uri); + env->DeleteLocalRef(activity); + if (displayName == nullptr || clear_pending_exception(env)) { + return {}; + } + + std::string result = to_string(env, displayName); + env->DeleteLocalRef(displayName); + return result; +} +#endif + #if USE_IOS_DIALOG struct IOSDialogCallbackState { FileCallback callback; @@ -88,4 +181,16 @@ void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window, default_location, allow_many); #endif } + +std::string display_name_for_path(std::string_view path) { +#if defined(__ANDROID__) || defined(ANDROID) + if (path.starts_with("content:") || path.starts_with("file:")) { + std::string displayName = android_display_name(path); + if (!displayName.empty()) { + return displayName; + } + } +#endif + return fallback_display_name(path); +} } // namespace dusk diff --git a/src/dusk/file_select.hpp b/src/dusk/file_select.hpp index 175c5aa086..8bef51cfe6 100644 --- a/src/dusk/file_select.hpp +++ b/src/dusk/file_select.hpp @@ -2,6 +2,9 @@ #include +#include +#include + struct SDL_Window; namespace dusk { @@ -9,7 +12,9 @@ namespace dusk { using FileCallback = void (*)(void* userdata, const char* path, const char* error); void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window, - const SDL_DialogFileFilter* filters, int nfilters, const char* default_location, - bool allow_many); + const SDL_DialogFileFilter* filters, int nfilters, const char* default_location, + bool allow_many); + +std::string display_name_for_path(std::string_view path); } // namespace dusk diff --git a/src/dusk/http/android.cpp b/src/dusk/http/android.cpp new file mode 100644 index 0000000000..7debcc7ce2 --- /dev/null +++ b/src/dusk/http/android.cpp @@ -0,0 +1,402 @@ +#include "http.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace dusk::http { +namespace { + +constexpr int JavaErrorNone = 0; +constexpr int JavaErrorInvalidUrl = 1; +constexpr int JavaErrorUnsupportedScheme = 2; +constexpr int JavaErrorTimeout = 3; +constexpr int JavaErrorTooLarge = 4; + +int timeout_ms(std::chrono::milliseconds timeout) { + const auto count = std::max(1, timeout.count()); + return static_cast( + std::min(count, std::numeric_limits::max())); +} + +jlong max_body_bytes(size_t maxBodyBytes) { + return static_cast(std::min( + maxBodyBytes, static_cast(std::numeric_limits::max()))); +} + +bool clear_pending_exception(JNIEnv* env) { + if (env == nullptr || !env->ExceptionCheck()) { + return false; + } + env->ExceptionClear(); + return true; +} + +std::string to_string(JNIEnv* env, jstring value) { + if (env == nullptr || value == nullptr) { + return {}; + } + + const char* utf8 = env->GetStringUTFChars(value, nullptr); + if (utf8 == nullptr) { + clear_pending_exception(env); + return {}; + } + + std::string result(utf8); + env->ReleaseStringUTFChars(value, utf8); + return result; +} + +jstring to_jstring(JNIEnv* env, std::string_view value) { + if (env == nullptr) { + return nullptr; + } + return env->NewStringUTF(std::string(value).c_str()); +} + +Error map_java_error(int error) { + switch (error) { + case JavaErrorNone: + return Error::None; + case JavaErrorInvalidUrl: + return Error::InvalidUrl; + case JavaErrorUnsupportedScheme: + return Error::UnsupportedScheme; + case JavaErrorTimeout: + return Error::Timeout; + case JavaErrorTooLarge: + return Error::TooLarge; + default: + return Error::Network; + } +} + +jclass load_dusk_class(JNIEnv* env, jobject activity, const char* className) { + jclass activityClass = env->GetObjectClass(activity); + if (activityClass == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + jmethodID getClassLoader = + env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;"); + env->DeleteLocalRef(activityClass); + if (getClassLoader == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + jobject classLoader = env->CallObjectMethod(activity, getClassLoader); + if (classLoader == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + jclass classLoaderClass = env->FindClass("java/lang/ClassLoader"); + if (classLoaderClass == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(classLoader); + return nullptr; + } + + jmethodID loadClass = env->GetMethodID( + classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); + env->DeleteLocalRef(classLoaderClass); + if (loadClass == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(classLoader); + return nullptr; + } + + jstring javaClassName = env->NewStringUTF(className); + if (javaClassName == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(classLoader); + return nullptr; + } + + auto* loadedClass = + static_cast(env->CallObjectMethod(classLoader, loadClass, javaClassName)); + env->DeleteLocalRef(javaClassName); + env->DeleteLocalRef(classLoader); + if (loadedClass == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + return loadedClass; +} + +jobjectArray make_string_array(JNIEnv* env, const std::vector
& headers, bool names) { + jclass stringClass = env->FindClass("java/lang/String"); + if (stringClass == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + jobjectArray array = + env->NewObjectArray(static_cast(headers.size()), stringClass, nullptr); + env->DeleteLocalRef(stringClass); + if (array == nullptr || clear_pending_exception(env)) { + return nullptr; + } + + for (jsize i = 0; i < static_cast(headers.size()); ++i) { + const std::string& value = names ? headers[static_cast(i)].name : + headers[static_cast(i)].value; + jstring javaValue = to_jstring(env, value); + if (javaValue == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(array); + return nullptr; + } + env->SetObjectArrayElement(array, i, javaValue); + env->DeleteLocalRef(javaValue); + if (clear_pending_exception(env)) { + env->DeleteLocalRef(array); + return nullptr; + } + } + + return array; +} + +std::vector
read_headers(JNIEnv* env, jobjectArray names, jobjectArray values) { + std::vector
headers; + if (names == nullptr || values == nullptr) { + return headers; + } + + const jsize count = std::min(env->GetArrayLength(names), env->GetArrayLength(values)); + headers.reserve(static_cast(count)); + for (jsize i = 0; i < count; ++i) { + auto* name = static_cast(env->GetObjectArrayElement(names, i)); + auto* value = static_cast(env->GetObjectArrayElement(values, i)); + if (clear_pending_exception(env)) { + if (name != nullptr) { + env->DeleteLocalRef(name); + } + if (value != nullptr) { + env->DeleteLocalRef(value); + } + headers.clear(); + return headers; + } + + if (name != nullptr) { + headers.push_back({ + .name = to_string(env, name), + .value = to_string(env, value), + }); + } + + if (name != nullptr) { + env->DeleteLocalRef(name); + } + if (value != nullptr) { + env->DeleteLocalRef(value); + } + } + + return headers; +} + +std::string read_body(JNIEnv* env, jbyteArray body) { + if (body == nullptr) { + return {}; + } + + const jsize bodySize = env->GetArrayLength(body); + std::string result(static_cast(bodySize), '\0'); + if (bodySize > 0) { + env->GetByteArrayRegion(body, 0, bodySize, reinterpret_cast(result.data())); + if (clear_pending_exception(env)) { + return {}; + } + } + return result; +} + +Result result_from_response(JNIEnv* env, jobject response) { + if (response == nullptr) { + return { + .error = Error::Network, + .message = "Android HTTP request did not return a response", + }; + } + + jclass responseClass = env->GetObjectClass(response); + if (responseClass == nullptr || clear_pending_exception(env)) { + return { + .error = Error::Network, + .message = "Failed to inspect Android HTTP response", + }; + } + + jfieldID errorField = env->GetFieldID(responseClass, "error", "I"); + jfieldID messageField = env->GetFieldID(responseClass, "message", "Ljava/lang/String;"); + jfieldID statusField = env->GetFieldID(responseClass, "statusCode", "I"); + jfieldID headerNamesField = + env->GetFieldID(responseClass, "headerNames", "[Ljava/lang/String;"); + jfieldID headerValuesField = + env->GetFieldID(responseClass, "headerValues", "[Ljava/lang/String;"); + jfieldID bodyField = env->GetFieldID(responseClass, "body", "[B"); + env->DeleteLocalRef(responseClass); + if (errorField == nullptr || messageField == nullptr || statusField == nullptr || + headerNamesField == nullptr || headerValuesField == nullptr || bodyField == nullptr || + clear_pending_exception(env)) + { + return { + .error = Error::Network, + .message = "Android HTTP response shape was not recognized", + }; + } + + const int javaError = env->GetIntField(response, errorField); + auto* message = static_cast(env->GetObjectField(response, messageField)); + auto* headerNames = static_cast(env->GetObjectField(response, headerNamesField)); + auto* headerValues = + static_cast(env->GetObjectField(response, headerValuesField)); + auto* body = static_cast(env->GetObjectField(response, bodyField)); + if (clear_pending_exception(env)) { + return { + .error = Error::Network, + .message = "Failed to read Android HTTP response", + }; + } + + Response httpResponse{ + .statusCode = static_cast(env->GetIntField(response, statusField)), + .headers = read_headers(env, headerNames, headerValues), + .body = read_body(env, body), + }; + + std::string messageString = to_string(env, message); + + if (message != nullptr) { + env->DeleteLocalRef(message); + } + if (headerNames != nullptr) { + env->DeleteLocalRef(headerNames); + } + if (headerValues != nullptr) { + env->DeleteLocalRef(headerValues); + } + if (body != nullptr) { + env->DeleteLocalRef(body); + } + + return { + .error = map_java_error(javaError), + .message = std::move(messageString), + .response = std::move(httpResponse), + }; +} + +} // namespace + +bool available() noexcept { + return true; +} + +Backend backend() noexcept { + return Backend::Android; +} + +const char* backend_name() noexcept { + return "Android"; +} + +Result get(const Request& request) { + if (request.url.empty()) { + return { + .error = Error::InvalidUrl, + .message = "URL is empty", + }; + } + if (!request.url.starts_with("https://")) { + return { + .error = Error::UnsupportedScheme, + .message = "Only https:// URLs are supported", + }; + } + + auto* env = static_cast(SDL_GetAndroidJNIEnv()); + if (env == nullptr) { + return { + .error = Error::Network, + .message = "Failed to access Android JNI environment", + }; + } + + jobject activity = static_cast(SDL_GetAndroidActivity()); + if (activity == nullptr || clear_pending_exception(env)) { + if (activity != nullptr) { + env->DeleteLocalRef(activity); + } + return { + .error = Error::Network, + .message = "Failed to access Android activity", + }; + } + + jclass clientClass = + load_dusk_class(env, activity, "com.twilitrealm.dusk.DuskHttpClient"); + env->DeleteLocalRef(activity); + if (clientClass == nullptr) { + return { + .error = Error::Network, + .message = "Failed to load Android HTTP helper", + }; + } + + jmethodID getMethod = env->GetStaticMethodID(clientClass, "get", + "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;IJ)" + "Lcom/twilitrealm/dusk/DuskHttpClient$Response;"); + if (getMethod == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(clientClass); + return { + .error = Error::Network, + .message = "Failed to find Android HTTP helper method", + }; + } + + jstring url = to_jstring(env, request.url); + jobjectArray headerNames = make_string_array(env, request.headers, true); + jobjectArray headerValues = make_string_array(env, request.headers, false); + if (url == nullptr || headerNames == nullptr || headerValues == nullptr || + clear_pending_exception(env)) + { + if (url != nullptr) { + env->DeleteLocalRef(url); + } + if (headerNames != nullptr) { + env->DeleteLocalRef(headerNames); + } + if (headerValues != nullptr) { + env->DeleteLocalRef(headerValues); + } + env->DeleteLocalRef(clientClass); + return { + .error = Error::Network, + .message = "Failed to prepare Android HTTP request", + }; + } + + jobject response = env->CallStaticObjectMethod(clientClass, getMethod, url, headerNames, + headerValues, timeout_ms(request.timeout), max_body_bytes(request.maxBodyBytes)); + env->DeleteLocalRef(url); + env->DeleteLocalRef(headerNames); + env->DeleteLocalRef(headerValues); + env->DeleteLocalRef(clientClass); + if (clear_pending_exception(env)) { + return { + .error = Error::Network, + .message = "Android HTTP request failed with a Java exception", + }; + } + + Result result = result_from_response(env, response); + if (response != nullptr) { + env->DeleteLocalRef(response); + } + return result; +} + +} // namespace dusk::http diff --git a/src/dusk/http/http.hpp b/src/dusk/http/http.hpp index d62b031fbc..54bef6eec1 100644 --- a/src/dusk/http/http.hpp +++ b/src/dusk/http/http.hpp @@ -13,6 +13,7 @@ enum class Backend { WinHttp, UrlSession, LibCurl, + Android, }; enum class Error { diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index aa31540898..0b9ee8e695 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -82,6 +82,7 @@ UserSettings g_userSettings = { .freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f}, .debugFlyCam {"game.debugFlyCam", false}, .debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true}, + .allowBackgroundInput {"game.allowBackgroundInput", true}, // Cheats .infiniteHearts {"game.infiniteHearts", false}, @@ -215,6 +216,7 @@ void registerSettings() { Register(g_userSettings.game.freeCamera); Register(g_userSettings.game.debugFlyCam); Register(g_userSettings.game.debugFlyCamLockEvents); + Register(g_userSettings.game.allowBackgroundInput); Register(g_userSettings.backend.isoPath); Register(g_userSettings.backend.isoVerification); diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index 87f00c3809..5c41958409 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -445,8 +445,7 @@ private: } if (mFileName != nullptr) { - std::string fileName = - std::filesystem::path(sDiscVerificationTask->path).filename().string(); + std::string fileName = display_name_for_path(sDiscVerificationTask->path); if (fileName.empty()) { fileName = sDiscVerificationTask->path; } diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index e41dbbe46a..9645779b46 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -6,6 +6,7 @@ #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" #include "dusk/config.hpp" +#include "dusk/file_select.hpp" #include "dusk/imgui/ImGuiEngine.hpp" #include "dusk/livesplit.h" #include "graphics_tuner.hpp" @@ -317,7 +318,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { if (path.empty()) { display = "(none)"; } else { - display = std::filesystem::path(path).filename().string(); + display = display_name_for_path(path); if (display.empty()) { display = path; } @@ -612,6 +613,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { pane.clear(); pane.add_text("Open controller binding configuration."); }); + config_bool_select(leftPane, rightPane, getSettings().game.allowBackgroundInput, + { + .key = "Allow Background Input", + .helpText = "Allow controller input even when the game window is not focused.", + .onChange = [](bool value) { aurora_set_background_input(value); }, + }); leftPane.add_section("Camera"); addOption("Free Camera", getSettings().game.freeCamera, @@ -923,6 +930,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .helpText = "Checks GitHub releases for a new Dusk version on startup.

" "No personal information is transmitted or collected.", }); + config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost, + { + .key = "Pause On Focus Lost", + .helpText = "Pause the game when window focus is lost.", + .onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); }, + }); config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings, { .key = "Enable Advanced Settings", diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp index a646527ee5..5b1ba7bb4a 100644 --- a/src/dusk/ui/ui.cpp +++ b/src/dusk/ui/ui.cpp @@ -255,11 +255,7 @@ void update() noexcept { } std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept { - const char* basePath = SDL_GetBasePath(); - if (basePath == nullptr) { - return std::filesystem::path("res") / filename; - } - return std::filesystem::path(basePath) / "res" / filename; + return std::filesystem::path("res") / filename; } std::string escape(std::string_view str) noexcept { diff --git a/src/dusk/ui/window.cpp b/src/dusk/ui/window.cpp index 55ce96c69f..41080ff287 100644 --- a/src/dusk/ui/window.cpp +++ b/src/dusk/ui/window.cpp @@ -16,10 +16,7 @@ namespace dusk::ui { namespace { float base_body_padding(Rml::Context* context) noexcept { - if (context == nullptr) { - return 64.0f; - } - const float dpRatio = std::max(context->GetDensityIndependentPixelRatio(), 0.001f); + const float dpRatio = context->GetDensityIndependentPixelRatio(); const float heightDp = static_cast(context->GetDimensions().y) / dpRatio; if (heightDp <= 640.0f) { return 16.0f * dpRatio; diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 8719998bd5..56ac05b16c 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -105,7 +105,6 @@ const int audioHeapSize = 0x14D800; bool dusk::IsRunning = true; bool dusk::IsShuttingDown = false; bool dusk::IsGameLaunched = false; -bool dusk::IsFocusPaused = false; bool dusk::RestartRequested = false; std::filesystem::path dusk::ConfigPath; #endif @@ -228,19 +227,16 @@ void main01(void) { switch (event->type) { case AURORA_NONE: goto eventsDone; + case AURORA_PAUSED: + dusk::audio::SetPaused(true); + break; + case AURORA_UNPAUSED: + dusk::audio::SetPaused(false); + dusk::game_clock::reset_frame_timer(); + break; case AURORA_SDL_EVENT: dusk::ui::handle_event(event->sdl); dusk::g_imguiConsole.HandleSDLEvent(event->sdl); - if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_LOST && - dusk::getSettings().game.pauseOnFocusLost) { - dusk::IsFocusPaused = true; - dusk::audio::SetPaused(true); - } else if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_GAINED && - dusk::IsFocusPaused) { - dusk::IsFocusPaused = false; - dusk::audio::SetPaused(false); - dusk::game_clock::reset_frame_timer(); - } break; case AURORA_DISPLAY_SCALE_CHANGED: dusk::ImGuiEngine_Initialize(event->windowSize.scale); @@ -254,19 +250,14 @@ void main01(void) { eventsDone:; - if (dusk::IsFocusPaused) { - std::this_thread::sleep_for(std::chrono::milliseconds(16)); + if (!aurora_begin_frame()) { + DuskLog.debug("aurora_begin_frame returned false, skipping draw this frame"); continue; } VIWaitForRetrace(); dusk::lastFrameAuroraStats = *aurora_get_stats(); - if (!aurora_begin_frame()) { - DuskLog.debug("aurora_begin_frame returned false, skipping draw this frame"); - continue; - } - mDoGph_gInf_c::updateRenderSize(); dusk::ui::update(); @@ -571,7 +562,8 @@ int game_main(int argc, char* argv[]) { config.logLevel = startupLogLevel; config.mem1Size = 256 * 1024 * 1024; config.mem2Size = 24 * 1024 * 1024; - config.allowJoystickBackgroundEvents = true; + config.allowJoystickBackgroundEvents = dusk::getSettings().game.allowBackgroundInput; + config.pauseOnFocusLost = dusk::getSettings().game.pauseOnFocusLost; config.imGuiInitCallback = &aurora_imgui_init_callback; config.allowTextureReplacements = true; config.allowTextureDumps = false; @@ -618,13 +610,19 @@ int game_main(int argc, char* argv[]) { // Invalidate a bad saved isoPath so that Dusk can't get blocked from starting up. // This is only a metadata check; full hash verification is handled by the prelaunch UI. + bool forcePreLaunchUI = false; + bool saveConfigBeforePrelaunch = false; + const std::string p = dusk::getSettings().backend.isoPath; dusk::iso::DiscInfo discInfo{}; if (!p.empty() && dusk::iso::inspect(p.c_str(), discInfo) != dusk::iso::ValidationError::Success) { + DuskLog.warn("Saved DVD image path failed validation, clearing configured path: {}", p); dusk::getSettings().backend.isoPath.setValue(""); dusk::getSettings().backend.isoVerification.setValue(dusk::DiscVerificationState::Unknown); + forcePreLaunchUI = true; + saveConfigBeforePrelaunch = true; } std::string dvd_path; @@ -636,6 +634,7 @@ int game_main(int argc, char* argv[]) { dvd_opened = aurora_dvd_open(dvd_path.c_str()); if (!dvd_opened) { DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path); + forcePreLaunchUI = true; } else { dusk::getSettings().backend.isoPath.setValue(dvd_path); dusk::getSettings().backend.isoVerification.setValue( @@ -645,10 +644,23 @@ int game_main(int argc, char* argv[]) { } } else { DuskLog.warn("DVD image from command line failed validation: {}, opening prelaunch UI", dvd_path); + forcePreLaunchUI = true; } } if (!dvd_opened) { + if (dusk::getSettings().backend.isoPath.getValue().empty()) { + forcePreLaunchUI = true; + } + if (forcePreLaunchUI && dusk::getSettings().backend.skipPreLaunchUI.getValue()) { + DuskLog.warn("Prelaunch UI was disabled with no usable DVD image, enabling prelaunch UI"); + dusk::getSettings().backend.skipPreLaunchUI.setValue(false); + saveConfigBeforePrelaunch = true; + } + if (saveConfigBeforePrelaunch) { + dusk::config::Save(); + } + if (!dusk::getSettings().backend.skipPreLaunchUI) { dusk::ui::push_document(std::make_unique(), true);