From 1c85ee63ebb3f08f2992dda709509e4b9ecee0b1 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 8 May 2026 16:57:19 -0600 Subject: [PATCH 01/53] Many mobile fixes, Android update check, "Background Input" & "Pause On Focus Lost" options --- CMakeLists.txt | 11 +- extern/aurora | 2 +- include/dusk/main.h | 1 - include/dusk/settings.h | 1 + platforms/android/app/build.gradle | 18 +- platforms/android/app/proguard-rules.pro | 2 + .../android/app/src/main/AndroidManifest.xml | 1 + .../com/twilitrealm/dusk/DuskActivity.java | 107 ++++- .../com/twilitrealm/dusk/DuskHttpClient.java | 237 +++++++++++ .../java/org/libsdl/app/HIDDeviceManager.java | 1 + .../main/java/org/libsdl/app/SDLActivity.java | 5 +- .../org/libsdl/app/SDLInputConnection.java | 20 +- platforms/android/scripts/stage-jni-libs.sh | 51 ++- res/rml/overlay.rcss | 39 +- res/rml/prelaunch.rcss | 69 ++- src/dusk/file_select.cpp | 105 +++++ src/dusk/file_select.hpp | 9 +- src/dusk/http/android.cpp | 402 ++++++++++++++++++ src/dusk/http/http.hpp | 1 + src/dusk/settings.cpp | 2 + src/dusk/ui/prelaunch.cpp | 3 +- src/dusk/ui/settings.cpp | 15 +- src/dusk/ui/ui.cpp | 6 +- src/dusk/ui/window.cpp | 5 +- src/m_Do/m_Do_main.cpp | 50 ++- 25 files changed, 1068 insertions(+), 95 deletions(-) create mode 100644 platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java mode change 100644 => 100755 platforms/android/scripts/stage-jni-libs.sh create mode 100644 src/dusk/http/android.cpp 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); From 84ffd67622b77f583f236d4fc4f1cc4c12cf3c5e Mon Sep 17 00:00:00 2001 From: Irastris Date: Fri, 8 May 2026 19:01:19 -0400 Subject: [PATCH 02/53] Refactor notification settings, allow disabling controller toasts (#721) * Refactor notification settings, allow disabling controller toasts * "Toasts" to "Notifications" --- include/dusk/settings.h | 4 +-- src/dusk/achievements.cpp | 2 +- src/dusk/settings.cpp | 6 ++-- src/dusk/ui/preset.cpp | 6 ++-- src/dusk/ui/settings.cpp | 67 ++++++++++++++++++++++++++++++++++++--- src/dusk/ui/ui.cpp | 64 +++++++++++++++++++------------------ 6 files changed, 108 insertions(+), 41 deletions(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index cb4945801a..d9d85fa36b 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -102,8 +102,8 @@ struct UserSettings { ConfigVar minimalHUD; ConfigVar pauseOnFocusLost; ConfigVar enableLinkDollRotation; - ConfigVar enableAchievementNotifications; - + ConfigVar enableAchievementToasts; + ConfigVar enableControllerToasts; // Graphics ConfigVar bloomMode; diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index eefcda978f..cac6f776b6 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -1113,7 +1113,7 @@ void AchievementSystem::processEntry(Entry& e) { if (nowUnlocked) { e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1; e.achievement.unlocked = true; - if (getSettings().game.enableAchievementNotifications) { + if (getSettings().game.enableAchievementToasts) { ui::push_toast({ .type = "achievement", .title = "Achievement Unlocked!", diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 0b9ee8e695..c8e7178d67 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -50,7 +50,8 @@ UserSettings g_userSettings = { .minimalHUD {"game.minimalHUD", false}, .pauseOnFocusLost {"game.pauseOnFocusLost", false}, .enableLinkDollRotation {"game.enableLinkDollRotation", false}, - .enableAchievementNotifications {"game.enableAchievementNotifications", true}, + .enableAchievementToasts {"game.enableAchievementToasts", true}, + .enableControllerToasts {"game.enableControllerToasts", true}, // Graphics .bloomMode {"game.bloomMode", BloomMode::Dusk}, @@ -184,7 +185,8 @@ void registerSettings() { Register(g_userSettings.game.freeMagicArmor); Register(g_userSettings.game.restoreWiiGlitches); Register(g_userSettings.game.enableLinkDollRotation); - Register(g_userSettings.game.enableAchievementNotifications); + Register(g_userSettings.game.enableAchievementToasts); + Register(g_userSettings.game.enableControllerToasts); Register(g_userSettings.game.noMissClimbing); Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); diff --git a/src/dusk/ui/preset.cpp b/src/dusk/ui/preset.cpp index 6b6bdc2248..1d559972f5 100644 --- a/src/dusk/ui/preset.cpp +++ b/src/dusk/ui/preset.cpp @@ -14,7 +14,8 @@ void applyPresetClassic() { auto& s = getSettings(); s.video.lockAspectRatio.setValue(true); s.game.bloomMode.setValue(BloomMode::Classic); - s.game.enableAchievementNotifications.setValue(false); + s.game.enableAchievementToasts.setValue(false); + s.game.enableControllerToasts.setValue(false); s.game.internalResolutionScale.setValue(1); s.game.shadowResolutionMultiplier.setValue(1); s.game.hideTvSettingsScreen.setValue(false); @@ -33,7 +34,8 @@ void applyPresetDusk() { s.game.biggerWallets.setValue(true); s.game.invertCameraXAxis.setValue(true); s.game.no2ndFishForCat.setValue(true); - s.game.enableAchievementNotifications.setValue(true); + s.game.enableAchievementToasts.setValue(true); + s.game.enableControllerToasts.setValue(true); s.game.enableQuickTransform.setValue(true); s.game.instantSaves.setValue(true); s.game.midnasLamentNonStop.setValue(true); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 9645779b46..cbfde1ba3a 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -896,10 +896,69 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { auto& rightPane = add_child(content, Pane::Type::Uncontrolled); leftPane.add_section("Dusk"); - config_bool_select(leftPane, rightPane, getSettings().game.enableAchievementNotifications, - { - .key = "Achievement Notifications", - .helpText = "Display a toast when an achievement is unlocked.", + leftPane.register_control( + leftPane.add_select_button({ + .key = "Notifications", + .getValue = [] { + const bool ach = getSettings().game.enableAchievementToasts.getValue(); + const bool ctl = getSettings().game.enableControllerToasts.getValue(); + if (!ach && !ctl) { + return Rml::String{"Off"}; + } + if (ach && ctl) { + return Rml::String{"All"}; + } + return Rml::String{"Some"}; + }, + .isModified = [] { + const auto& ach = getSettings().game.enableAchievementToasts; + const auto& ctl = getSettings().game.enableControllerToasts; + return ach.getValue() != ach.getDefaultValue() || ctl.getValue() != ctl.getDefaultValue(); + }, + }), + rightPane, [](Pane& pane) { + pane.clear(); + pane.add_button("Select All").on_pressed([] { + mDoAud_seStartMenu(kSoundItemChange); + getSettings().game.enableAchievementToasts.setValue(true); + getSettings().game.enableControllerToasts.setValue(true); + config::Save(); + }); + pane.add_button("Select None").on_pressed([] { + mDoAud_seStartMenu(kSoundItemChange); + getSettings().game.enableAchievementToasts.setValue(false); + getSettings().game.enableControllerToasts.setValue(false); + config::Save(); + }); + + pane.add_section("Types"); + pane.add_button( + { + .text = "Achievements", + .isSelected = + [] { + return getSettings().game.enableAchievementToasts.getValue(); + }, + }) + .on_pressed([] { + mDoAud_seStartMenu(kSoundItemChange); + auto& v = getSettings().game.enableAchievementToasts; + v.setValue(!v.getValue()); + config::Save(); + }); + pane.add_button( + { + .text = "Controller", + .isSelected = + [] { return getSettings().game.enableControllerToasts.getValue(); }, + }) + .on_pressed([] { + mDoAud_seStartMenu(kSoundItemChange); + auto& v = getSettings().game.enableControllerToasts; + v.setValue(!v.getValue()); + config::Save(); + }); + pane.add_rml("
Choose which notifications can be displayed."); }); #if DUSK_ENABLE_SENTRY_NATIVE config_bool_select(leftPane, rightPane, getSettings().backend.enableCrashReporting, diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp index 5b1ba7bb4a..0ee59e7938 100644 --- a/src/dusk/ui/ui.cpp +++ b/src/dusk/ui/ui.cpp @@ -125,43 +125,47 @@ void handle_event(const SDL_Event& event) noexcept { if (event.type == SDL_EVENT_GAMEPAD_ADDED) { auto* gamepad = SDL_GetGamepadFromID(event.gdevice.which); if (SDL_GamepadConnected(gamepad)) { - const char* name = SDL_GetGamepadName(gamepad); - Rml::String content = fmt::format("{}", name ? name : "[Unknown]"); - Rml::String title = "Controller connected"; - if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) { - title = fmt::format( - "{} &#x{};", title, - icon); - } - int batteryLevel = -1; - const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel); - if (powerState != SDL_POWERSTATE_UNKNOWN) { - content = fmt::format( - "{}&#x{};", - content, battery_icon(powerState, batteryLevel)); - if (batteryLevel > -1) { - content = fmt::format("{} {}%", content, batteryLevel); + if (getSettings().game.enableControllerToasts) { + const char* name = SDL_GetGamepadName(gamepad); + Rml::String content = fmt::format("{}", name ? name : "[Unknown]"); + Rml::String title = "Controller connected"; + if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) { + title = fmt::format( + "{} &#x{};", title, + icon); } - content += ""; + int batteryLevel = -1; + const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel); + if (powerState != SDL_POWERSTATE_UNKNOWN) { + content = fmt::format( + "{}&#x{};", + content, battery_icon(powerState, batteryLevel)); + if (batteryLevel > -1) { + content = fmt::format("{} {}%", content, batteryLevel); + } + content += ""; + } + push_toast({ + .type = "controller", + .title = title, + .content = content, + .duration = std::chrono::seconds(4), + }); } - push_toast({ - .type = "controller", - .title = title, - .content = content, - .duration = std::chrono::seconds(4), - }); sConnectedGamepads.insert(event.gdevice.which); } } else if (event.type == SDL_EVENT_GAMEPAD_REMOVED && sConnectedGamepads.contains(event.gdevice.which)) { - const char* name = SDL_GetGamepadNameForID(event.gdevice.which); - push_toast({ - .type = "controller", - .title = "Controller disconnected", - .content = name ? name : "[Unknown]", - .duration = std::chrono::seconds(4), - }); + if (getSettings().game.enableControllerToasts) { + const char* name = SDL_GetGamepadNameForID(event.gdevice.which); + push_toast({ + .type = "controller", + .title = "Controller disconnected", + .content = name ? name : "[Unknown]", + .duration = std::chrono::seconds(4), + }); + } sConnectedGamepads.erase(event.gdevice.which); } input::handle_event(event); From 4d67033ff8f70519f576f13809db0ec3baeee6f6 Mon Sep 17 00:00:00 2001 From: Tom Lube Date: Fri, 8 May 2026 18:45:05 -0400 Subject: [PATCH 03/53] Remove reset via CTRL + R --- src/dusk/imgui/ImGuiConsole.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index e933e314e4..4793b717cc 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -253,13 +253,6 @@ namespace dusk { UpdateSettings(); - if (!fpcM_SearchByName(fpcNm_LOGO_SCENE_e) && - (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && - ImGui::IsKeyPressed(ImGuiKey_R)) - { - JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; - } - if (ImGui::IsKeyPressed(ImGuiKey_F11)) { getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen); VISetWindowFullscreen(getSettings().video.enableFullscreen); From 699d069b0ac05b7f1e4d5f53cb6feb87deb251e4 Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Fri, 8 May 2026 19:10:37 -0400 Subject: [PATCH 04/53] Fix Letter Menu Page Numbers (#722) This bug comes from `getBounds()` returning a reference to a static variable -- the compiler can load the float right away or later (after the second call), so the value it is referencing may have changed. If it waits to load the first operand, the subtraction results in 0. Further discussion at https://discord.com/channels/1446645861529550869/1446648842387722411/1502415404386091118. Resolves #719 --- src/d/d_menu_letter.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/d/d_menu_letter.cpp b/src/d/d_menu_letter.cpp index 589b2553c0..4bfda4b9cd 100644 --- a/src/d/d_menu_letter.cpp +++ b/src/d/d_menu_letter.cpp @@ -965,7 +965,8 @@ void dMenu_Letter_c::screenSetBase() { } if (field_0x374 > 1) { J2DPane* pJVar6 = mpBaseScreen->search('pi_n'); - f32 dVar18 = field_0x1f0[1]->getBounds().i.x - field_0x1f0[0]->getBounds().i.x; + f32 x1 = field_0x1f0[1]->getBounds().i.x; + f32 dVar18 = x1 - field_0x1f0[0]->getBounds().i.x; f32 dVar17 = dVar18 * (field_0x374 - 1); f32 dVar16 = (pJVar6->getWidth() / 2) - (dVar17 / 2); for (int i = 0; i < 9; i++) { From abec043249d47143aa6980dfec14cac4baa88c72 Mon Sep 17 00:00:00 2001 From: Irastris Date: Fri, 8 May 2026 19:11:13 -0400 Subject: [PATCH 05/53] Add mouse as a gyro input source (#720) * Add mouse as a gyro input source * Revisions * Grammar --- include/dusk/gyro.h | 1 + include/dusk/settings.h | 12 +++++ src/d/actor/d_a_mg_fshop.cpp | 8 +-- src/dusk/config.cpp | 1 + src/dusk/gyro.cpp | 101 ++++++++++++++++++++++++++++------- src/dusk/settings.cpp | 2 + src/dusk/ui/settings.cpp | 56 +++++++++++++++++-- 7 files changed, 155 insertions(+), 26 deletions(-) diff --git a/include/dusk/gyro.h b/include/dusk/gyro.h index 279aeae16e..a206100739 100644 --- a/include/dusk/gyro.h +++ b/include/dusk/gyro.h @@ -12,6 +12,7 @@ void rollgoalTableOffset(s16& out_ax, s16& out_az); extern bool s_sensor_keep_alive; bool get_sensor_keep_alive(); void set_sensor_keep_alive(bool value); +bool rollgoal_gyro_enabled(); } // namespace dusk::gyro #endif diff --git a/include/dusk/settings.h b/include/dusk/settings.h index d9d85fa36b..f74a4bf61f 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -27,6 +27,11 @@ enum class DiscVerificationState : u8 { HashMismatch, }; +enum class GyroMode : u8 { + Sensor = 0, + Mouse = 1, +}; + namespace config { template <> struct ConfigEnumRange { @@ -45,6 +50,12 @@ struct ConfigEnumRange { static constexpr auto min = DiscVerificationState::Unknown; static constexpr auto max = DiscVerificationState::HashMismatch; }; + +template <> +struct ConfigEnumRange { + static constexpr auto min = GyroMode::Sensor; + static constexpr auto max = GyroMode::Mouse; +}; } // Persistent user settings @@ -120,6 +131,7 @@ struct UserSettings { ConfigVar midnasLamentNonStop; // Input + ConfigVar gyroMode; ConfigVar enableGyroAim; ConfigVar enableGyroRollgoal; ConfigVar gyroSensitivityX; diff --git a/src/d/actor/d_a_mg_fshop.cpp b/src/d/actor/d_a_mg_fshop.cpp index f0905efd5d..0d5b6dd2c9 100644 --- a/src/d/actor/d_a_mg_fshop.cpp +++ b/src/d/actor/d_a_mg_fshop.cpp @@ -729,10 +729,12 @@ static void koro2_game(fshop_class* i_this) { cLib_addCalcAngleS2(&i_this->field_0x4020.z, 0, 2, 0x200); case 2: #if TARGET_PC - if (dusk::getSettings().game.enableGyroRollgoal) { + if (dusk::gyro::rollgoal_gyro_enabled()) { if (!dusk::gyro::get_sensor_keep_alive()) { dusk::gyro::set_sensor_keep_alive(true); } + } else if (dusk::gyro::get_sensor_keep_alive()) { + dusk::gyro::set_sensor_keep_alive(false); } #endif @@ -753,7 +755,7 @@ static void koro2_game(fshop_class* i_this) { old_stick_x = mDoCPd_c::getSubStickX(PAD_1); cLib_addCalcAngleS2(&i_this->field_0x4060, i_this->field_0x4062, 4, 0x1000); #if TARGET_PC - if (dusk::getSettings().game.enableGyroRollgoal) { + if (dusk::gyro::rollgoal_gyro_enabled()) { dusk::gyro::rollgoalTick(true, i_this->field_0x4060); } #endif @@ -791,7 +793,7 @@ static void koro2_game(fshop_class* i_this) { s16 gyro_ax = 0; s16 gyro_az = 0; #if TARGET_PC - if (dusk::getSettings().game.enableGyroRollgoal) { + if (dusk::gyro::rollgoal_gyro_enabled()) { dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az); } #endif diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index f4af7a2961..940797c9ce 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -156,6 +156,7 @@ namespace dusk::config { template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; + template class ConfigImpl; } void dusk::config::Register(ConfigVarBase& configVar) { diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index abe22909c1..4011cbb721 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -1,5 +1,9 @@ #include "dusk/gyro.h" +#include "dusk/ui/ui.hpp" #include "d/actor/d_a_alink.h" + +#include +#include #include namespace dusk::gyro { @@ -12,11 +16,14 @@ constexpr float kGravityEmaAlpha = 0.1f; constexpr float kMinGravityProjection = 0.2f; // Let roll contribute more strongly as the pad approaches an upright posture. constexpr float kRollAimBoostMax = 2.0f; +constexpr float kMousePixelToRad = 0.0025f; bool s_sensor_enabled = false; bool s_accel_enabled = false; bool s_was_aiming = false; bool s_have_gravity_baseline = false; +bool s_mouse_enabled = false; +bool s_mouse_relative = false; float s_smooth_gx = 0.0f; float s_smooth_gy = 0.0f; float s_smooth_gz = 0.0f; @@ -36,6 +43,7 @@ void reset_filter_state() { s_baseline_gravity_y = s_baseline_gravity_z = 0.0f; s_was_aiming = false; s_have_gravity_baseline = false; + s_mouse_enabled = false; s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f; s_rollgoal_ax = s_rollgoal_az = 0; } @@ -46,14 +54,29 @@ float apply_deadband(float v, float deadband_rad_s) { } return v; } + +void disable_pad_sensors() { + if (s_sensor_enabled) { + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE); + s_sensor_enabled = false; + } + if (s_accel_enabled) { + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE); + s_accel_enabled = false; + } +} } // namespace bool s_sensor_keep_alive = false; bool get_sensor_keep_alive() { return s_sensor_keep_alive; } void set_sensor_keep_alive(bool value) { s_sensor_keep_alive = value; } +bool rollgoal_gyro_enabled() { + return getSettings().game.enableGyroRollgoal && getSettings().game.gyroMode.getValue() != GyroMode::Mouse; +} + bool queryGyroAimContext() { - if (!static_cast(dusk::getSettings().game.enableGyroAim)) { + if (!static_cast(getSettings().game.enableGyroAim)) { return false; } @@ -71,15 +94,28 @@ void read(float dt) { const bool aim_just_ended = !aim_active && s_was_aiming; s_was_aiming = aim_active; + const bool mouse_mode = getSettings().game.gyroMode.getValue() == GyroMode::Mouse; + const bool mouse_gyro_active = !ui::any_document_visible() && mouse_mode && (aim_active || s_sensor_keep_alive); + SDL_Window* window = aurora::window::get_sdl_window(); + if (window != nullptr && mouse_gyro_active != s_mouse_relative && + SDL_SetWindowRelativeMouseMode(window, mouse_gyro_active)) + { + s_mouse_relative = mouse_gyro_active; + } + + if (mouse_gyro_active && !s_mouse_enabled && window != nullptr) { + const AuroraWindowSize sz = aurora::window::get_window_size(); + const float cx = static_cast(sz.width) * 0.5f; + const float cy = static_cast(sz.height) * 0.5f; + SDL_WarpMouseInWindow(window, cx, cy); + float discard_x = 0.0f; + float discard_y = 0.0f; + SDL_GetRelativeMouseState(&discard_x, &discard_y); + } + s_mouse_enabled = mouse_gyro_active; + if (!s_sensor_keep_alive && !aim_active) { - if (s_sensor_enabled) { - PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE); - s_sensor_enabled = false; - } - if (s_accel_enabled) { - PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE); - s_accel_enabled = false; - } + disable_pad_sensors(); reset_filter_state(); return; } @@ -90,6 +126,31 @@ void read(float dt) { s_have_gravity_baseline = false; } + if (mouse_mode && !mouse_gyro_active) { + s_pitch_rad = 0.0f; + s_yaw_rad = 0.0f; + s_roll_rad = 0.0f; + return; + } + + if (mouse_mode) { + disable_pad_sensors(); + + float mx_rel = 0.0f; + float my_rel = 0.0f; + SDL_GetRelativeMouseState(&mx_rel, &my_rel); + // Convert pixels to radians + s_pitch_rad = my_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX; + s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityY; + s_roll_rad = 0.0f; + + s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; + s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; + s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; + + return; + } + if (!s_sensor_enabled) { if (!PADHasSensor(PAD_CHAN0, PAD_SENSOR_GYRO)) { return; @@ -112,8 +173,8 @@ void read(float dt) { return; } - const float smooth_alpha = kGyroEmaAlphaMax + dusk::getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax); - const float deadband = dusk::getSettings().game.gyroDeadband; + const float smooth_alpha = kGyroEmaAlphaMax + getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax); + const float deadband = getSettings().game.gyroDeadband; s_smooth_gx += smooth_alpha * (gyro[0] - s_smooth_gx); s_smooth_gy += smooth_alpha * (gyro[1] - s_smooth_gy); @@ -123,8 +184,8 @@ void read(float dt) { const float yaw_rate = apply_deadband(s_smooth_gy, deadband); const float roll_rate = apply_deadband(s_smooth_gz, deadband); - s_pitch_rad = -pitch_rate * dt * dusk::getSettings().game.gyroSensitivityX; - s_roll_rad = roll_rate * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X + s_pitch_rad = -pitch_rate * dt * getSettings().game.gyroSensitivityX; + s_roll_rad = roll_rate * dt * getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X float horizontal_rate = yaw_rate; if (aim_active && s_accel_enabled) { @@ -162,11 +223,11 @@ void read(float dt) { } } - s_yaw_rad = horizontal_rate * dt * dusk::getSettings().game.gyroSensitivityY; + s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityY; - s_pitch_rad = dusk::getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; - s_yaw_rad = dusk::getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; - s_yaw_rad = dusk::getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; + s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; + s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; + s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; } void getAimDeltas(float& out_yaw, float& out_pitch) { @@ -180,9 +241,9 @@ void rollgoalTick(bool play_active, s16 camera_yaw) { return; } - float pitch_rad = -s_pitch_rad * dusk::getSettings().game.gyroSensitivityRollgoal; - float roll_rad = s_roll_rad * dusk::getSettings().game.gyroSensitivityRollgoal; - roll_rad = dusk::getSettings().game.enableMirrorMode ? -roll_rad : roll_rad; + float pitch_rad = -s_pitch_rad * getSettings().game.gyroSensitivityRollgoal; + float roll_rad = s_roll_rad * getSettings().game.gyroSensitivityRollgoal; + roll_rad = getSettings().game.enableMirrorMode ? -roll_rad : roll_rad; s_rollgoal_az += cM_rad2s(roll_rad); cXyz in(roll_rad, 0.0f, pitch_rad); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index c8e7178d67..f0d10d4226 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -68,6 +68,7 @@ UserSettings g_userSettings = { .midnasLamentNonStop {"game.midnasLamentNonStop", false}, // Input + .gyroMode {"game.gyroMode", GyroMode::Sensor}, .enableGyroAim {"game.enableGyroAim", false}, .enableGyroRollgoal {"game.enableGyroRollgoal", false}, .gyroSensitivityX {"game.gyroSensitivityX", 1.0f}, @@ -206,6 +207,7 @@ void registerSettings() { Register(g_userSettings.game.superClawshot); Register(g_userSettings.game.alwaysGreatspin); Register(g_userSettings.game.enableFrameInterpolation); + Register(g_userSettings.game.gyroMode); Register(g_userSettings.game.enableGyroAim); Register(g_userSettings.game.enableGyroRollgoal); Register(g_userSettings.game.gyroSensitivityX); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index cbfde1ba3a..fd7365a39d 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -13,6 +13,7 @@ #include "m_Do/m_Do_main.h" #include "menu_bar.hpp" #include "number_button.hpp" +#include "menu_bar.hpp" #include "pane.hpp" #include "prelaunch.hpp" #include "ui.hpp" @@ -42,6 +43,11 @@ constexpr std::array kFpsOverlayCornerNames = { "Bottom Right", }; +constexpr std::array kGyroInputModeLabels = { + "Sensor", + "Mouse", +}; + bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) { if (backend == "auto") { outBackend = BACKEND_AUTO; @@ -200,7 +206,9 @@ int float_setting_percent(ConfigVar& var) { } bool gyro_enabled() { - return getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal; + return getSettings().game.enableGyroAim || + (getSettings().game.enableGyroRollgoal && + getSettings().game.gyroMode.getValue() != GyroMode::Mouse); } struct ConfigBoolProps { @@ -634,12 +642,50 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { [] { return !getSettings().game.freeCamera; }); leftPane.add_section("Gyro"); + leftPane.register_control( + leftPane.add_select_button({ + .key = "Gyro Input Method", + .getValue = + [] { + const auto mode = getSettings().game.gyroMode.getValue(); + const auto idx = static_cast(mode); + return Rml::String{kGyroInputModeLabels[idx]}; + }, + .isModified = + [] { + return getSettings().game.gyroMode.getValue() != + getSettings().game.gyroMode.getDefaultValue(); + }, + }), + rightPane, [](Pane& pane) { + for (size_t i = 0; i < kGyroInputModeLabels.size(); i++) { + pane + .add_button({ + .text = Rml::String{kGyroInputModeLabels[i]}, + .isSelected = + [i] { + return getSettings().game.gyroMode.getValue() == static_cast(i); + }, + }) + .on_pressed([i] { + mDoAud_seStartMenu(kSoundItemChange); + const GyroMode mode = static_cast(i); + getSettings().game.gyroMode.setValue(mode); + config::Save(); + }); + } + pane.add_rml( + "
Sensor reads motion directly from a supported controller's gyro via SDL.
" + "
Mouse treats mouse input as gyro, intended for use with the Steam Deck.
" + "
Mouse input cannot currently be used with Gyro Rollgoal."); + }); addOption("Gyro Aim", getSettings().game.enableGyroAim, "Enables gyro controls while in look mode, aiming a hawk, and aiming " "supported items.

Supported items include the Slingshot, Gale Boomerang, " "Hero's Bow, Clawshot(s), Ball and Chain, and Dominion Rod."); addOption("Gyro Rollgoal", getSettings().game.enableGyroRollgoal, - "Enables gyro controls for Rollgoal in Hena's Cabin."); + "Enables gyro controls for Rollgoal in Hena's Cabin.", + [] { return getSettings().game.gyroMode.getValue() == GyroMode::Mouse; }); config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityY, "Gyro Pitch Sensitivity", "Controls vertical gyro aiming sensitivity.", 25, 400, 5, [] { return !gyro_enabled(); }); @@ -648,7 +694,11 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { [] { return !gyro_enabled(); }); config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityRollgoal, "Rollgoal Sensitivity", "Controls how strongly gyro input tilts the Rollgoal table.", - 25, 400, 5, [] { return !getSettings().game.enableGyroRollgoal; }); + 25, 400, 5, + [] { + return !getSettings().game.enableGyroRollgoal || + getSettings().game.gyroMode.getValue() == GyroMode::Mouse; + }); config_percent_select(leftPane, rightPane, getSettings().game.gyroDeadband, "Gyro Deadband", "Ignores small gyro movement to reduce drift and jitter.", 0, 50, 1, [] { return !gyro_enabled(); }); From a2c2988666c6edfc341bd44e888bbb11d0554a59 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 8 May 2026 17:12:50 -0600 Subject: [PATCH 06/53] Disable Sentry in CI builds --- CMakePresets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index 92799249a9..3b2f0cae67 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -30,7 +30,7 @@ "CMAKE_CXX_COMPILER_LAUNCHER": "sccache", "DUSK_ENABLE_SENTRY_NATIVE": { "type": "BOOL", - "value": true + "value": false }, "DUSK_SENTRY_DSN": "$env{SENTRY_DSN}", "DUSK_SENTRY_ENVIRONMENT": "production" From a4fcc10f5f30934bc3c79b01981ae13cb63da4ff Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 8 May 2026 17:25:09 -0600 Subject: [PATCH 07/53] Make version logic tolerate prerelease semver --- CMakeLists.txt | 12 +-- src/dusk/update_check.cpp | 167 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 168 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 02eaeb183e..60aeb4e3fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,13 +48,15 @@ else () message(STATUS "Unable to find git, commit information will not be available") endif () -if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+).*)?$") +if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$") set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") - if (CMAKE_MATCH_5) - set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${CMAKE_MATCH_5}") - else () - set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.0") + set(DUSK_VERSION_TWEAK "0") + if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$") + set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}") + elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$") + set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}") endif () + set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}") else () set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION") set(DUSK_VERSION_STRING "0.0.0.0") diff --git a/src/dusk/update_check.cpp b/src/dusk/update_check.cpp index 11c11795af..c1e332c432 100644 --- a/src/dusk/update_check.cpp +++ b/src/dusk/update_check.cpp @@ -5,9 +5,12 @@ #include "nlohmann/json.hpp" #include "version.h" +#include #include #include +#include #include +#include namespace dusk::update_check { namespace { @@ -20,8 +23,7 @@ struct Version { int major = 0; int minor = 0; int patch = 0; - - friend auto operator<=>(const Version&, const Version&) = default; + std::vector prerelease; }; std::string json_string(const json& value, const char* key) { @@ -57,6 +59,134 @@ bool consume(std::string_view& value, char expected) { return true; } +bool is_digit(char value) { + return value >= '0' && value <= '9'; +} + +bool is_identifier_char(char value) { + return is_digit(value) || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || value == '-'; +} + +bool is_numeric_identifier(std::string_view value) { + if (value.empty()) { + return false; + } + for (const char c : value) { + if (!is_digit(c)) { + return false; + } + } + return true; +} + +bool is_identifier_list(std::string_view value) { + if (value.empty()) { + return false; + } + + bool expectingIdentifier = true; + for (const char c : value) { + if (c == '.') { + if (expectingIdentifier) { + return false; + } + expectingIdentifier = true; + continue; + } + if (!is_identifier_char(c)) { + return false; + } + expectingIdentifier = false; + } + + return !expectingIdentifier; +} + +std::string_view trim_git_describe_suffix(std::string_view value) { + if (value.ends_with("-dirty")) { + value.remove_suffix(6); + } + if (is_numeric_identifier(value)) { + return {}; + } + + const size_t suffixStart = value.rfind('-'); + if (suffixStart != std::string_view::npos && value.substr(0, suffixStart).find('.') != std::string_view::npos + && is_numeric_identifier(value.substr(suffixStart + 1))) { + value.remove_suffix(value.size() - suffixStart); + } + return value; +} + +void split_identifiers(std::string_view value, std::vector& identifiers) { + while (!value.empty()) { + const size_t separator = value.find('.'); + if (separator == std::string_view::npos) { + identifiers.push_back(value); + return; + } + identifiers.push_back(value.substr(0, separator)); + value.remove_prefix(separator + 1); + } +} + +std::string_view trim_leading_zeroes(std::string_view value) { + while (value.size() > 1 && value.front() == '0') { + value.remove_prefix(1); + } + return value; +} + +int compare_identifier(std::string_view lhs, std::string_view rhs) { + const bool lhsNumeric = is_numeric_identifier(lhs); + const bool rhsNumeric = is_numeric_identifier(rhs); + if (lhsNumeric && rhsNumeric) { + lhs = trim_leading_zeroes(lhs); + rhs = trim_leading_zeroes(rhs); + if (lhs.size() != rhs.size()) { + return lhs.size() < rhs.size() ? -1 : 1; + } + } else if (lhsNumeric != rhsNumeric) { + return lhsNumeric ? -1 : 1; + } + + const int result = lhs.compare(rhs); + if (result < 0) { + return -1; + } + if (result > 0) { + return 1; + } + return 0; +} + +int compare_version(const Version& lhs, const Version& rhs) { + if (lhs.major != rhs.major) { + return lhs.major < rhs.major ? -1 : 1; + } + if (lhs.minor != rhs.minor) { + return lhs.minor < rhs.minor ? -1 : 1; + } + if (lhs.patch != rhs.patch) { + return lhs.patch < rhs.patch ? -1 : 1; + } + if (lhs.prerelease.empty() != rhs.prerelease.empty()) { + return lhs.prerelease.empty() ? 1 : -1; + } + + const size_t commonSize = std::min(lhs.prerelease.size(), rhs.prerelease.size()); + for (size_t i = 0; i < commonSize; ++i) { + const int result = compare_identifier(lhs.prerelease[i], rhs.prerelease[i]); + if (result != 0) { + return result; + } + } + if (lhs.prerelease.size() != rhs.prerelease.size()) { + return lhs.prerelease.size() < rhs.prerelease.size() ? -1 : 1; + } + return 0; +} + std::optional parse_version(std::string_view value) { if (!value.empty() && value.front() == 'v') { value.remove_prefix(1); @@ -75,13 +205,38 @@ std::optional parse_version(std::string_view value) { if (!patch) { return std::nullopt; } - if (!value.empty() && value.front() != '-' && value.front() != '+') { - return std::nullopt; - } version.major = *major; version.minor = *minor; version.patch = *patch; + + if (value.empty()) { + return version; + } + if (value.front() == '+') { + value.remove_prefix(1); + if (!is_identifier_list(value)) { + return std::nullopt; + } + return version; + } + if (!consume(value, '-')) { + return std::nullopt; + } + + const size_t buildStart = value.find('+'); + std::string_view prerelease = value.substr(0, buildStart); + if (!is_identifier_list(prerelease)) { + return std::nullopt; + } + if (buildStart != std::string_view::npos && !is_identifier_list(value.substr(buildStart + 1))) { + return std::nullopt; + } + + prerelease = trim_git_describe_suffix(prerelease); + if (!prerelease.empty()) { + split_identifiers(prerelease, version.prerelease); + } return version; } @@ -185,7 +340,7 @@ Result check_latest_github_release(std::string_view owner, std::string_view repo }; } - const bool updateAvailable = *latestVersion > *currentVersion; + const bool updateAvailable = compare_version(*latestVersion, *currentVersion) > 0; return { .status = updateAvailable ? Status::UpdateAvailable : Status::UpToDate, .message = updateAvailable ? "Update available" : "Dusk is up to date", From 65e8577253cce7a51dc9120e70012da2fe8def6a Mon Sep 17 00:00:00 2001 From: Howard Luck Date: Fri, 8 May 2026 17:26:33 -0600 Subject: [PATCH 08/53] frame interp: fix b&c chain links (#724) --- include/d/actor/d_a_alink.h | 12 ++++++++++++ src/d/actor/d_a_alink.cpp | 26 ++++++++++++++++++++++++++ src/d/actor/d_a_alink_hook.inc | 27 +++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 9980ab776f..107afc1ed3 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -4552,6 +4552,18 @@ public: void handleWolfHowl(); void handleQuickTransform(); bool checkGyroAimContext(); + + void onIronBallChainInterpCallback(); + + static const int IRON_BALL_CHAIN_COUNT = 102; + cXyz mIBChainInterpPrevPos[IRON_BALL_CHAIN_COUNT]; + cXyz mIBChainInterpCurrPos[IRON_BALL_CHAIN_COUNT]; + csXyz mIBChainInterpPrevAngle[IRON_BALL_CHAIN_COUNT]; + csXyz mIBChainInterpCurrAngle[IRON_BALL_CHAIN_COUNT]; + cXyz mIBChainInterpPrevHandRoot; + cXyz mIBChainInterpCurrHandRoot; + bool mIBChainInterpPrevValid; + bool mIBChainInterpCurrValid; #endif }; // Size: 0x385C diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index e14f598487..0204278ddc 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -51,6 +51,7 @@ #include "d/actor/d_a_ni.h" #include "d/d_s_play.h" +#include "dusk/frame_interpolation.h" #include "dusk/settings.h" #include "res/Object/Alink.h" #include @@ -14787,6 +14788,10 @@ void daAlink_c::deleteEquipItem(BOOL i_isPlaySound, BOOL i_isDeleteKantera) { mIronBallChainPos = NULL; mIronBallChainAngle = NULL; field_0x3848 = NULL; +#if TARGET_PC + mIBChainInterpPrevValid = false; + mIBChainInterpCurrValid = false; +#endif field_0x0774 = NULL; field_0x0778 = NULL; mpHookshotLinChk = NULL; @@ -19717,6 +19722,27 @@ int daAlink_c::draw() { ) { dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0); + +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation && + mEquipItem == dItemNo_IRONBALL_e && + mIronBallChainPos != NULL && mIronBallChainAngle != NULL) + { + if (mIBChainInterpCurrValid) { + memcpy(mIBChainInterpPrevPos, mIBChainInterpCurrPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz)); + memcpy(mIBChainInterpPrevAngle, mIBChainInterpCurrAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz)); + mIBChainInterpPrevHandRoot = mIBChainInterpCurrHandRoot; + mIBChainInterpPrevValid = true; + } + + memcpy(mIBChainInterpCurrPos, mIronBallChainPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz)); + memcpy(mIBChainInterpCurrAngle, mIronBallChainAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz)); + mIBChainInterpCurrHandRoot = mHookshotTopPos; + mIBChainInterpCurrValid = true; + + dusk::frame_interp::add_interpolation_callback(&ironBallChainInterpCallback, this); + } +#endif } } diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc index c960d37a7b..36fba0c6d8 100644 --- a/src/d/actor/d_a_alink_hook.inc +++ b/src/d/actor/d_a_alink_hook.inc @@ -8,6 +8,8 @@ #include "d/actor/d_a_obj_swhang.h" #include "d/actor/d_a_obj_chandelier.h" #include "JSystem/J3DGraphBase/J3DMaterial.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" enum { HS_MODE_NONE_e, @@ -235,6 +237,31 @@ void daAlink_c::hsChainShape_c::draw() { } } +#if TARGET_PC +static void ironBallChainInterpCallback(bool isSimFrame, void* pUserWork) { + static_cast(pUserWork)->onIronBallChainInterpCallback(); +} + +void daAlink_c::onIronBallChainInterpCallback() { + if (!mIBChainInterpPrevValid || !mIBChainInterpCurrValid) { + return; + } + if (mIronBallChainPos == NULL || mIronBallChainAngle == NULL) { + return; + } + + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + + for (int i = 0; i < IRON_BALL_CHAIN_COUNT; i++) { + mIronBallChainPos[i] = mIBChainInterpPrevPos[i] + (mIBChainInterpCurrPos[i] - mIBChainInterpPrevPos[i]) * alpha; + mIronBallChainAngle[i].x = mIBChainInterpPrevAngle[i].x + (s16)((s16)(mIBChainInterpCurrAngle[i].x - mIBChainInterpPrevAngle[i].x) * alpha); + mIronBallChainAngle[i].y = mIBChainInterpPrevAngle[i].y + (s16)((s16)(mIBChainInterpCurrAngle[i].y - mIBChainInterpPrevAngle[i].y) * alpha); + mIronBallChainAngle[i].z = mIBChainInterpPrevAngle[i].z + (s16)((s16)(mIBChainInterpCurrAngle[i].z - mIBChainInterpPrevAngle[i].z) * alpha); + } + mHookshotTopPos = mIBChainInterpPrevHandRoot + (mIBChainInterpCurrHandRoot - mIBChainInterpPrevHandRoot) * alpha; +} +#endif + void daAlink_c::hookshotAtHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor, dCcD_GObjInf* i_tgObjInf) { if (i_tgActor != NULL && fopAcM_IsActor(i_tgActor) && !i_tgObjInf->ChkTgHookshotThrough()) { From 34e10d384420e19b6fb43038f9021ca264c2151e Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 8 May 2026 17:55:39 -0600 Subject: [PATCH 09/53] Show "3-finger tap" for menu notif on mobile --- src/dusk/ui/overlay.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index 0ce0b904ac..45482230a9 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -10,6 +10,10 @@ #include #include +#if defined(__APPLE__) +#include +#endif + namespace dusk::ui { namespace { aurora::Module Log{"dusk::ui::overlay"}; @@ -33,7 +37,7 @@ constexpr std::array, 3> kAutoSaveLayers{{ constexpr auto kMenuNotificationDuration = std::chrono::milliseconds(2500); -constexpr std::array kFpsCorners = { "tl", "tr", "bl", "br" }; +constexpr std::array kFpsCorners = {"tl", "tr", "bl", "br"}; Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) { if (toast.type == "autosave") { @@ -130,13 +134,19 @@ Rml::String back_button_name() { return "Back"; } +#if defined(TARGET_ANDROID) || (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST) +constexpr auto kMenuNotificationPrefix = "3-finger tap or"; +#else +constexpr auto kMenuNotificationPrefix = "Press F1 or"; +#endif + Rml::Element* create_menu_notification(Rml::Element* parent) { auto* elem = append(parent, "toast"); elem->SetClass("menu-notification", true); auto* message = append(elem, "message"); auto* row = append(message, "row"); - append(row, "span")->SetInnerRML("Press F1 or"); + append(row, "span")->SetInnerRML(kMenuNotificationPrefix); auto* icon = append(row, "icon"); icon->SetClass("controller", true); append(row, "span")->SetInnerRML(escape(back_button_name())); @@ -242,7 +252,8 @@ void Overlay::update() { const Uint64 now = SDL_GetPerformanceCounter(); // Limit updates to twice per second - const bool refreshLabel = perfFreq == 0 || mFpsLastUpdate == 0 || + const bool refreshLabel = + perfFreq == 0 || mFpsLastUpdate == 0 || static_cast(now - mFpsLastUpdate) >= 0.5 * static_cast(perfFreq); if (refreshLabel) { mFpsLastUpdate = now; From 78179eb93f8188596bccb0751af7d753b844dfcb Mon Sep 17 00:00:00 2001 From: roeming Date: Fri, 8 May 2026 20:56:38 -0400 Subject: [PATCH 10/53] comment out flags tab in editor (#727) Co-authored-by: roeming --- src/dusk/ui/editor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dusk/ui/editor.cpp b/src/dusk/ui/editor.cpp index 09216a3b8a..469bb920e3 100644 --- a/src/dusk/ui/editor.cpp +++ b/src/dusk/ui/editor.cpp @@ -1881,9 +1881,9 @@ EditorWindow::EditorWindow() { } }); - add_tab("Flags", [this](Rml::Element* content) { - // TODO - }); + //add_tab("Flags", [this](Rml::Element* content) { + // // TODO + //}); add_tab("Minigame", [this](Rml::Element* content) { auto& leftPane = add_child(content, Pane::Type::Controlled); From d2a1dda52348f4f8adf0ea47b91c384c571b10df Mon Sep 17 00:00:00 2001 From: Irastris Date: Fri, 8 May 2026 21:00:12 -0400 Subject: [PATCH 11/53] Add interp callbacks to the stalks of four Baba variants --- include/d/actor/d_a_e_db.h | 6 ++++++ include/d/actor/d_a_e_hb.h | 6 ++++++ include/d/actor/d_a_e_yd.h | 6 ++++++ include/d/actor/d_a_e_yh.h | 6 ++++++ src/d/actor/d_a_e_db.cpp | 31 +++++++++++++++++++++++++++++++ src/d/actor/d_a_e_hb.cpp | 31 +++++++++++++++++++++++++++++++ src/d/actor/d_a_e_yd.cpp | 31 +++++++++++++++++++++++++++++++ src/d/actor/d_a_e_yh.cpp | 31 +++++++++++++++++++++++++++++++ 8 files changed, 148 insertions(+) diff --git a/include/d/actor/d_a_e_db.h b/include/d/actor/d_a_e_db.h index a95722a6d1..4f10715443 100644 --- a/include/d/actor/d_a_e_db.h +++ b/include/d/actor/d_a_e_db.h @@ -80,6 +80,12 @@ public: /* 0x125C */ u32 field_0x125c; /* 0x1260 */ u8 field_0x1260[0x126C - 0x1260]; /* 0x126C */ u8 HIOInit; +#if TARGET_PC + cXyz mStalkLineInterpPrev[12]; + cXyz mStalkLineInterpCurr[12]; + bool mStalkLineInterpPrevValid; + bool mStalkLineInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_db_class) == 0x1270); diff --git a/include/d/actor/d_a_e_hb.h b/include/d/actor/d_a_e_hb.h index a7e0241007..3069bcd325 100644 --- a/include/d/actor/d_a_e_hb.h +++ b/include/d/actor/d_a_e_hb.h @@ -73,6 +73,12 @@ public: /* 0x124C */ f32 field_0x124c; /* 0x1250 */ u8 field_0x1250[0x1264 - 0x1250]; /* 0x1264 */ u8 HIOInit; +#if TARGET_PC + cXyz mStalkLineInterpPrev[12]; + cXyz mStalkLineInterpCurr[12]; + bool mStalkLineInterpPrevValid; + bool mStalkLineInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_hb_class) == 0x1268); diff --git a/include/d/actor/d_a_e_yd.h b/include/d/actor/d_a_e_yd.h index 188e435ba5..44d6036600 100644 --- a/include/d/actor/d_a_e_yd.h +++ b/include/d/actor/d_a_e_yd.h @@ -74,6 +74,12 @@ public: /* 0x1250 */ f32 field_0x1250; /* 0x1254 */ u8 field_0x1254[0x1268 - 0x1254]; /* 0x1268 */ u8 field_0x1268; +#if TARGET_PC + cXyz mLineMatInterpPrev[12]; + cXyz mLineMatInterpCurr[12]; + bool mLineMatInterpPrevValid; + bool mLineMatInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_yd_class) == 0x126c); diff --git a/include/d/actor/d_a_e_yh.h b/include/d/actor/d_a_e_yh.h index 519e5e2779..63e8ac1882 100644 --- a/include/d/actor/d_a_e_yh.h +++ b/include/d/actor/d_a_e_yh.h @@ -77,6 +77,12 @@ public: /* 0x1260 */ u32 field_0x1260; /* 0x1260 */ u8 field_0x1264[0x1270 - 0x1264]; /* 0x1270 */ bool mIsHIOOwner; +#if TARGET_PC + cXyz mLineInterpPrev[12]; + cXyz mLineInterpCurr[12]; + bool mLineInterpPrevValid; + bool mLineInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_yh_class) == 0x1274); diff --git a/src/d/actor/d_a_e_db.cpp b/src/d/actor/d_a_e_db.cpp index cbd21176b9..4d60fbd6f6 100644 --- a/src/d/actor/d_a_e_db.cpp +++ b/src/d/actor/d_a_e_db.cpp @@ -10,6 +10,10 @@ #include "f_op/f_op_kankyo_mng.h" #include "f_op/f_op_actor_enemy.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + class daE_DB_HIO_c : public JORReflexible { public: daE_DB_HIO_c(); @@ -66,6 +70,22 @@ static BOOL leaf_anm_init(e_db_class* i_this, int i_anm, f32 i_morf, u8 i_mode, return FALSE; } +#if TARGET_PC +static void daE_DB_interp_callback(bool isSimFrame, void* pUserWork) { + e_db_class* i_this = (e_db_class*)pUserWork; + if (!i_this->mStalkLineInterpPrevValid || !i_this->mStalkLineInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->stalkLine.getPos(0); + for (int i = 0; i < 12; i++) { + const cXyz& p0 = i_this->mStalkLineInterpPrev[i]; + const cXyz& p1 = i_this->mStalkLineInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daE_DB_Draw(e_db_class* i_this) { fopAc_ac_c* actor = &i_this->enemy; @@ -95,6 +115,17 @@ static int daE_DB_Draw(e_db_class* i_this) { static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF}; i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mStalkLineInterpCurrValid) { + memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); + i_this->mStalkLineInterpPrevValid = true; + } + memcpy(i_this->mStalkLineInterpCurr, i_this->stalkLine.getPos(0), 12 * sizeof(cXyz)); + i_this->mStalkLineInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_DB_interp_callback, i_this); + } +#endif for (int i = 1; i < 11; i++) { if (i_this->thornModel[i] != NULL) { diff --git a/src/d/actor/d_a_e_hb.cpp b/src/d/actor/d_a_e_hb.cpp index d3177ba0f6..8ed9058c6f 100644 --- a/src/d/actor/d_a_e_hb.cpp +++ b/src/d/actor/d_a_e_hb.cpp @@ -9,6 +9,10 @@ #include "d/actor/d_a_e_hb_leaf.h" #include "f_op/f_op_actor_enemy.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + enum daE_HB_ACTION { ACTION_STAY, ACTION_APPEAR, @@ -64,6 +68,22 @@ static BOOL leaf_anm_init(e_hb_class* i_this, int i_anm, f32 i_morf, u8 i_mode, return FALSE; } +#if TARGET_PC +static void daE_HB_interp_callback(bool isSimFrame, void* pUserWork) { + e_hb_class* i_this = (e_hb_class*)pUserWork; + if (!i_this->mStalkLineInterpPrevValid || !i_this->mStalkLineInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->stalkLine.getPos(0); + for (int i = 0; i < 12; i++) { + const cXyz& p0 = i_this->mStalkLineInterpPrev[i]; + const cXyz& p1 = i_this->mStalkLineInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daE_HB_Draw(e_hb_class* i_this) { fopAc_ac_c* actor = &i_this->enemy; @@ -82,6 +102,17 @@ static int daE_HB_Draw(e_hb_class* i_this) { static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF}; i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mStalkLineInterpCurrValid) { + memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); + i_this->mStalkLineInterpPrevValid = true; + } + memcpy(i_this->mStalkLineInterpCurr, i_this->stalkLine.getPos(0), 12 * sizeof(cXyz)); + i_this->mStalkLineInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_HB_interp_callback, i_this); + } +#endif for (int i = 1; i < 11; i++) { if (i_this->thornModel[i] != NULL) { diff --git a/src/d/actor/d_a_e_yd.cpp b/src/d/actor/d_a_e_yd.cpp index bdcd044eed..18809e05ea 100644 --- a/src/d/actor/d_a_e_yd.cpp +++ b/src/d/actor/d_a_e_yd.cpp @@ -12,6 +12,10 @@ #include "d/d_cc_uty.h" #include "f_op/f_op_actor_enemy.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + class daE_YD_HIO_c { public: daE_YD_HIO_c(); @@ -73,6 +77,22 @@ static s32 leaf_anm_init(e_yd_class* i_this, int param_1, f32 param_2, u8 param_ return false; } +#if TARGET_PC +static void daE_YD_interp_callback(bool isSimFrame, void* pUserWork) { + e_yd_class* i_this = (e_yd_class*)pUserWork; + if (!i_this->mLineMatInterpPrevValid || !i_this->mLineMatInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->mLineMat.getPos(0); + for (int i = 0; i < 12; i++) { + const cXyz& p0 = i_this->mLineMatInterpPrev[i]; + const cXyz& p1 = i_this->mLineMatInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static s32 daE_YD_Draw(e_yd_class* i_this) { static GXColor l_color = { 0x14, 0x0F, 0x00, 0xFF }; @@ -86,6 +106,17 @@ static s32 daE_YD_Draw(e_yd_class* i_this) { i_this->mpMorf->entryDL(); i_this->mLineMat.update(12, l_color, &i_this->actor.tevStr); dComIfGd_set3DlineMat(&i_this->mLineMat); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mLineMatInterpCurrValid) { + memcpy(i_this->mLineMatInterpPrev, i_this->mLineMatInterpCurr, sizeof(i_this->mLineMatInterpCurr)); + i_this->mLineMatInterpPrevValid = true; + } + memcpy(i_this->mLineMatInterpCurr, i_this->mLineMat.getPos(0), 12 * sizeof(cXyz)); + i_this->mLineMatInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_YD_interp_callback, i_this); + } +#endif for (s32 i = 1; i < 11; i++) { if (i_this->field_0x77c[i] != 0) { g_env_light.setLightTevColorType_MAJI(i_this->field_0x77c[i], &i_this->actor.tevStr); diff --git a/src/d/actor/d_a_e_yh.cpp b/src/d/actor/d_a_e_yh.cpp index bbbd3e5129..cf9955a7db 100644 --- a/src/d/actor/d_a_e_yh.cpp +++ b/src/d/actor/d_a_e_yh.cpp @@ -12,6 +12,10 @@ #include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_kankyo_mng.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + class daE_YH_HIO_c : public JORReflexible { public: daE_YH_HIO_c(); @@ -85,6 +89,22 @@ static BOOL leaf_anm_init(e_yh_class* i_this, int param_2, f32 param_3, u8 param return FALSE; } +#if TARGET_PC +static void daE_YH_interp_callback(bool isSimFrame, void* pUserWork) { + e_yh_class* i_this = (e_yh_class*)pUserWork; + if (!i_this->mLineInterpPrevValid || !i_this->mLineInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->mLine.getPos(0); + for (int i = 0; i < 12; i++) { + const cXyz& p0 = i_this->mLineInterpPrev[i]; + const cXyz& p1 = i_this->mLineInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daE_YH_Draw(e_yh_class* i_this) { fopAc_ac_c* a_this = (fopAc_ac_c*)i_this; @@ -114,6 +134,17 @@ static int daE_YH_Draw(e_yh_class* i_this) { i_this->mLine.update(12, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mLine); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mLineInterpCurrValid) { + memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, sizeof(i_this->mLineInterpCurr)); + i_this->mLineInterpPrevValid = true; + } + memcpy(i_this->mLineInterpCurr, i_this->mLine.getPos(0), 12 * sizeof(cXyz)); + i_this->mLineInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_YH_interp_callback, i_this); + } +#endif for (int i = 1; i < 11; i++) { if (i_this->mModels[i] != NULL) { From 71c892368dfe146f6ffdcf13cd6e5194b77c7615 Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Fri, 8 May 2026 21:31:46 -0400 Subject: [PATCH 12/53] Use float vertex positions for trim (#729) Fixes #726 and looks much smoother. --- src/m_Do/m_Do_graphic.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index 61eaf3bc2d..424f69cf4b 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -1194,8 +1194,13 @@ static void trimming(view_class* param_0, view_port_class* param_1) { if ((y_orig_pos == 0) && (param_1->scissor.y_orig != param_1->y_orig || (param_1->scissor.height != param_1->height))) { + #if TARGET_PC + f32 sc_top = param_1->scissor.y_orig; + f32 sc_bottom = param_1->scissor.y_orig + param_1->scissor.height; + #else s32 sc_top = (int)param_1->scissor.y_orig; s32 sc_bottom = param_1->scissor.y_orig + param_1->scissor.height; + #endif GXSetNumChans(1); GXSetChanCtrl(GX_ALPHA0, GX_FALSE, GX_SRC_REG, GX_SRC_REG, 0, GX_DF_NONE, GX_AF_NONE); GXSetNumTexGens(0); @@ -1224,20 +1229,20 @@ static void trimming(view_class* param_0, view_port_class* param_1) { GXLoadPosMtxImm(cMtx_getIdentity(), 0); GXClearVtxDesc(); GXSetVtxDesc(GX_VA_POS, GX_DIRECT); - GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGBA, GX_RGBA4, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGBA, DUSK_IF_ELSE(GX_F32, GX_RGBA4), 0); GXSetProjection(ortho, GX_ORTHOGRAPHIC); GXSetCurrentMtx(0); GXBegin(GX_QUADS, GX_VTXFMT0, 8); #if TARGET_PC - GXPosition3s16(0, 0, -5); - GXPosition3s16(param_1->width, 0, -5); - GXPosition3s16(param_1->width, sc_top, -5); - GXPosition3s16(0, sc_top, -5); - GXPosition3s16(0, sc_bottom, -5); - GXPosition3s16(param_1->width, sc_bottom, -5); - GXPosition3s16(param_1->width, param_1->height, -5); - GXPosition3s16(0, param_1->height, -5); + GXPosition3f32(0, 0, -5); + GXPosition3f32(param_1->width, 0, -5); + GXPosition3f32(param_1->width, sc_top, -5); + GXPosition3f32(0, sc_top, -5); + GXPosition3f32(0, sc_bottom, -5); + GXPosition3f32(param_1->width, sc_bottom, -5); + GXPosition3f32(param_1->width, param_1->height, -5); + GXPosition3f32(0, param_1->height, -5); #else GXPosition3s16(0, 0, -5); GXPosition3s16(FB_WIDTH, 0, -5); From 4d4a80891feb7e249fdc28ba126a4c13edbb95f3 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Fri, 8 May 2026 20:21:37 -0600 Subject: [PATCH 13/53] frame interp: fix obj_fchain --- include/d/actor/d_a_obj_fchain.h | 12 ++++++++++ src/d/actor/d_a_obj_fchain.cpp | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/include/d/actor/d_a_obj_fchain.h b/include/d/actor/d_a_obj_fchain.h index 1bd7b9a810..7c4ae74faf 100644 --- a/include/d/actor/d_a_obj_fchain.h +++ b/include/d/actor/d_a_obj_fchain.h @@ -31,6 +31,10 @@ public: csXyz* getAngle() { return field_0x8a4; } J3DModelData* getModelData() { return mModelData; } +#if TARGET_PC + void onInterpCallback(); +#endif + private: /* 0x568 */ request_of_phase_process_class mPhase; /* 0x570 */ J3DModelData* mModelData; @@ -42,6 +46,14 @@ private: /* 0x694 */ cXyz field_0x694[22]; /* 0x79C */ cXyz field_0x79c[22]; /* 0x8A4 */ csXyz field_0x8a4[22]; + +#if TARGET_PC + static const int CHAIN_COUNT = 22; + cXyz mChainInterpPrev[CHAIN_COUNT]; + cXyz mChainInterpCurr[CHAIN_COUNT]; + bool mChainInterpPrevValid; + bool mChainInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(daObjFchain_c) == 0x928); diff --git a/src/d/actor/d_a_obj_fchain.cpp b/src/d/actor/d_a_obj_fchain.cpp index e681fec4e7..024b4ecf8b 100644 --- a/src/d/actor/d_a_obj_fchain.cpp +++ b/src/d/actor/d_a_obj_fchain.cpp @@ -10,6 +10,8 @@ #include "JSystem/J3DGraphBase/J3DDrawBuffer.h" #include "SSystem/SComponent/c_math.h" #include "d/d_com_inf_game.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" #include static char const l_arcName[] = "Fchain"; @@ -65,6 +67,10 @@ int daObjFchain_c::create() { local_48++; } rv = cPhs_COMPLEATE_e; +#if TARGET_PC + mChainInterpPrevValid = false; + mChainInterpCurrValid = false; +#endif break; } return rv; @@ -289,6 +295,26 @@ void daObjFchain_shape_c::draw() { } } +#if TARGET_PC +static void fchain_interp_callback(bool isSimFrame, void* pUserWork) { + static_cast(pUserWork)->onInterpCallback(); +} + +void daObjFchain_c::onInterpCallback() { + if (!mChainInterpPrevValid || !mChainInterpCurrValid) { + return; + } + + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + + for (int i = 0; i < CHAIN_COUNT; i++) { + const cXyz& p0 = mChainInterpPrev[i]; + const cXyz& p1 = mChainInterpCurr[i]; + field_0x694[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + int daObjFchain_c::draw() { if (field_0x584 != 0) { g_env_light.settingTevStruct(0, ¤t.pos, &tevStr); @@ -297,6 +323,19 @@ int daObjFchain_c::draw() { return 1; } dComIfGd_getOpaListDark()->entryImm(&mShape, 0); + +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (mChainInterpCurrValid) { + memcpy(mChainInterpPrev, mChainInterpCurr, sizeof(mChainInterpCurr)); + mChainInterpPrevValid = true; + } + + memcpy(mChainInterpCurr, field_0x694, sizeof(mChainInterpCurr)); + mChainInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&fchain_interp_callback, this); + } +#endif } return 1; } From 06c77a68184d64d93504ad54bdf6655e4700a90b Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Fri, 8 May 2026 21:15:52 -0600 Subject: [PATCH 14/53] frame interp: e_s1 & e_yg --- include/d/actor/d_a_e_s1.h | 9 ++++++++ include/d/actor/d_a_e_yg.h | 9 ++++++++ src/d/actor/d_a_e_s1.cpp | 42 +++++++++++++++++++++++++++++++++++ src/d/actor/d_a_e_yg.cpp | 45 +++++++++++++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/include/d/actor/d_a_e_s1.h b/include/d/actor/d_a_e_s1.h index a631ba47ac..5cbae84696 100644 --- a/include/d/actor/d_a_e_s1.h +++ b/include/d/actor/d_a_e_s1.h @@ -81,6 +81,15 @@ public: /* 0x306D */ u8 field_0x306D[0x307C - 0x306D]; /* 0x307C */ u32 mBodyEffEmtrID; /* 0x3080 */ u8 mInitHIO; + +#if TARGET_PC + static const int HAIR_STRAND_COUNT = 22; + static const int HAIR_SEGMENT_COUNT = 16; + cXyz mHairInterpPrev[HAIR_STRAND_COUNT * HAIR_SEGMENT_COUNT]; + cXyz mHairInterpCurr[HAIR_STRAND_COUNT * HAIR_SEGMENT_COUNT]; + bool mHairInterpPrevValid; + bool mHairInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_s1_class) == 0x3084); diff --git a/include/d/actor/d_a_e_yg.h b/include/d/actor/d_a_e_yg.h index 69a85430a8..6be19933cb 100644 --- a/include/d/actor/d_a_e_yg.h +++ b/include/d/actor/d_a_e_yg.h @@ -63,6 +63,15 @@ public: /* 0x0BB4 */ yg_ke_s mYgKes[13]; /* 0x1880 */ mDoExt_3DlineMat0_c mLineMat; /* 0x189C */ u8 mIsFirstSpawn; + +#if TARGET_PC + static const int TENTACLE_STRAND_COUNT = 13; + static const int TENTACLE_SEGMENT_COUNT = 10; + cXyz mTentacleInterpPrev[TENTACLE_STRAND_COUNT * TENTACLE_SEGMENT_COUNT]; + cXyz mTentacleInterpCurr[TENTACLE_STRAND_COUNT * TENTACLE_SEGMENT_COUNT]; + bool mTentacleInterpPrevValid; + bool mTentacleInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_yg_class) == 0x18a0); diff --git a/src/d/actor/d_a_e_s1.cpp b/src/d/actor/d_a_e_s1.cpp index 48cd7d6800..a5a05fce20 100644 --- a/src/d/actor/d_a_e_s1.cpp +++ b/src/d/actor/d_a_e_s1.cpp @@ -14,6 +14,8 @@ #include "d/d_s_play.h" #include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_camera_mng.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" #include class daE_S1_HIO_c { @@ -99,6 +101,25 @@ static void anm_init(e_s1_class* i_this, int i_resNo, f32 i_morf, u8 i_attr, f32 i_this->mAnm = i_resNo; } +#if TARGET_PC +static void daE_S1_interp_callback(bool isSimFrame, void* pUserWork) { + e_s1_class* i_this = (e_s1_class*)pUserWork; + if (!i_this->mHairInterpPrevValid || !i_this->mHairInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + for (int s = 0; s < e_s1_class::HAIR_STRAND_COUNT; s++) { + cXyz* dst = i_this->mLineMat.getPos(s); + for (int i = 0; i < e_s1_class::HAIR_SEGMENT_COUNT; i++) { + int idx = s * e_s1_class::HAIR_SEGMENT_COUNT + i; + const cXyz& p0 = i_this->mHairInterpPrev[idx]; + const cXyz& p1 = i_this->mHairInterpCurr[idx]; + dst[i] = p0 + (p1 - p0) * alpha; + } + } +} +#endif + static int daE_S1_Draw(e_s1_class* i_this) { if (i_this->field_0x306c != 0) { return 1; @@ -132,6 +153,22 @@ static int daE_S1_Draw(e_s1_class* i_this) { i_this->mLineMat.update(16, line_color, &i_this->tevStr); dComIfGd_set3DlineMatDark(&i_this->mLineMat); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mHairInterpCurrValid) { + memcpy(i_this->mHairInterpPrev, i_this->mHairInterpCurr, sizeof(i_this->mHairInterpCurr)); + i_this->mHairInterpPrevValid = true; + } + for (int s = 0; s < e_s1_class::HAIR_STRAND_COUNT; s++) { + cXyz* src = i_this->mLineMat.getPos(s); + memcpy(&i_this->mHairInterpCurr[s * e_s1_class::HAIR_SEGMENT_COUNT], src, + e_s1_class::HAIR_SEGMENT_COUNT * sizeof(cXyz)); + } + i_this->mHairInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_S1_interp_callback, i_this); + } +#endif + dComIfGd_setList(); return 1; } @@ -2149,6 +2186,11 @@ static int daE_S1_Create(fopAc_ac_c* i_this) { return cPhs_ERROR_e; } +#if TARGET_PC + a_this->mHairInterpPrevValid = false; + a_this->mHairInterpCurrValid = false; +#endif + OS_REPORT("//////////////E_S1 SET 2 !!\n"); if (path_no != 0xFF) { diff --git a/src/d/actor/d_a_e_yg.cpp b/src/d/actor/d_a_e_yg.cpp index 9ab6c80e6d..44a8aee249 100644 --- a/src/d/actor/d_a_e_yg.cpp +++ b/src/d/actor/d_a_e_yg.cpp @@ -10,6 +10,8 @@ #include "f_op/f_op_kankyo_mng.h" #include "d/actor/d_a_obj_carry.h" #include "Z2AudioLib/Z2Instances.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" #include "f_op/f_op_actor_enemy.h" enum E_yg_RES_File_ID { @@ -134,7 +136,26 @@ static BOOL pl_check(e_yg_class* i_this, f32 i_dist) { return FALSE; } -static int daE_YG_Draw(e_yg_class* i_this) { +#if TARGET_PC +static void daE_YG_interp_callback(bool isSimFrame, void* pUserWork) { + e_yg_class* i_this = (e_yg_class*)pUserWork; + if (!i_this->mTentacleInterpPrevValid || !i_this->mTentacleInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + for (int s = 0; s < e_yg_class::TENTACLE_STRAND_COUNT; s++) { + cXyz* dst = i_this->mLineMat.getPos(s); + for (int i = 0; i < e_yg_class::TENTACLE_SEGMENT_COUNT; i++) { + int idx = s * e_yg_class::TENTACLE_SEGMENT_COUNT + i; + const cXyz& p0 = i_this->mTentacleInterpPrev[idx]; + const cXyz& p1 = i_this->mTentacleInterpCurr[idx]; + dst[i] = p0 + (p1 - p0) * alpha; + } + } +} +#endif + +static int daE_YG_Draw(e_yg_class* i_this) { if (i_this->mDispFlag) { return 1; } @@ -160,6 +181,23 @@ static int daE_YG_Draw(e_yg_class* i_this) { color.a = 0xFF; i_this->mLineMat.update(10, color, &actor->tevStr); dComIfGd_set3DlineMatDark(&i_this->mLineMat); + +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mTentacleInterpCurrValid) { + memcpy(i_this->mTentacleInterpPrev, i_this->mTentacleInterpCurr, sizeof(i_this->mTentacleInterpCurr)); + i_this->mTentacleInterpPrevValid = true; + } + for (int s = 0; s < e_yg_class::TENTACLE_STRAND_COUNT; s++) { + cXyz* src = i_this->mLineMat.getPos(s); + memcpy(&i_this->mTentacleInterpCurr[s * e_yg_class::TENTACLE_SEGMENT_COUNT], src, + e_yg_class::TENTACLE_SEGMENT_COUNT * sizeof(cXyz)); + } + i_this->mTentacleInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&daE_YG_interp_callback, i_this); + } +#endif + dComIfGd_setList(); return 1; @@ -1378,6 +1416,11 @@ static cPhs_Step daE_YG_Create(fopAc_ac_c* actor) { return cPhs_ERROR_e; } +#if TARGET_PC + i_this->mTentacleInterpPrevValid = false; + i_this->mTentacleInterpCurrValid = false; +#endif + if (!hio_set) { i_this->mIsFirstSpawn = 1; hio_set = true; From e42c4d3174321c1fecd0a3b5ef376c753d891289 Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Fri, 8 May 2026 23:19:48 -0400 Subject: [PATCH 15/53] Number Button Fix - Add `is_editing` helper to BaseStringButton - Block left/right input from changing number while typing Resolves #706 --- src/dusk/ui/number_button.cpp | 4 ++-- src/dusk/ui/string_button.cpp | 10 +++++----- src/dusk/ui/string_button.hpp | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/dusk/ui/number_button.cpp b/src/dusk/ui/number_button.cpp index ab5095cf3f..3735855140 100644 --- a/src/dusk/ui/number_button.cpp +++ b/src/dusk/ui/number_button.cpp @@ -54,7 +54,7 @@ void NumberButton::set_value(Rml::String value) { } bool NumberButton::handle_nav_command(NavCommand cmd) { - if (cmd == NavCommand::Left || cmd == NavCommand::Right) { + if (!is_editing() && (cmd == NavCommand::Left || cmd == NavCommand::Right)) { const int newValue = std::clamp( mGetValue() + (cmd == NavCommand::Right ? mStep : -mStep), mMin, mMax); if (newValue != mGetValue()) { @@ -66,4 +66,4 @@ bool NumberButton::handle_nav_command(NavCommand cmd) { return BaseStringButton::handle_nav_command(cmd); } -} // namespace dusk::ui \ No newline at end of file +} // namespace dusk::ui diff --git a/src/dusk/ui/string_button.cpp b/src/dusk/ui/string_button.cpp index a267e5b0e0..7210e87803 100644 --- a/src/dusk/ui/string_button.cpp +++ b/src/dusk/ui/string_button.cpp @@ -24,7 +24,7 @@ void BaseStringButton::update() { } void BaseStringButton::start_editing() { - if (mInputElem != nullptr) { + if (is_editing()) { return; } @@ -79,14 +79,14 @@ void BaseStringButton::request_stop_editing(bool commit, bool refocusRoot) { bool BaseStringButton::handle_nav_command(NavCommand cmd) { if (cmd == NavCommand::Confirm) { - if (mInputElem == nullptr) { + if (!is_editing()) { start_editing(); } else { request_stop_editing(true, true); } return true; } else if (cmd == NavCommand::Cancel) { - if (mInputElem != nullptr) { + if (is_editing()) { request_stop_editing(false, true); return true; } @@ -95,7 +95,7 @@ bool BaseStringButton::handle_nav_command(NavCommand cmd) { } void BaseStringButton::focus_input() { - if (mInputElem == nullptr) { + if (!is_editing()) { return; } @@ -111,7 +111,7 @@ void BaseStringButton::focus_input() { void BaseStringButton::stop_editing(bool commit, bool refocusRoot) { mPendingStopEditing = false; mPendingInputFocusFrames = 0; - if (mInputElem == nullptr) { + if (!is_editing()) { return; } if (commit) { diff --git a/src/dusk/ui/string_button.hpp b/src/dusk/ui/string_button.hpp index d84b977f37..94f2cebcbe 100644 --- a/src/dusk/ui/string_button.hpp +++ b/src/dusk/ui/string_button.hpp @@ -20,6 +20,7 @@ public: void request_stop_editing(bool commit, bool refocusRoot); protected: + bool is_editing() { return mInputElem != nullptr; } bool handle_nav_command(NavCommand cmd) override; virtual void set_value(Rml::String value) = 0; virtual Rml::String input_value() { return format_value(); } From 13dd3c3932e3db8c38aaec2a66953d9120c96062 Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Sat, 9 May 2026 08:30:46 -0400 Subject: [PATCH 16/53] Clamp LOD For Cutscene Midna (#728) - Sets max eye LOD level to 0 for `demo00_Midna_cut00_FC_tongue` and `demo00_Midna_cut00_BD_tmp` --- src/d/actor/d_a_midna.cpp | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/d/actor/d_a_midna.cpp b/src/d/actor/d_a_midna.cpp index 2356b693be..3bc500991d 100644 --- a/src/d/actor/d_a_midna.cpp +++ b/src/d/actor/d_a_midna.cpp @@ -419,9 +419,30 @@ int daMidna_c::createHeap() { return 0; } +#if TARGET_PC + if (mpDemoFCTongueBmd != NULL) { + if(!daAlink_c::initDemoBck(&mpDemoFCTmpBck, "demo00_Midna_cut00_FC_tmp.bck")) { + return 0; + } + + // Update Midna's eye maxLOD to prevent the eyes from disappearing + J3DTexture* tex = mpDemoFCTongueBmd->getModelData()->getTexture(); + JUTNameTab* nametable = mpDemoFCTongueBmd->getModelData()->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != NULL && strcmp(tex_name, "midona_eyeball") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } + } +#else if (mpDemoFCTongueBmd != NULL && !daAlink_c::initDemoBck(&mpDemoFCTmpBck, "demo00_Midna_cut00_FC_tmp.bck")) { return 0; } +#endif modelData = (J3DModelData*)dComIfG_getObjectRes(dStage_roomControl_c::getDemoArcName(), "demo00_Midna_cut00_BD_tmp.bmd"); @@ -433,6 +454,21 @@ int daMidna_c::createHeap() { modelData->getMaterialNodePointer(2)->setMaterialAnm(mpEyeMatAnm[0]); modelData->getMaterialNodePointer(3)->setMaterialAnm(mpEyeMatAnm[1]); + +#if TARGET_PC + // Update Midna's eye maxLOD to prevent the eyes from disappearing + J3DTexture* tex = modelData->getTexture(); + JUTNameTab* nametable = modelData->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != NULL && strcmp(tex_name, "midona_eyeball") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } +#endif } if (!initDemoModel(&mpDemoBDMaskBmd, "demo00_Midna_cut00_BD_mask.bmd", 0)) { From d8a1dd1da452b0fca4def6b130fa55d5a4bea1f9 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Sat, 9 May 2026 09:15:10 -0400 Subject: [PATCH 17/53] Recording Mode - Mute streams & Fix 1 note playing --- src/f_ap/f_ap_game.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index dc14933b50..2c6cb0a925 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -745,7 +745,8 @@ static void duskExecute() { updateAutoSave(); if (dusk::getSettings().game.recordingMode) { - Z2GetSeqMgr()->bgmAllMute(0, 0); + Z2GetSoundMgr()->getSeqMgr()->getParams()->moveVolume(0.0f, 0); + Z2GetSoundMgr()->getStreamMgr()->getParams()->moveVolume(0.0f, 0); } if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) { From 80dd5ff2780a15c29af2eea61c254ed09c8053dc Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 9 May 2026 07:32:29 -0700 Subject: [PATCH 18/53] revert mirror mode message override for now --- src/d/d_msg_object.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index 7e3be2cb08..44b5c66571 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -427,14 +427,15 @@ static void dummyStrings() { dMsgObject_HIO_c g_MsgObject_HIO_c; int dMsgObject_c::_execute() { -#if TARGET_PC +// TODO: enabling wii message overrides fixes direction text, but gives wrong item control text +/*#if TARGET_PC if (dusk::getSettings().game.enableMirrorMode) { // enable wii message index override g_MsgObject_HIO_c.mMessageDisplay = 1; } else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) { g_MsgObject_HIO_c.mMessageDisplay = 0; } -#endif +#endif*/ field_0x4c7 = 0; From 4290726691b9818878e9fa76614dff6f84b5a841 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 9 May 2026 07:38:19 -0700 Subject: [PATCH 19/53] update setup instructions --- README.md | 17 ++++++++++--- docs/ios-install-altstore.md | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 docs/ios-install-altstore.md diff --git a/README.md b/README.md index 5e7412e6aa..b5275e6bdc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

Official Website • - Discord + Discord

@@ -28,13 +28,24 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d | GameCube USA | `75edd3ddff41f125d1b4ce1a40378f1b565519e7` | | GameCube EUR | `2601822a488eeb86fb89db16ca8f29c2c953e1ca` | +*Support for other versions of the game is planned in the future. + ### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases) ### 3. Setup the game - +**Windows / macOS / Linux** - Extract the .zip file - Launch Dusk -- Press **Select Disc Image** and provide the path to your supported game dump. +- Press **Select Disc Image** and provide the path to your supported game dump +- Press **Play**! + +**iOS** +- Follow the [iOS setup guide](docs/ios-install-altstore.md) + +**Android** +- Install the Dusk apk +- Launch Dusk +- Press **Select Disc Image** and provide the path to your supported game dump - Press **Play**! # Building diff --git a/docs/ios-install-altstore.md b/docs/ios-install-altstore.md new file mode 100644 index 0000000000..fdfc7689d6 --- /dev/null +++ b/docs/ios-install-altstore.md @@ -0,0 +1,46 @@ +# Installing Dusk on iOS via AltStore + +## Prerequisites + +- Mac with Homebrew installed +- iPhone connected via USB +- Dusk IPA file (download the latest `Dusk-vX.X.X-ios-arm64.ipa` from the [releases page](https://github.com/TwilitRealm/dusk/releases)) +- Game disc - `GZ2E01` (Gamecube USA) or `GZ2PE01` (Gamecube PAL) + +## 1. Install AltServer + +```sh +brew install altserver +open -a AltServer +``` + +AltServer will appear in your menu bar. + +## 2. Enable Developer Mode (iOS 16+) + +- On your iPhone, go to **Settings > Privacy & Security > Developer Mode** +- Toggle it on and restart when prompted + +## 3. Install AltStore on Your iPhone + +- Click AltServer in the menu bar +- Click **Install AltStore > [Your iPhone]** +- Enter your Apple ID credentials when prompted +- On your iPhone, go to **Settings > General > VPN & Device Management** +- Tap your Apple ID under "Developer App" and tap **Trust** + +## 4. Copy Files to Your iPhone + +Transfer the IPA and game disc to your iPhone so they're accessible in the Files app. A few ways to do this: + +- **AirDrop** - Right-click the files on your Mac and choose Share > AirDrop +- **iCloud Drive** - Place files in iCloud Drive on your Mac and they'll sync to Files on your iPhone +- **USB transfer** - Connect your iPhone and drag files via Finder's sidebar +- **Cloud storage** - Upload to Google Drive, Dropbox, etc. and download on your iPhone + +## 5. Install via AltStore + +- Open **AltStore** on your iPhone +- Go to the **My Apps** tab +- Tap the **+** button (top left) +- Open the **Files** app and select the `.ipa` file \ No newline at end of file From 594cadcf7d0c49ec379e79ad6bc18ac056158130 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 09:55:18 -0600 Subject: [PATCH 20/53] Update Android app ID to dev.twilitrealm.dusk --- extern/aurora | 2 +- platforms/android/README.md | 5 ++--- platforms/android/app/build.gradle | 4 ++-- platforms/android/app/proguard-rules.pro | 4 ++-- platforms/android/app/src/main/AndroidManifest.xml | 2 +- .../src/main/java/com/twilitrealm/dusk/DuskActivity.java | 7 +------ .../src/main/java/com/twilitrealm/dusk/DuskHttpClient.java | 2 +- src/dusk/http/android.cpp | 2 +- 8 files changed, 11 insertions(+), 17 deletions(-) diff --git a/extern/aurora b/extern/aurora index 1eeff98783..9413f24f65 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 1eeff98783c36fba83970c011783a4fd9deac018 +Subproject commit 9413f24f65e20969b8320e651af306a2fd0798c8 diff --git a/platforms/android/README.md b/platforms/android/README.md index c7ecd684c1..f51b37db73 100644 --- a/platforms/android/README.md +++ b/platforms/android/README.md @@ -66,12 +66,11 @@ Output APK: You can pass command-line args through the activity intent: ```bash -adb shell am start -n com.twilitrealm.dusk/.DuskActivity \ - --es dusk_args "'/sdcard/Download/The Legend of Zelda: Twilight Princess (USA).iso'" +adb shell am start -n dev.twilitrealm.dusk/.DuskActivity \ + --es dusk_args "--backend vulkan" ``` Supported extras: - `dusk_args`: single shell-like argument string - `dusk_argv`: string-array argv -- `dusk_disc`: compatibility shortcut (single ISO path) diff --git a/platforms/android/app/build.gradle b/platforms/android/app/build.gradle index 01e706a382..9cf56899df 100644 --- a/platforms/android/app/build.gradle +++ b/platforms/android/app/build.gradle @@ -13,11 +13,11 @@ def syncDuskAssets = tasks.register('syncDuskAssets', Sync) { } android { - namespace 'com.twilitrealm.dusk' + namespace 'dev.twilitrealm.dusk' compileSdk 36 defaultConfig { - applicationId 'com.twilitrealm.dusk' + applicationId 'dev.twilitrealm.dusk' minSdk 26 targetSdk 36 versionCode 1 diff --git a/platforms/android/app/proguard-rules.pro b/platforms/android/app/proguard-rules.pro index 8f2cf4a4e2..72b7ca16fa 100644 --- a/platforms/android/app/proguard-rules.pro +++ b/platforms/android/app/proguard-rules.pro @@ -1,4 +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 { *; } +-keep class dev.twilitrealm.dusk.DuskHttpClient { *; } +-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; } diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml index a902065475..f3e11030ca 100644 --- a/platforms/android/app/src/main/AndroidManifest.xml +++ b/platforms/android/app/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ android:resource="@xml/game_mode_config" /> DeleteLocalRef(activity); if (clientClass == nullptr) { return { From eeb0ad77a4e4f4fee6b464407e2c98937d6c8ee6 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 10:03:05 -0600 Subject: [PATCH 21/53] Missed a spot --- src/dusk/http/android.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dusk/http/android.cpp b/src/dusk/http/android.cpp index 125ddf6808..3f4eef8d84 100644 --- a/src/dusk/http/android.cpp +++ b/src/dusk/http/android.cpp @@ -348,7 +348,7 @@ Result get(const Request& request) { jmethodID getMethod = env->GetStaticMethodID(clientClass, "get", "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;IJ)" - "Lcom/twilitrealm/dusk/DuskHttpClient$Response;"); + "Ldev/twilitrealm/dusk/DuskHttpClient$Response;"); if (getMethod == nullptr || clear_pending_exception(env)) { env->DeleteLocalRef(clientClass); return { From 2f83753260760c7bde6ca8326215ac950b89d7be Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 10:13:59 -0600 Subject: [PATCH 22/53] Fix Android release gradle build --- platforms/android/app/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platforms/android/app/build.gradle b/platforms/android/app/build.gradle index 9cf56899df..b7775df8e0 100644 --- a/platforms/android/app/build.gradle +++ b/platforms/android/app/build.gradle @@ -60,7 +60,8 @@ dependencies { } tasks.configureEach { task -> - if (task.name.startsWith('merge') && task.name.endsWith('Assets')) { + if ((task.name.startsWith('merge') && task.name.endsWith('Assets')) || + task.name.toLowerCase().contains('lint')) { task.dependsOn(syncDuskAssets) } } From d0b9b6d10f70856f921c8d004fa69f38507aee81 Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Sat, 9 May 2026 15:37:14 -0400 Subject: [PATCH 23/53] Fix Prelaunch Break-out (#738) - Prevent users from breaking out of the prelaunch menu through the GraphicsTuner pages --- src/dusk/ui/graphics_tuner.cpp | 8 +++++--- src/dusk/ui/graphics_tuner.hpp | 3 ++- src/dusk/ui/settings.cpp | 14 +++++++------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/dusk/ui/graphics_tuner.cpp b/src/dusk/ui/graphics_tuner.cpp index 8a11528914..188f0911c5 100644 --- a/src/dusk/ui/graphics_tuner.cpp +++ b/src/dusk/ui/graphics_tuner.cpp @@ -193,9 +193,10 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) { return ""; } -GraphicsTuner::GraphicsTuner(GraphicsTunerProps props) +GraphicsTuner::GraphicsTuner(GraphicsTunerProps props, bool prelaunch) : Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin), - mValueMax(props.valueMax), mDefaultValue(props.defaultValue) { + mValueMax(props.valueMax), mDefaultValue(props.defaultValue), mPrelaunch(prelaunch) +{ if (mDocument == nullptr) { return; } @@ -281,7 +282,8 @@ bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) { pop(); return true; } - return Document::handle_nav_command(event, cmd); + + return mPrelaunch ? false : Document::handle_nav_command(event, cmd); } void GraphicsTuner::reset_default() { diff --git a/src/dusk/ui/graphics_tuner.hpp b/src/dusk/ui/graphics_tuner.hpp index 3f2418ef30..b020db1128 100644 --- a/src/dusk/ui/graphics_tuner.hpp +++ b/src/dusk/ui/graphics_tuner.hpp @@ -59,7 +59,7 @@ struct GraphicsTunerProps { class GraphicsTuner : public Document { public: - explicit GraphicsTuner(GraphicsTunerProps props); + explicit GraphicsTuner(GraphicsTunerProps props, bool prelaunch); void show() override; void hide(bool close) override; @@ -87,6 +87,7 @@ private: int mDefaultValue = 0; std::vector > mComponents; Rml::Element* mRoot; + bool mPrelaunch; }; } // namespace dusk::ui diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index fd7365a39d..edb136e5e0 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -274,7 +274,7 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar& var, - const GraphicsTunerProps& props) { + const GraphicsTunerProps& props, bool prelaunch) { leftPane.register_control( leftPane .add_select_button({ @@ -292,10 +292,10 @@ void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, Con .isModified = [&var] { return var.getValue() != var.getDefaultValue(); }, .submit = false, }) - .on_nav_command([&window, props](Rml::Event&, NavCommand cmd) { + .on_nav_command([&window, props, prelaunch](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) { - window.push(std::make_unique(props)); + window.push(std::make_unique(props, prelaunch)); return true; } return false; @@ -551,7 +551,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .valueMin = 0, .valueMax = 12, .defaultValue = 0, - }); + }, mPrelaunch); graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.shadowResolutionMultiplier, GraphicsTunerProps{ @@ -561,7 +561,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .valueMin = 1, .valueMax = 8, .defaultValue = 1, - }); + }, mPrelaunch); leftPane.add_section("Post-Processing"); graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode, @@ -572,7 +572,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .valueMin = static_cast(BloomMode::Off), .valueMax = static_cast(BloomMode::Dusk), .defaultValue = static_cast(BloomMode::Classic), - }); + }, mPrelaunch); graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier, GraphicsTunerProps{ .option = GraphicsOption::BloomMultiplier, @@ -581,7 +581,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .valueMin = 0, .valueMax = 100, .defaultValue = 100, - }); + }, mPrelaunch); leftPane.add_section("Rendering"); config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation, From bf23d44389261f000b8918625e09f2f2a185b41e Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Sat, 9 May 2026 16:14:36 -0400 Subject: [PATCH 24/53] Fix Syntax Warning (#743) - Remove invalid property value --- res/rml/window.rcss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 8dd29b55e4..45473f312f 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -498,7 +498,6 @@ progress.verification-progress-bar { display: flex; flex-direction: row; flex-wrap: nowrap; - justify-content: stretch; align-items: stretch; gap: 12dp; width: 100%; From 23dc9bc39ac75b1017d6b08cf36293d3754b4500 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Sat, 9 May 2026 13:14:45 -0700 Subject: [PATCH 25/53] Disable setting default mappings for now (#742) --- extern/aurora | 2 +- src/m_Do/m_Do_main.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/extern/aurora b/extern/aurora index 9413f24f65..bd46867176 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 9413f24f65e20969b8320e651af306a2fd0798c8 +Subproject commit bd46867176c8e0020ddd45368d952ef026584517 diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 56ac05b16c..041c219fe9 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -544,7 +544,8 @@ int game_main(int argc, char* argv[]) { ApplyCVarOverrides(parsed_arg_options["cvar"]); dusk::InitializeCrashReporting(); EnsureInitialPipelineCache(dusk::ConfigPath); - PADSetDefaultMapping(&defaultPadMapping); + // TODO: How to handle this? + //PADSetDefaultMapping(&defaultPadMapping, PAD_TYPE_STANDARD); { const auto configPathString = dusk::ConfigPath.string(); From ad9c460ec9da7adaf7851a1a5b7d115e76cf4565 Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Sat, 9 May 2026 16:23:59 -0400 Subject: [PATCH 26/53] Navigate Carousel Without Focus (#741) * Navigate Carousel Without Focus - Allow left/right inputs to change the setting value without having the arrow explicitly clicked * Formatting --- src/dusk/ui/graphics_tuner.cpp | 9 ++++++--- src/dusk/ui/graphics_tuner.hpp | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/dusk/ui/graphics_tuner.cpp b/src/dusk/ui/graphics_tuner.cpp index 188f0911c5..4f68d4a078 100644 --- a/src/dusk/ui/graphics_tuner.cpp +++ b/src/dusk/ui/graphics_tuner.cpp @@ -195,8 +195,7 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) { GraphicsTuner::GraphicsTuner(GraphicsTunerProps props, bool prelaunch) : Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin), - mValueMax(props.valueMax), mDefaultValue(props.defaultValue), mPrelaunch(prelaunch) -{ + mValueMax(props.valueMax), mDefaultValue(props.defaultValue), mPrelaunch(prelaunch) { if (mDocument == nullptr) { return; } @@ -208,7 +207,7 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props, bool prelaunch) description->SetInnerRML(escape(props.helpText)); } if (auto* carouselParent = mDocument->GetElementById("carousel-container")) { - add_component(carouselParent, + mCarousel = &add_component(carouselParent, SteppedCarousel::Props{ .min = mValueMin, .max = mValueMax, @@ -283,6 +282,10 @@ bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) { return true; } + if (mCarousel && mCarousel->handle_nav_command(cmd)) { + return true; + } + return mPrelaunch ? false : Document::handle_nav_command(event, cmd); } diff --git a/src/dusk/ui/graphics_tuner.hpp b/src/dusk/ui/graphics_tuner.hpp index b020db1128..254aada22b 100644 --- a/src/dusk/ui/graphics_tuner.hpp +++ b/src/dusk/ui/graphics_tuner.hpp @@ -28,9 +28,9 @@ public: bool focus() override; void update() override; + bool handle_nav_command(NavCommand cmd); private: - bool handle_nav_command(NavCommand cmd); void apply(int value); Props mProps; @@ -86,6 +86,7 @@ private: int mValueMax = 0; int mDefaultValue = 0; std::vector > mComponents; + SteppedCarousel* mCarousel; Rml::Element* mRoot; bool mPrelaunch; }; From 45ef0d72b1555e33f63b40022fc1bb44933dfab9 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 15:03:21 -0600 Subject: [PATCH 27/53] Fix Android backgrounding & re-hide bars after swipe --- CMakeLists.txt | 4 -- extern/aurora | 2 +- .../com/twilitrealm/dusk/DuskActivity.java | 38 +++++++++++++++---- .../main/java/org/libsdl/app/SDLSurface.java | 5 +++ 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 60aeb4e3fd..4d69993b5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -115,10 +115,6 @@ option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON) if(ANDROID) set(DUSK_MOVIE_SUPPORT OFF) - set(NOD_COMPRESS_BZIP2 OFF CACHE BOOL "" FORCE) - set(NOD_COMPRESS_LZMA OFF CACHE BOOL "" FORCE) - set(NOD_COMPRESS_ZLIB OFF CACHE BOOL "" FORCE) - set(NOD_COMPRESS_ZSTD OFF CACHE BOOL "" FORCE) endif () option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF) diff --git a/extern/aurora b/extern/aurora index bd46867176..ac165cf1a6 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit bd46867176c8e0020ddd45368d952ef026584517 +Subproject commit ac165cf1a6b283b300021078bd322358fb865558 diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java index f9046ad5fc..d7098239cd 100644 --- a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java +++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java @@ -10,6 +10,7 @@ import android.os.Bundle; import android.provider.OpenableColumns; import android.util.Log; import android.view.View; +import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController; @@ -70,19 +71,42 @@ public class DuskActivity extends SDLActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + hideSystemBars(); + } + @Override + protected void onResume() { + super.onResume(); + hideSystemBars(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + hideSystemBars(); + } + } + + private void hideSystemBars() { + Window window = getWindow(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - WindowInsetsController ctrl = getWindow().getDecorView().getWindowInsetsController(); + window.setDecorFitsSystemWindows(false); + WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController(); if (ctrl != null) { + ctrl.setSystemBarsBehavior( + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); ctrl.hide(WindowInsets.Type.systemBars()); } - }else { - View decorView = getWindow().getDecorView(); - // Hide the status bar. - int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN; + } else { + View decorView = window.getDecorView(); + int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE; decorView.setSystemUiVisibility(uiOptions); - // Remember that you should never show the action bar if the - // status bar is hidden, so hide that too if necessary. ActionBar actionBar = getActionBar(); if (actionBar != null) { actionBar.hide(); diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java index 1579b73345..5ed335ac39 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java @@ -35,6 +35,8 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener, SensorEventListener, ScaleGestureDetector.OnScaleGestureListener { + private static native void auroraNativeSetSurfaceReady(boolean ready); + // Sensors protected SensorManager mSensorManager; protected Display mDisplay; @@ -96,6 +98,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, @Override public void surfaceCreated(SurfaceHolder holder) { Log.v("SDL", "surfaceCreated()"); + auroraNativeSetSurfaceReady(false); SDLActivity.onNativeSurfaceCreated(); } @@ -103,6 +106,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.v("SDL", "surfaceDestroyed()"); + auroraNativeSetSurfaceReady(false); // Transition to pause, if needed SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED; @@ -192,6 +196,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, /* Surface is ready */ mIsSurfaceReady = true; + auroraNativeSetSurfaceReady(true); SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED; SDLActivity.handleNativeState(); From 8280ac00a0569c378005592ede87d8dd2bc2f212 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Sat, 9 May 2026 17:06:58 -0400 Subject: [PATCH 28/53] Always disable cursor if Gyro is set to Mouse mode and the menu is not open (#736) Co-authored-by: MelonSpeedruns --- src/dusk/imgui/ImGuiConsole.cpp | 24 ++++++++++++------------ src/dusk/ui/document.cpp | 15 +++++++++++++++ src/dusk/ui/document.hpp | 2 ++ src/dusk/ui/menu_bar.cpp | 2 ++ src/dusk/ui/prelaunch.cpp | 2 ++ 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 4793b717cc..13f785cdee 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -376,18 +376,18 @@ namespace dusk { } // Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds. - ImGuiIO& io = ImGui::GetIO(); - if (showMenu) { - mouseHideTimer = 0.0f; - ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. - } else if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) { - mouseHideTimer = 0.0f; - ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. - } else if (mouseHideTimer <= 3.0f) { - mouseHideTimer += ImGui::GetIO().DeltaTime; - } else { - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; - SDL_HideCursor(); + if (dusk::getSettings().game.gyroMode.getValue() != GyroMode::Mouse) + { + ImGuiIO& io = ImGui::GetIO(); + if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) { + mouseHideTimer = 0.0f; + ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. + } else if (mouseHideTimer <= 3.0f) { + mouseHideTimer += ImGui::GetIO().DeltaTime; + } else { + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; + SDL_HideCursor(); + } } ShowToasts(); diff --git a/src/dusk/ui/document.cpp b/src/dusk/ui/document.cpp index a7bcc3f9ed..4296ac27d9 100644 --- a/src/dusk/ui/document.cpp +++ b/src/dusk/ui/document.cpp @@ -5,6 +5,7 @@ #include "Z2AudioLib/Z2SeMgr.h" #include "m_Do/m_Do_audio.h" +#include namespace dusk::ui { namespace { @@ -106,6 +107,7 @@ bool Document::visible() const { bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) { if (cmd == NavCommand::Menu) { + toggle_cursor_if_gyro(!visible()); mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen); toggle(); return true; @@ -113,4 +115,17 @@ bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) { return false; } +void Document::toggle_cursor_if_gyro(bool cursor_enabled) { + if (dusk::getSettings().game.gyroMode.getValue() == GyroMode::Mouse) + { + if (cursor_enabled) { + ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; + SDL_ShowCursor(); + } else { + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; + SDL_HideCursor(); + } + } +} + } // namespace dusk::ui diff --git a/src/dusk/ui/document.hpp b/src/dusk/ui/document.hpp index d0f4cae841..eed489beb3 100644 --- a/src/dusk/ui/document.hpp +++ b/src/dusk/ui/document.hpp @@ -43,6 +43,8 @@ public: bool pending_close() const { return mPendingClose; } bool closed() const { return mClosed; } + void toggle_cursor_if_gyro(bool); + protected: virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd); diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index 81c2eea441..ea791d745b 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -42,6 +42,7 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById( mTabBar = std::make_unique(mRoot, TabBar::Props{ .onClose = [this] { + toggle_cursor_if_gyro(false); mDoAud_seStartMenu(kSoundMenuClose); hide(false); }, @@ -203,6 +204,7 @@ bool MenuBar::handle_nav_command(Rml::Event& event, NavCommand cmd) { return true; } if (cmd == NavCommand::Cancel && visible()) { + toggle_cursor_if_gyro(false); mDoAud_seStartMenu(kSoundMenuClose); hide(false); return true; diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index 5c41958409..0be1a3790c 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -687,6 +687,8 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB return; } + toggle_cursor_if_gyro(false); + mDoAud_seStartMenu(kSoundPlay); show_menu_notification(); From 63b3ce484929dfdc47680d8f059b5061bf297c5b Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 9 May 2026 15:16:54 -0700 Subject: [PATCH 29/53] temp fix for speedrun timer controls --- src/dusk/imgui/ImGuiMenuGame.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 79c032ee35..783bbbdeec 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -56,12 +56,12 @@ namespace dusk { } // L+R+A+Start to reset timer - if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigStart(PAD_1)) { + if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) { m_speedrunInfo.reset(); } // L+R+A+Z to manually stop timer - if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) { + if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) { if (m_speedrunInfo.m_isRunStarted) { m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp; m_speedrunInfo.m_isRunStarted = false; From a2f463d14609295d0048f48e1d1ef0dfb722fc0d Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Sat, 9 May 2026 20:15:33 -0400 Subject: [PATCH 30/53] Fix Gyro Sensitivity Axes Fixes #759 --- src/dusk/gyro.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index 4011cbb721..680d500631 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -140,8 +140,8 @@ void read(float dt) { float my_rel = 0.0f; SDL_GetRelativeMouseState(&mx_rel, &my_rel); // Convert pixels to radians - s_pitch_rad = my_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX; - s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityY; + s_pitch_rad = my_rel * kMousePixelToRad * getSettings().game.gyroSensitivityY; + s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX; s_roll_rad = 0.0f; s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; @@ -184,7 +184,7 @@ void read(float dt) { const float yaw_rate = apply_deadband(s_smooth_gy, deadband); const float roll_rate = apply_deadband(s_smooth_gz, deadband); - s_pitch_rad = -pitch_rate * dt * getSettings().game.gyroSensitivityX; + s_pitch_rad = -pitch_rate * dt * getSettings().game.gyroSensitivityY; s_roll_rad = roll_rate * dt * getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X float horizontal_rate = yaw_rate; @@ -223,7 +223,7 @@ void read(float dt) { } } - s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityY; + s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityX; s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; From 3b1118229b7ba4ae719adc13893fa9604ba18a36 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 20:22:30 -0600 Subject: [PATCH 31/53] Add Android DocumentsProvider --- .../android/app/src/main/AndroidManifest.xml | 11 + .../dusk/DuskDocumentsProvider.java | 373 ++++++++++++++++++ .../app/src/main/res/values/strings.xml | 2 + 3 files changed, 386 insertions(+) create mode 100644 platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml index f3e11030ca..f5a0365856 100644 --- a/platforms/android/app/src/main/AndroidManifest.xml +++ b/platforms/android/app/src/main/AndroidManifest.xml @@ -24,6 +24,17 @@ + + + + + + 0 ? displayName.substring(0, dot) : displayName; + final String extension = dot > 0 ? displayName.substring(dot) : ""; + for (int i = 1; i < 100; ++i) { + file = new File(parent, baseName + " (" + i + ")" + extension); + if (!file.exists()) { + return file; + } + } + return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension); + } + + private static int modeToParcelMode(String mode) { + if ("r".equals(mode)) { + return ParcelFileDescriptor.MODE_READ_ONLY; + } + if ("w".equals(mode) || "wt".equals(mode)) { + return ParcelFileDescriptor.MODE_WRITE_ONLY | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE; + } + if ("wa".equals(mode)) { + return ParcelFileDescriptor.MODE_WRITE_ONLY | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_APPEND; + } + if ("rw".equals(mode)) { + return ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE; + } + if ("rwt".equals(mode)) { + return ParcelFileDescriptor.MODE_READ_WRITE | + ParcelFileDescriptor.MODE_CREATE | + ParcelFileDescriptor.MODE_TRUNCATE; + } + return ParcelFileDescriptor.MODE_READ_ONLY; + } + + private static String getMimeType(File file) { + final int dot = file.getName().lastIndexOf('.'); + if (dot >= 0) { + final String extension = file.getName().substring(dot + 1).toLowerCase(); + final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mimeType != null) { + return mimeType; + } + } + return "application/octet-stream"; + } + + private static void deleteRecursively(File file) throws FileNotFoundException { + if (file.isDirectory()) { + final File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + } + if (!file.delete()) { + throw new FileNotFoundException("Unable to delete document: " + file); + } + } + + private static boolean isInside(File parent, File child) { + try { + final String parentPath = canonicalPath(parent); + final String childPath = canonicalPath(child); + return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator); + } catch (FileNotFoundException e) { + return false; + } + } + + private static boolean sameFile(File a, File b) { + try { + return canonicalPath(a).equals(canonicalPath(b)); + } catch (FileNotFoundException e) { + return false; + } + } + + private static String canonicalPath(File file) throws FileNotFoundException { + try { + return file.getCanonicalPath(); + } catch (IOException e) { + throw asFileNotFound("Unable to resolve path", e); + } + } + + private static FileNotFoundException asFileNotFound(String message, IOException cause) { + final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage()); + exception.initCause(cause); + return exception; + } +} diff --git a/platforms/android/app/src/main/res/values/strings.xml b/platforms/android/app/src/main/res/values/strings.xml index 79d8812b82..752dae76bf 100644 --- a/platforms/android/app/src/main/res/values/strings.xml +++ b/platforms/android/app/src/main/res/values/strings.xml @@ -1,4 +1,6 @@ Dusk + Dusk Data + Saves, texture packs, settings, and logs From c66cccf6607a52f3621bf135bd8c99a321143379 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sun, 10 May 2026 04:24:50 +0200 Subject: [PATCH 32/53] Fix handling of Unicode paths on Windows (#767) I love C++ --- include/dusk/io.hpp | 32 ++++++++++++++++++++++++++++++-- src/dusk/achievements.cpp | 4 ++-- src/dusk/config.cpp | 12 +++++++----- src/dusk/io.cpp | 28 +++++++++++++++++++++++++--- src/dusk/logging.cpp | 10 ++++++---- src/m_Do/m_Do_main.cpp | 6 +++--- 6 files changed, 73 insertions(+), 19 deletions(-) diff --git a/include/dusk/io.hpp b/include/dusk/io.hpp index fb71a77d68..57fda3feac 100644 --- a/include/dusk/io.hpp +++ b/include/dusk/io.hpp @@ -7,6 +7,10 @@ // C++ (no error codes) have a file system API functional enough for me to use. // Here you go, this one's inspired by C#. I only wrote the functions I need. +namespace std::filesystem { + class path; +} + namespace dusk::io { /** @@ -15,7 +19,7 @@ namespace dusk::io { * Methods on this class throw appropriate C++ exceptions when an error occurs. */ class FileStream { - void* file; + FILE* file; public: FileStream() noexcept; @@ -23,7 +27,7 @@ public: /** * \brief Take ownership of a FILE* handle. */ - explicit FileStream(void* file); + explicit FileStream(FILE* file); FileStream(const FileStream& other) = delete; FileStream(FileStream&& other) noexcept; @@ -34,6 +38,11 @@ public: */ static FileStream OpenRead(const char* utf8Path); + /** + * \brief Open a file for reading at the given path. + */ + static FileStream OpenRead(const std::filesystem::path& path); + /** * \brief Create a file for writing. * @@ -41,16 +50,33 @@ public: */ static FileStream Create(const char* utf8Path); + /** + * \brief Create a file for writing. + * + * If there is an existing file, its contents are demolished. + */ + static FileStream Create(const std::filesystem::path& path); + /** * \brief Read the byte contents of a file directly into a vector. */ static std::vector ReadAllBytes(const char* utf8Path); + /** + * \brief Read the byte contents of a file directly into a vector. + */ + static std::vector ReadAllBytes(const std::filesystem::path& path); + /** * \brief Read the byte contents of a file directly into a vector. */ static void WriteAllText(const char* utf8Path, std::string_view text); + /** + * \brief Read the byte contents of a file directly into a vector. + */ + static void WriteAllText(const std::filesystem::path& path, std::string_view text); + /** * \brief Read the remaining contents of the file directly into a vector. */ @@ -67,6 +93,8 @@ public: * Write data to the file. */ void Write(const char* data, size_t dataLen); + + FILE* ToInner(); }; } diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index cac6f776b6..28e66c891e 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -1030,7 +1030,7 @@ void AchievementSystem::load() { return; } try { - auto data = io::FileStream::ReadAllBytes(filePath.string().c_str()); + auto data = io::FileStream::ReadAllBytes(filePath); auto j = json::parse(data); if (!j.is_object()) { return; @@ -1067,7 +1067,7 @@ void AchievementSystem::save() { } try { io::FileStream::WriteAllText( - (dusk::ConfigPath / ACHIEVEMENTS_FILENAME).string().c_str(), + dusk::ConfigPath / ACHIEVEMENTS_FILENAME, j.dump(2) ); } catch (const std::exception&) {} diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 940797c9ce..8225536d34 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -23,8 +23,8 @@ aurora::Module DuskConfigLog("dusk::config"); static absl::flat_hash_map RegisteredConfigVars; static bool RegistrationDone = false; -static std::string GetConfigJsonPath() { - return (dusk::ConfigPath / ConfigFileName).string(); +static std::u8string GetConfigJsonPath() { + return (dusk::ConfigPath / ConfigFileName).u8string(); } ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) { @@ -189,7 +189,7 @@ void dusk::config::LoadFromUserPreferences() { if (configJsonPath.empty()) { return; } - LoadFromFileName(configJsonPath.c_str()); + LoadFromFileName(reinterpret_cast(configJsonPath.c_str())); } static void LoadFromPath(const char* path) { @@ -241,7 +241,9 @@ void dusk::config::Save() { return; } - DuskConfigLog.info("Saving config to '{}'", configJsonPath); + DuskConfigLog.info( + "Saving config to '{}'", + reinterpret_cast(configJsonPath.c_str())); json j; @@ -251,7 +253,7 @@ void dusk::config::Save() { } } - io::FileStream::WriteAllText(configJsonPath.c_str(), j.dump(4)); + io::FileStream::WriteAllText(reinterpret_cast(configJsonPath.c_str()), j.dump(4)); } ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) { diff --git a/src/dusk/io.cpp b/src/dusk/io.cpp index 50b5cab5bd..dca8a25e82 100644 --- a/src/dusk/io.cpp +++ b/src/dusk/io.cpp @@ -58,7 +58,7 @@ static FILE* OpenCore(const char* path, const MODE_TYPE* mode) { FileStream::FileStream() noexcept : file(nullptr) { } -FileStream::FileStream(void* file) : file(file) { +FileStream::FileStream(FILE* file) : file(file) { if (!file) { CRASH("Invalid file handle"); } @@ -78,10 +78,18 @@ FileStream FileStream::OpenRead(const char* utf8Path) { return FileStream(OpenCore(utf8Path, MODE("rb"))); } +FileStream FileStream::OpenRead(const std::filesystem::path& path) { + return FileStream(OpenCore(path, MODE("rb"))); +} + FileStream FileStream::Create(const char* utf8Path) { return FileStream(OpenCore(utf8Path, MODE("wb"))); } +FileStream FileStream::Create(const std::filesystem::path& path) { + return FileStream(OpenCore(path, MODE("wb"))); +} + std::vector FileStream::ReadFull() { const auto fileHandle = ThrowIfNotOpen(*this); @@ -128,7 +136,11 @@ std::vector FileStream::ReadFull() { } std::vector FileStream::ReadAllBytes(const char* utf8Path) { - auto handle = OpenRead(utf8Path); + return ReadAllBytes(reinterpret_cast(utf8Path)); +} + +std::vector FileStream::ReadAllBytes(const std::filesystem::path& path) { + auto handle = OpenRead(path); return std::move(handle.ReadFull()); } @@ -142,6 +154,16 @@ void FileStream::Write(const char* data, size_t dataLen) { } void FileStream::WriteAllText(const char* utf8Path, const std::string_view text) { - auto handle = Create(utf8Path); + WriteAllText(reinterpret_cast(utf8Path), text); +} + +void FileStream::WriteAllText(const std::filesystem::path& path, const std::string_view text) { + auto handle = Create(path); handle.Write(text.data(), text.size()); } + +FILE* FileStream::ToInner() { + auto handle = file; + file = nullptr; + return handle; +} \ No newline at end of file diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index 8b96fcffd5..67723af4df 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -8,6 +8,7 @@ #include #include +#include "dusk/io.hpp" #include "tracy/Tracy.hpp" #if TARGET_ANDROID @@ -40,7 +41,7 @@ std::atomic g_logStateAlive(true); struct LogState { std::mutex mutex; FILE* file = nullptr; - std::string filePath; + std::u8string filePath; ~LogState() { CloseFile(); @@ -212,14 +213,14 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL } const std::filesystem::path logPath = logsDir / MakeTimestampedLogName(); - g_logState.file = std::fopen(logPath.string().c_str(), "wb"); + g_logState.file = io::FileStream::Create(logPath).ToInner(); if (g_logState.file == nullptr) { std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n", logPath.string().c_str()); return; } - g_logState.filePath = logPath.string(); + g_logState.filePath = logPath.u8string(); aurora::g_config.logCallback = &aurora_log_callback; aurora::g_config.logLevel = logLevel; WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24); @@ -237,5 +238,6 @@ const char* dusk::GetLogFilePath() { return nullptr; } std::lock_guard lock(g_logState.mutex); - return g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str(); + return reinterpret_cast( + g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str()); } diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 041c219fe9..55481cfeb5 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -398,7 +398,7 @@ static std::filesystem::path CalculateConfigPath() { DuskLog.fatal("Unable to get PrefPath: {}", SDL_GetError()); } - return result; + return reinterpret_cast(result); } static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) { @@ -548,10 +548,10 @@ int game_main(int argc, char* argv[]) { //PADSetDefaultMapping(&defaultPadMapping, PAD_TYPE_STANDARD); { - const auto configPathString = dusk::ConfigPath.string(); + const auto configPathString = dusk::ConfigPath.u8string(); AuroraConfig config{}; config.appName = dusk::AppName; - config.configPath = configPathString.c_str(); + config.configPath = reinterpret_cast(configPathString.c_str()); config.vsync = dusk::getSettings().video.enableVsync; config.startFullscreen = dusk::getSettings().video.enableFullscreen; config.windowPosX = -1; From 5e08b810fc026116da95107a497d383cc6fa346f Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 20:26:37 -0600 Subject: [PATCH 33/53] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index ac165cf1a6..63606a4326 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit ac165cf1a6b283b300021078bd322358fb865558 +Subproject commit 63606a43265a3bc18dafd500ab4d7a2108f109e6 From 7562486449e58a9133acafcb9af681b70f25f26e Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 20:40:59 -0600 Subject: [PATCH 34/53] Log disc verification status --- src/dusk/io.cpp | 3 ++- src/dusk/iso_validate.cpp | 20 ++++++++++++++++++++ src/dusk/iso_validate.hpp | 1 + src/dusk/ui/prelaunch.cpp | 10 +++++++++- src/m_Do/m_Do_main.cpp | 4 ++++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/dusk/io.cpp b/src/dusk/io.cpp index dca8a25e82..3c87a66acf 100644 --- a/src/dusk/io.cpp +++ b/src/dusk/io.cpp @@ -1,7 +1,8 @@ -#include "dusk/io.hpp" #include #include +#include "dusk/io.hpp" + using namespace dusk::io; #if _WIN32 diff --git a/src/dusk/iso_validate.cpp b/src/dusk/iso_validate.cpp index 4d9c48d826..00cd365ca9 100644 --- a/src/dusk/iso_validate.cpp +++ b/src/dusk/iso_validate.cpp @@ -9,6 +9,8 @@ #include #include +#include "dusk/logging.h" + namespace { constexpr uint8_t hex_nibble_to_u8(char c) { @@ -42,6 +44,18 @@ constexpr XXH128_hash_t parse_xxh128(std::string_view hex) { }; } +const char* verification_state_name(dusk::DiscVerificationState state) noexcept { + switch (state) { + case dusk::DiscVerificationState::Success: + return "verified"; + case dusk::DiscVerificationState::HashMismatch: + return "hash mismatch"; + case dusk::DiscVerificationState::Unknown: + default: + return "unknown"; + } +} + } // namespace namespace dusk::iso { @@ -248,4 +262,10 @@ bool isPal(const char* path) { DiscInfo info{}; return inspect(path, info) == ValidationError::Success && info.isPal; } + +void log_verification_state(std::string_view path, DiscVerificationState state) { + const std::string pathText = path.empty() ? "" : std::string(path); + DuskLog.info( + "Disc verification status: {} (path: {})", verification_state_name(state), pathText); +} } // namespace dusk::iso diff --git a/src/dusk/iso_validate.hpp b/src/dusk/iso_validate.hpp index c737a017d9..176284fd02 100644 --- a/src/dusk/iso_validate.hpp +++ b/src/dusk/iso_validate.hpp @@ -31,6 +31,7 @@ struct DiscInfo { ValidationError inspect(const char* path, DiscInfo& info); ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info); bool isPal(const char* path); +void log_verification_state(std::string_view path, DiscVerificationState state); } // namespace dusk::iso diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index 0be1a3790c..03ad57a3bb 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -302,9 +302,17 @@ std::string get_error_msg(iso::ValidationError error) { } void persist_disc_choice(const std::string& path, iso::ValidationError validation) { + const auto previousPath = getSettings().backend.isoPath.getValue(); + const auto previousVerification = getSettings().backend.isoVerification.getValue(); + const auto verification = verification_to_config(validation); + getSettings().backend.isoPath.setValue(path); - getSettings().backend.isoVerification.setValue(verification_to_config(validation)); + getSettings().backend.isoVerification.setValue(verification); config::Save(); + + if (previousPath != path || previousVerification != verification) { + iso::log_verification_state(path, verification); + } } void apply_valid_disc_result( diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 55481cfeb5..cf51f5fb86 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -649,6 +649,10 @@ int game_main(int argc, char* argv[]) { } } + dusk::iso::log_verification_state( + dusk::getSettings().backend.isoPath.getValue(), + dusk::getSettings().backend.isoVerification.getValue()); + if (!dvd_opened) { if (dusk::getSettings().backend.isoPath.getValue().empty()) { forcePreLaunchUI = true; From 0c23bd433248f634f2f2a6424b590b0e6eb67c5c Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 20:57:34 -0600 Subject: [PATCH 35/53] Add "Open Data Folder" to Interface menu --- include/dusk/main.h | 9 +++++++++ src/dusk/imgui/ImGuiConsole.hpp | 22 ---------------------- src/dusk/ui/settings.cpp | 13 +++++++++++++ src/m_Do/m_Do_main.cpp | 26 ++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/include/dusk/main.h b/include/dusk/main.h index 787c2541e2..c5c4fb27d1 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -7,6 +7,14 @@ #include +#if defined(_WIN32) || \ + (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \ + (defined(__linux__) && !defined(__ANDROID__)) +#define DUSK_CAN_OPEN_DATA_FOLDER 1 +#else +#define DUSK_CAN_OPEN_DATA_FOLDER 0 +#endif + namespace dusk { extern bool IsRunning; extern bool IsShuttingDown; @@ -22,6 +30,7 @@ namespace dusk { #endif void RequestRestart() noexcept; + bool OpenDataFolder(); } #endif // DUSK_MAIN_H diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 6715846e7b..6362a146b6 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -2,11 +2,9 @@ #define DUSK_IMGUI_HPP #include -#include #include #include -#include #include #include "ImGuiMenuGame.hpp" @@ -73,24 +71,4 @@ bool ImGuiButtonCenter(std::string_view text); float ImGuiScale(); } // namespace dusk -#if defined(_WIN32) || \ - (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \ - (defined(__linux__) && !defined(__ANDROID__)) -#define DUSK_CAN_OPEN_DATA_FOLDER 1 - -namespace fs = std::filesystem; - -static void OpenDataFolder() { - const std::string path = fs::absolute(dusk::ConfigPath).generic_string(); -#if defined(_WIN32) - const std::string url = std::string("file:///") + path; -#else - const std::string url = std::string("file://") + path; -#endif - (void)SDL_OpenURL(url.c_str()); -} -#else -#define DUSK_CAN_OPEN_DATA_FOLDER 0 -#endif - #endif // DUSK_IMGUI_HPP diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index edb136e5e0..d904c823d7 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -9,6 +9,7 @@ #include "dusk/file_select.hpp" #include "dusk/imgui/ImGuiEngine.hpp" #include "dusk/livesplit.h" +#include "dusk/main.h" #include "graphics_tuner.hpp" #include "m_Do/m_Do_main.h" #include "menu_bar.hpp" @@ -946,6 +947,18 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { auto& rightPane = add_child(content, Pane::Type::Uncontrolled); leftPane.add_section("Dusk"); +#if DUSK_CAN_OPEN_DATA_FOLDER + leftPane.register_control( + leftPane.add_button("Open Data Folder").on_pressed([] { + mDoAud_seStartMenu(kSoundClick); + dusk::OpenDataFolder(); + }), + rightPane, [](Pane& pane) { + pane.add_text( + "Open the folder where Dusk stores settings, saves, logs, texture " + "replacements, and other app data."); + }); +#endif leftPane.register_control( leftPane.add_select_button({ .key = "Notifications", diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index cf51f5fb86..937c9b81aa 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -71,6 +71,7 @@ #include #include "SDL3/SDL_filesystem.h" +#include "SDL3/SDL_misc.h" #include "cxxopts.hpp" #include "d/actor/d_a_movie_player.h" #include "dusk/audio/DuskAudioSystem.h" @@ -114,6 +115,31 @@ void dusk::RequestRestart() noexcept { IsRunning = false; } +bool dusk::OpenDataFolder() { +#if DUSK_CAN_OPEN_DATA_FOLDER + std::error_code ec; + std::filesystem::path path = std::filesystem::absolute(ConfigPath, ec); + if (ec) { + DuskLog.warn("Failed to resolve absolute data folder path '{}': {}", + ConfigPath.string(), ec.message()); + path = ConfigPath; + } + +#if defined(_WIN32) + const std::string url = "file:///" + path.generic_string(); +#else + const std::string url = "file://" + path.generic_string(); +#endif + if (!SDL_OpenURL(url.c_str())) { + DuskLog.warn("Failed to open data folder '{}': {}", path.string(), SDL_GetError()); + return false; + } + return true; +#else + return false; +#endif +} + s32 LOAD_COPYDATE(void*) { char buffer[32]; memset(buffer, 0, sizeof(buffer)); From daff15702787f35af52823ab28610df6ede87d89 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 21:42:32 -0600 Subject: [PATCH 36/53] Fix change notifications in Android DocumentsProvider --- .../dusk/DuskDocumentsProvider.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java index edab8d9bc1..79fa8894d0 100644 --- a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java +++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java @@ -1,10 +1,14 @@ package dev.twilitrealm.dusk; +import android.content.ContentResolver; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Bundle; import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; @@ -76,10 +80,24 @@ public class DuskDocumentsProvider extends DocumentsProvider { @Override public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException + { + return queryChildDocumentsInternal(parentDocumentId, projection); + } + + @Override + public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs) + throws FileNotFoundException + { + return queryChildDocumentsInternal(parentDocumentId, projection); + } + + private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection) + throws FileNotFoundException { final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); final File parent = getFileForDocumentId(parentDocumentId); final File[] files = parent.listFiles(); + result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId)); if (files == null) { return result; @@ -129,6 +147,7 @@ public class DuskDocumentsProvider extends DocumentsProvider { throw new FileNotFoundException("Unable to create document: " + displayName); } + notifyChildrenChanged(parentDocumentId); return getDocumentIdForFile(file); } @@ -140,9 +159,13 @@ public class DuskDocumentsProvider extends DocumentsProvider { } final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName)); + final String parentDocumentId = getDocumentIdForFile(file.getParentFile()); if (!file.renameTo(target)) { throw new FileNotFoundException("Unable to rename document: " + documentId); } + notifyDocumentChanged(documentId); + notifyDocumentChanged(getDocumentIdForFile(target)); + notifyChildrenChanged(parentDocumentId); return getDocumentIdForFile(target); } @@ -153,7 +176,10 @@ public class DuskDocumentsProvider extends DocumentsProvider { } final File file = getFileForDocumentId(documentId); + final String parentDocumentId = getDocumentIdForFile(file.getParentFile()); deleteRecursively(file); + notifyDocumentChanged(documentId); + notifyChildrenChanged(parentDocumentId); } @Override @@ -325,6 +351,20 @@ public class DuskDocumentsProvider extends DocumentsProvider { return "application/octet-stream"; } + private Uri getChildDocumentsUri(String parentDocumentId) { + return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId); + } + + private void notifyChildrenChanged(String parentDocumentId) { + final ContentResolver resolver = getContext().getContentResolver(); + resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false); + } + + private void notifyDocumentChanged(String documentId) { + final ContentResolver resolver = getContext().getContentResolver(); + resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false); + } + private static void deleteRecursively(File file) throws FileNotFoundException { if (file.isDirectory()) { final File[] children = file.listFiles(); From df23edcb697d8bc72073d21368b7929c73c7d2c4 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 9 May 2026 21:42:43 -0600 Subject: [PATCH 37/53] Add iOS UIFileSharingEnabled integration --- platforms/ios/Info.plist.in | 4 +++ src/m_Do/m_Do_main.cpp | 71 +++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/platforms/ios/Info.plist.in b/platforms/ios/Info.plist.in index 881d5aff35..72174d487d 100644 --- a/platforms/ios/Info.plist.in +++ b/platforms/ios/Info.plist.in @@ -79,5 +79,9 @@ CADisableMinimumFrameDurationOnPhone + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 937c9b81aa..98269c0817 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -84,6 +84,9 @@ #include "f_pc/f_pc_draw.h" #include "tracy/Tracy.hpp" #include +#ifdef __APPLE__ +#include +#endif // --- GLOBALS --- s8 mDoMain::developmentMode = -1; @@ -418,7 +421,71 @@ static void ApplyCVarOverrides(const cxxopts::OptionValue& option) { } } -static std::filesystem::path CalculateConfigPath() { +static void migrate_directory(const std::filesystem::path& from, const std::filesystem::path& to) { + std::error_code ec; + std::filesystem::create_directories(to, ec); + if (ec) { + return; + } + + for (std::filesystem::recursive_directory_iterator it( + from, std::filesystem::directory_options::skip_permission_denied, ec); + it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) + { + if (ec) { + return; + } + + const auto relativePath = std::filesystem::relative(it->path(), from, ec); + if (ec) { + return; + } + + const auto targetPath = to / relativePath; + if (it->is_directory(ec)) { + std::filesystem::create_directories(targetPath, ec); + if (ec) { + return; + } + } else if (it->is_regular_file(ec) && !std::filesystem::exists(targetPath, ec)) { + std::filesystem::create_directories(targetPath.parent_path(), ec); + if (ec) { + return; + } + std::filesystem::copy_file( + it->path(), targetPath, std::filesystem::copy_options::skip_existing, ec); + if (ec) { + return; + } + } + } +} + +static std::filesystem::path calculate_config_path() { +#ifdef __APPLE__ +#if TARGET_OS_IOS && !TARGET_OS_TV + const char* documentsPath = SDL_GetUserFolder(SDL_FOLDER_DOCUMENTS); + if (!documentsPath) { + DuskLog.fatal("Unable to get iOS Documents path: {}", SDL_GetError()); + } + + std::filesystem::path configPath = reinterpret_cast(documentsPath); + + char* oldPrefPath = SDL_GetPrefPath(dusk::OrgName, dusk::AppName); + if (oldPrefPath) { + const std::filesystem::path oldConfigPath = reinterpret_cast(oldPrefPath); + SDL_free(oldPrefPath); + + std::error_code ec; + if (oldConfigPath != configPath && std::filesystem::exists(oldConfigPath, ec)) { + migrate_directory(oldConfigPath, configPath); + } + } + + return configPath; +#endif +#endif + const auto result = SDL_GetPrefPath(dusk::OrgName, dusk::AppName); if (!result) { DuskLog.fatal("Unable to get PrefPath: {}", SDL_GetError()); @@ -562,7 +629,7 @@ int game_main(int argc, char* argv[]) { exit(1); } - dusk::ConfigPath = CalculateConfigPath(); + dusk::ConfigPath = calculate_config_path(); const auto startupLogLevel = static_cast(parsed_arg_options["log-level"].as()); dusk::InitializeFileLogging(dusk::ConfigPath, startupLogLevel); From 8f71c70d1492938df43531b2574cd1946ce390cc Mon Sep 17 00:00:00 2001 From: Krutonium <3945538+Krutonium@users.noreply.github.com> Date: Sun, 10 May 2026 01:41:15 -0400 Subject: [PATCH 38/53] fix io.hpp to enable compiling on GCC15 (#790) --- include/dusk/io.hpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/include/dusk/io.hpp b/include/dusk/io.hpp index 57fda3feac..6de60e3449 100644 --- a/include/dusk/io.hpp +++ b/include/dusk/io.hpp @@ -2,14 +2,12 @@ #define DUSK_IO_HPP #include +#include // I can't believe it's 2026 and neither SDL (no error codes) nor // C++ (no error codes) have a file system API functional enough for me to use. // Here you go, this one's inspired by C#. I only wrote the functions I need. -namespace std::filesystem { - class path; -} namespace dusk::io { From e7d2fbcc0bae243fe1a1ae219fb514788f54d46e Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 10 May 2026 00:08:42 -0600 Subject: [PATCH 39/53] Update CI --- .github/workflows/build.yml | 3 --- CMakeLists.txt | 21 +++++++++++---------- CMakePresets.json | 1 + extern/aurora | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 37679e3f56..49d9bbd89c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,6 @@ jobs: run: ci/build-appimage.sh - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}} @@ -137,7 +136,6 @@ jobs: run: cmake --build --preset ${{matrix.preset}} - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}} @@ -203,7 +201,6 @@ jobs: run: cmake --build --preset x-windows-ci-${{matrix.preset}} - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}} diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d69993b5d..4c4f3180f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,16 @@ option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN") set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment") +# Edit & Continue +if (MSVC) + if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug") + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue") + endif () + if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue") + add_link_options("/INCREMENTAL") + endif () +endif () + if (DUSK_MOVIE_SUPPORT) find_package(libjpeg-turbo 3.0 CONFIG QUIET) if (libjpeg-turbo_FOUND) @@ -150,6 +160,7 @@ if (DUSK_MOVIE_SUPPORT) CMAKE_C_COMPILER_LAUNCHER CMAKE_MAKE_PROGRAM CMAKE_MSVC_RUNTIME_LIBRARY + CMAKE_MSVC_DEBUG_INFORMATION_FORMAT CMAKE_OSX_ARCHITECTURES DEPLOYMENT_TARGET ENABLE_ARC @@ -366,16 +377,6 @@ if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS) list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1) endif () -# Edit & Continue -if (MSVC) - if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug") - set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue") - endif () - if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue") - add_link_options("/INCREMENTAL") - endif () -endif () - if(ANDROID) list(APPEND GAME_COMPILE_DEFS TARGET_ANDROID=1) endif () diff --git a/CMakePresets.json b/CMakePresets.json index 3b2f0cae67..187748c400 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -412,6 +412,7 @@ "x-macos-ci" ], "cacheVariables": { + "AURORA_DAWN_PROVIDER": "vendor", "CMAKE_OSX_ARCHITECTURES": "x86_64", "Rust_CARGO_TARGET": "x86_64-apple-darwin" } diff --git a/extern/aurora b/extern/aurora index 63606a4326..398054316e 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 63606a43265a3bc18dafd500ab4d7a2108f109e6 +Subproject commit 398054316ed66c554e5b900bb1b12f5ccb1104b3 From 453e9580682039fb4e2d3770eadc93f18b9367e1 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 10 May 2026 00:11:05 -0600 Subject: [PATCH 40/53] Update CI (again) --- .github/workflows/build.yml | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49d9bbd89c..f5560bab51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,9 +8,9 @@ on: pull_request: env: - # SCCACHE_GHA_ENABLED: "true" + SCCACHE_GHA_ENABLED: "true" RUSTC_WRAPPER: "sccache" - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + # SENTRY_DSN: ${{ secrets.SENTRY_DSN }} jobs: build-linux: @@ -22,7 +22,7 @@ jobs: matrix: include: - name: GCC x86_64 - runner: [self-hosted, Linux] + runner: ubuntu-latest preset: gcc artifact_arch: x86_64 # - name: GCC aarch64 @@ -41,7 +41,6 @@ jobs: submodules: recursive - name: Install dependencies - if: 'false' # disabled for self-hosted run: | sudo apt-get update sudo apt-get -y install ninja-build clang lld openssl libcurl4-openssl-dev \ @@ -51,7 +50,6 @@ jobs: libxss-dev libfuse2 libusb-1.0-0-dev libdecor-0-dev libpipewire-0.3-dev libunwind-dev - name: Setup sccache - if: 'false' # disabled for self-hosted uses: mozilla-actions/sccache-action@v0.0.9 - name: Print sccache stats @@ -67,6 +65,7 @@ jobs: run: ci/build-appimage.sh - name: Upload artifacts + if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}} @@ -76,7 +75,7 @@ jobs: build-apple: name: Build Apple (${{matrix.name}}) - runs-on: [self-hosted, macOS] + runs-on: macos-latest strategy: fail-fast: false matrix: @@ -85,14 +84,14 @@ jobs: platform: macos preset: x-macos-ci-arm64 artifact_name: macos-appleclang-arm64 - # - name: AppleClang macOS x86_64 - # platform: macos - # preset: x-macos-ci-x86_64 - # artifact_name: macos-appleclang-x86_64 - # - name: AppleClang iOS arm64 - # platform: ios - # preset: x-ios-ci - # artifact_name: ios-appleclang-arm64 + - name: AppleClang macOS x86_64 + platform: macos + preset: x-macos-ci-x86_64 + artifact_name: macos-appleclang-x86_64 + - name: AppleClang iOS arm64 + platform: ios + preset: x-ios-ci + artifact_name: ios-appleclang-arm64 # - name: AppleClang tvOS arm64 # platform: tvos # preset: x-tvos-ci @@ -105,7 +104,6 @@ jobs: submodules: recursive - name: Install dependencies - if: 'false' run: brew install cmake ninja - name: Install Rust iOS target @@ -136,6 +134,7 @@ jobs: run: cmake --build --preset ${{matrix.preset}} - name: Upload artifacts + if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}} @@ -152,7 +151,7 @@ jobs: matrix: include: - name: MSVC x86_64 - runner: [self-hosted, Windows] + runner: windows-latest preset: msvc msvc_arch: amd64 vcpkg_arch: x64 @@ -189,7 +188,6 @@ jobs: uses: mozilla-actions/sccache-action@v0.0.9 - name: Install dependencies - if: 'false' # disabled for self-hosted run: | choco install ninja vcpkg install freetype:${{matrix.vcpkg_arch}}-windows-static zstd:${{matrix.vcpkg_arch}}-windows-static @@ -201,6 +199,7 @@ jobs: run: cmake --build --preset x-windows-ci-${{matrix.preset}} - name: Upload artifacts + if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}} From 04b5861f294441582d01f13df5d79a8eef18f629 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 10 May 2026 00:12:11 -0600 Subject: [PATCH 41/53] Update CI (again x2) --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5560bab51..2008997cec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,7 +65,6 @@ jobs: run: ci/build-appimage.sh - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}} @@ -134,7 +133,6 @@ jobs: run: cmake --build --preset ${{matrix.preset}} - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}} @@ -199,7 +197,6 @@ jobs: run: cmake --build --preset x-windows-ci-${{matrix.preset}} - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags/v') uses: actions/upload-artifact@v7 with: name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}} From 286532904ac0bfa05b9929780217779e332bd03c Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 10 May 2026 00:31:49 -0600 Subject: [PATCH 42/53] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 398054316e..22c2e5e55a 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 398054316ed66c554e5b900bb1b12f5ccb1104b3 +Subproject commit 22c2e5e55a7e00404149dd499e2639cd74fb1189 From 97d032f8b5cab89c1b0efe60dfe634c08ea5431e Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 10 May 2026 00:26:00 -0600 Subject: [PATCH 43/53] Add Android CI --- .github/workflows/build.yml | 67 +++++++++++++++++++++++++++++++++++++ CMakePresets.json | 28 ++++++++++++++++ platforms/android/gradlew | 0 3 files changed, 95 insertions(+) mode change 100644 => 100755 platforms/android/gradlew diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2008997cec..82b387db4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -140,6 +140,73 @@ jobs: build/install/Dusk.app build/install/debug.tar.* + build-android: + name: Build Android (${{matrix.name}}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - name: Clang arm64-v8a + preset: x-android-ci-arm64 + abi: arm64-v8a + artifact_arch: arm64 + rust_target: aarch64-linux-android + + env: + ANDROID_NDK_VERSION: "29.0.14206865" + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get -y install ninja-build + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 17 + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK packages + run: sdkmanager "platforms;android-36" "build-tools;36.1.0" "ndk;${ANDROID_NDK_VERSION}" + + - name: Install Rust Android target + run: | + rustup toolchain install stable + rustup target add ${{matrix.rust_target}} + + - name: Setup sccache + uses: mozilla-actions/sccache-action@v0.0.9 + + - name: Configure CMake + run: cmake --preset ${{matrix.preset}} + + - name: Build native library + run: cmake --build --preset ${{matrix.preset}} --target dusk + + - name: Stage stripped JNI library + run: ANDROID_STAGE_ABIS="${{matrix.abi}}" platforms/android/scripts/stage-jni-libs.sh + + - name: Build APK + working-directory: platforms/android + run: ./gradlew :app:assembleRelease --rerun-tasks + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: dusk-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}} + path: platforms/android/app/build/outputs/apk/release/app-${{matrix.abi}}-release-unsigned.apk + build-windows: name: Build Windows (${{matrix.name}}) runs-on: ${{matrix.runner}} diff --git a/CMakePresets.json b/CMakePresets.json index 187748c400..ea58a72243 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -352,6 +352,25 @@ "ANDROID_ABI": "x86_64" } }, + { + "name": "x-android-ci", + "hidden": true, + "inherits": [ + "android-base", + "ci" + ] + }, + { + "name": "x-android-ci-arm64", + "binaryDir": "${sourceDir}/build/android-arm64", + "inherits": [ + "x-android-ci" + ], + "cacheVariables": { + "ANDROID_ABI": "arm64-v8a", + "Rust_CARGO_TARGET": "aarch64-linux-android" + } + }, { "name": "x-linux-ci", "hidden": true, @@ -556,6 +575,15 @@ "dusk" ] }, + { + "name": "x-android-ci-arm64", + "configurePreset": "x-android-ci-arm64", + "description": "(Internal) Android CI arm64-v8a", + "displayName": "(Internal) Android CI arm64-v8a", + "targets": [ + "dusk" + ] + }, { "name": "windows-msvc-debug", "configurePreset": "windows-msvc-debug", diff --git a/platforms/android/gradlew b/platforms/android/gradlew old mode 100644 new mode 100755 From 4ec8c1aaeed847b6ff4521d6bcb41b2bbd206b7d Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Sun, 10 May 2026 11:32:55 -0400 Subject: [PATCH 44/53] Fix recording mode muting music until next reboot (#832) Co-authored-by: MelonSpeedruns --- src/f_ap/f_ap_game.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 2c6cb0a925..55ef94f8a3 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -740,6 +740,8 @@ static void fapGm_AfterRecord() { fapGm_After(); } +BOOL isRecording = false; + static void duskExecute() { handleGamepadColor(); updateAutoSave(); @@ -747,6 +749,11 @@ static void duskExecute() { if (dusk::getSettings().game.recordingMode) { Z2GetSoundMgr()->getSeqMgr()->getParams()->moveVolume(0.0f, 0); Z2GetSoundMgr()->getStreamMgr()->getParams()->moveVolume(0.0f, 0); + isRecording = true; + } else if (isRecording) { + Z2GetSoundMgr()->getSeqMgr()->getParams()->moveVolume(1.0f, 0); + Z2GetSoundMgr()->getStreamMgr()->getParams()->moveVolume(1.0f, 0); + isRecording = false; } if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) { From a86fa9c16279a47172cd8b78e7538aedb15e7907 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sun, 10 May 2026 18:35:11 +0200 Subject: [PATCH 45/53] State share Unicode fix (#834) --- src/dusk/imgui/ImGuiStateShare.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index a0bd450984..48849e2bc3 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -48,19 +48,18 @@ void ImGuiStateShare::onMergeFileSelected(void* userdata, const char* path, cons -static std::string GetStatesFilePath() { - return (dusk::ConfigPath / STATES_FILENAME).string(); +static std::filesystem::path GetStatesFilePath() { + return ConfigPath / STATES_FILENAME; } void ImGuiStateShare::loadStatesFile() { m_loaded = true; - const std::filesystem::path filePath = dusk::ConfigPath / STATES_FILENAME; + const std::filesystem::path filePath = GetStatesFilePath(); if (!std::filesystem::exists(filePath)) { return; } try { - const std::string pathStr = filePath.string(); - auto data = io::FileStream::ReadAllBytes(pathStr.c_str()); + auto data = io::FileStream::ReadAllBytes(filePath); auto j = json::parse(data); if (!j.is_array()) { return; @@ -85,7 +84,7 @@ void ImGuiStateShare::saveStatesFile() { j.push_back(json{{"name", s.name}, {"data", s.encoded}}); } try { - io::FileStream::WriteAllText(GetStatesFilePath().c_str(), j.dump(2)); + io::FileStream::WriteAllText(GetStatesFilePath(), j.dump(2)); } catch (const std::exception& e) { m_statusMsg = fmt::format("Failed to save states: {}", e.what()); } From 5187fe90c3cb7e95bc74d30a9a0f5dc785a679c9 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 10 May 2026 10:39:11 -0600 Subject: [PATCH 46/53] Seed initial pipeline cache through SDL IO & UTF8 path fixes --- include/dusk/io.hpp | 14 ++++--- src/dusk/logging.cpp | 4 +- src/dusk/main.cpp | 3 +- src/dusk/ui/ui.cpp | 3 +- src/m_Do/m_Do_main.cpp | 92 ++++++++++++++++++++++++++++++++++-------- 5 files changed, 90 insertions(+), 26 deletions(-) diff --git a/include/dusk/io.hpp b/include/dusk/io.hpp index 6de60e3449..2efc4a8d3b 100644 --- a/include/dusk/io.hpp +++ b/include/dusk/io.hpp @@ -1,14 +1,13 @@ #ifndef DUSK_IO_HPP #define DUSK_IO_HPP -#include #include +#include // I can't believe it's 2026 and neither SDL (no error codes) nor // C++ (no error codes) have a file system API functional enough for me to use. // Here you go, this one's inspired by C#. I only wrote the functions I need. - namespace dusk::io { /** @@ -83,9 +82,7 @@ public: /** * Get direct access to the underlying FILE* handle. */ - [[nodiscard]] void* GetFileHandle() const noexcept { - return file; - } + [[nodiscard]] void* GetFileHandle() const noexcept { return file; } /** * Write data to the file. @@ -95,7 +92,14 @@ public: FILE* ToInner(); }; +/** + * Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows. + */ +inline std::string fs_path_to_string(const std::filesystem::path& path) { + const auto u8str = path.u8string(); + return {reinterpret_cast(u8str.c_str())}; } +} // namespace dusk::io #endif // DUSK_IO_HPP diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index 67723af4df..1c0a6ecbdb 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -208,7 +208,7 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL std::filesystem::create_directories(logsDir, ec); if (ec) { std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n", - logsDir.string().c_str(), ec.message().c_str()); + io::fs_path_to_string(logsDir).c_str(), ec.message().c_str()); return; } @@ -216,7 +216,7 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL g_logState.file = io::FileStream::Create(logPath).ToInner(); if (g_logState.file == nullptr) { std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n", - logPath.string().c_str()); + io::fs_path_to_string(logPath).c_str()); return; } diff --git a/src/dusk/main.cpp b/src/dusk/main.cpp index e1b2fd0b6e..d88594248a 100644 --- a/src/dusk/main.cpp +++ b/src/dusk/main.cpp @@ -6,6 +6,7 @@ #include #include "dusk/main.h" +#include "dusk/io.hpp" #include #include @@ -91,7 +92,7 @@ bool RestartProcess(int argc, char* argv[]) { std::vector args; args.reserve(static_cast(std::max(argc, 1))); - args.push_back(executablePath.string()); + args.push_back(dusk::io::fs_path_to_string(executablePath)); for (int i = 1; i < argc; ++i) { args.emplace_back(argv[i] != nullptr ? argv[i] : ""); } diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp index 0ee59e7938..5ef15a6c41 100644 --- a/src/dusk/ui/ui.cpp +++ b/src/dusk/ui/ui.cpp @@ -11,6 +11,7 @@ #include #include "aurora/lib/window.hpp" +#include "dusk/io.hpp" #include "input.hpp" #include "prelaunch.hpp" #include "window.hpp" @@ -19,7 +20,7 @@ namespace dusk::ui { namespace { void load_font(const char* filename, bool fallback = false) { - Rml::LoadFontFace(resource_path(filename).string(), fallback); + Rml::LoadFontFace(io::fs_path_to_string(resource_path(filename)), fallback); } bool sInitialized = false; diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 98269c0817..6db94cc317 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -71,6 +71,7 @@ #include #include "SDL3/SDL_filesystem.h" +#include "SDL3/SDL_iostream.h" #include "SDL3/SDL_misc.h" #include "cxxopts.hpp" #include "d/actor/d_a_movie_player.h" @@ -78,6 +79,7 @@ #include "dusk/audio/DuskDsp.hpp" #include "dusk/config.hpp" #include "dusk/settings.h" +#include "dusk/io.hpp" #include "dusk/version.hpp" #include "dusk/discord_presence.hpp" #include "tracy/Tracy.hpp" @@ -124,7 +126,7 @@ bool dusk::OpenDataFolder() { std::filesystem::path path = std::filesystem::absolute(ConfigPath, ec); if (ec) { DuskLog.warn("Failed to resolve absolute data folder path '{}': {}", - ConfigPath.string(), ec.message()); + io::fs_path_to_string(ConfigPath), ec.message()); path = ConfigPath; } @@ -134,7 +136,8 @@ bool dusk::OpenDataFolder() { const std::string url = "file://" + path.generic_string(); #endif if (!SDL_OpenURL(url.c_str())) { - DuskLog.warn("Failed to open data folder '{}': {}", path.string(), SDL_GetError()); + DuskLog.warn( + "Failed to open data folder '{}': {}", io::fs_path_to_string(path), SDL_GetError()); return false; } return true; @@ -504,16 +507,21 @@ static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) { return; } - const char* basePath = SDL_GetBasePath(); - if (basePath == nullptr) { - DuskLog.warn("Unable to resolve base path while seeding pipeline cache: {}", SDL_GetError()); - return; - } + std::string sourcePathString; + SDL_IOStream* source = nullptr; - const std::filesystem::path initialPipelineCachePath = - std::filesystem::path(basePath) / "initial_pipeline_cache.db"; - if (!std::filesystem::exists(initialPipelineCachePath)) { - DuskLog.info("No bundled initial pipeline cache found at '{}'", initialPipelineCachePath.string()); + const char* basePath = SDL_GetBasePath(); + if (basePath != nullptr) { + sourcePathString = dusk::io::fs_path_to_string( + std::filesystem::path(basePath) / "initial_pipeline_cache.db"); + source = SDL_IOFromFile(sourcePathString.c_str(), "rb"); + } + if (source == nullptr) { + sourcePathString = "initial_pipeline_cache.db"; + source = SDL_IOFromFile(sourcePathString.c_str(), "rb"); + } + if (source == nullptr) { + DuskLog.info("No bundled initial pipeline cache found"); return; } @@ -521,18 +529,68 @@ static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) { std::filesystem::create_directories(configDir, ec); if (ec) { DuskLog.warn("Failed to create config directory '{}' for pipeline cache: {}", - configDir.string(), ec.message()); + dusk::io::fs_path_to_string(configDir), ec.message()); + SDL_CloseIO(source); return; } - std::filesystem::copy_file(initialPipelineCachePath, pipelineCachePath, std::filesystem::copy_options::none, ec); - if (ec) { - DuskLog.warn("Failed to seed pipeline cache from '{}' to '{}': {}", - initialPipelineCachePath.string(), pipelineCachePath.string(), ec.message()); + const auto pipelineCacheString = dusk::io::fs_path_to_string(pipelineCachePath); + SDL_IOStream* destination = SDL_IOFromFile(pipelineCacheString.c_str(), "wb"); + if (destination == nullptr) { + DuskLog.warn("Failed to open '{}' for seeded pipeline cache: {}", pipelineCacheString, + SDL_GetError()); + SDL_CloseIO(source); return; } - DuskLog.info("Seeded pipeline cache from '{}'", initialPipelineCachePath.string()); + bool copied = true; + std::array buffer{}; + while (true) { + const size_t bytesRead = SDL_ReadIO(source, buffer.data(), buffer.size()); + if (bytesRead > 0) { + size_t bytesWritten = 0; + while (bytesWritten < bytesRead) { + const size_t written = SDL_WriteIO( + destination, buffer.data() + bytesWritten, bytesRead - bytesWritten); + if (written == 0) { + DuskLog.warn("Failed to write seeded pipeline cache '{}': {}", + pipelineCacheString, SDL_GetError()); + copied = false; + break; + } + bytesWritten += written; + } + } + + if (!copied) { + break; + } + + if (bytesRead < buffer.size()) { + if (SDL_GetIOStatus(source) == SDL_IO_STATUS_EOF) { + break; + } + + DuskLog.warn( + "Failed to read bundled pipeline cache '{}': {}", sourcePathString, SDL_GetError()); + copied = false; + break; + } + } + + if (!SDL_CloseIO(destination)) { + DuskLog.warn("Failed to close seeded pipeline cache '{}': {}", + dusk::io::fs_path_to_string(pipelineCachePath), SDL_GetError()); + copied = false; + } + SDL_CloseIO(source); + + if (!copied) { + std::filesystem::remove(pipelineCachePath, ec); + return; + } + + DuskLog.info("Seeded pipeline cache from '{}'", sourcePathString); } static constexpr PADDefaultMapping defaultPadMapping = { From 4ec7b01213e28348f3c1c7bab78437b1a0ea5352 Mon Sep 17 00:00:00 2001 From: project516 <138796702+Project516@users.noreply.github.com> Date: Sun, 10 May 2026 11:39:55 -0500 Subject: [PATCH 47/53] update github actions in ci (#852) --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 82b387db4d..37a0849fd0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,7 +50,7 @@ jobs: libxss-dev libfuse2 libusb-1.0-0-dev libdecor-0-dev libpipewire-0.3-dev libunwind-dev - name: Setup sccache - uses: mozilla-actions/sccache-action@v0.0.9 + uses: mozilla-actions/sccache-action@v0.0.10 - name: Print sccache stats run: sccache --show-stats @@ -124,7 +124,7 @@ jobs: rustup target add x86_64-apple-darwin - name: Setup sccache - uses: mozilla-actions/sccache-action@v0.0.9 + uses: mozilla-actions/sccache-action@v0.0.10 - name: Configure CMake run: cmake --preset ${{matrix.preset}} @@ -175,7 +175,7 @@ jobs: java-version: 17 - name: Setup Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@v4 - name: Install Android SDK packages run: sdkmanager "platforms;android-36" "build-tools;36.1.0" "ndk;${ANDROID_NDK_VERSION}" @@ -186,7 +186,7 @@ jobs: rustup target add ${{matrix.rust_target}} - name: Setup sccache - uses: mozilla-actions/sccache-action@v0.0.9 + uses: mozilla-actions/sccache-action@v0.0.10 - name: Configure CMake run: cmake --preset ${{matrix.preset}} From 0a1fea4bc76c4a378a06d2e3ad6c301a0064c2c7 Mon Sep 17 00:00:00 2001 From: Markos Theocharis <87304130+Markos-Th09@users.noreply.github.com> Date: Sun, 10 May 2026 19:51:45 +0300 Subject: [PATCH 48/53] Add `LSSupportsGameMode` to enable game mode (#859) --- platforms/ios/Info.plist.in | 2 ++ platforms/macos/Info.plist.in | 2 ++ platforms/tvos/Info.plist.in | 2 ++ 3 files changed, 6 insertions(+) diff --git a/platforms/ios/Info.plist.in b/platforms/ios/Info.plist.in index 72174d487d..395b29b7d7 100644 --- a/platforms/ios/Info.plist.in +++ b/platforms/ios/Info.plist.in @@ -83,5 +83,7 @@ LSSupportsOpeningDocumentsInPlace + LSSupportsGameMode + diff --git a/platforms/macos/Info.plist.in b/platforms/macos/Info.plist.in index b7bc7b706c..aee7393ee8 100644 --- a/platforms/macos/Info.plist.in +++ b/platforms/macos/Info.plist.in @@ -28,5 +28,7 @@ ${MACOSX_BUNDLE_SHORT_VERSION_STRING} NSHighResolutionCapable + LSSupportsGameMode + diff --git a/platforms/tvos/Info.plist.in b/platforms/tvos/Info.plist.in index 841d120cc2..49ed85edd7 100644 --- a/platforms/tvos/Info.plist.in +++ b/platforms/tvos/Info.plist.in @@ -45,5 +45,7 @@ LaunchScreen UIUserInterfaceStyle Automatic + LSSupportsGameMode + From 80245387f3d7264d2a49a4afbcb122620242d0b1 Mon Sep 17 00:00:00 2001 From: Irastris Date: Sun, 10 May 2026 12:52:34 -0400 Subject: [PATCH 49/53] Update README & fallback ImGui error message (#857) --- README.md | 3 +++ src/dusk/imgui/ImGuiConsole.cpp | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5275e6bdc..54492e36d3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ It aims to be as accurate as possible to the original while also providing new o > [!IMPORTANT] > Dusk does *not* provide any copyrighted assets. You must provide your own copy of the original game. +> [!IMPORTANT] +> At a minimum, Dusk requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelyhood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them. + ### 1. Verify your dump First, make sure your dump of the game is clean and supported by Dusk. You can do this by checking the SHA-1 hash of your dump against this list of supported versions: diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 13f785cdee..dca2a88d79 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -313,7 +313,9 @@ namespace dusk { ImGui::PopFont(); } ImGui::PushFont(ImGuiEngine::fontLarge); - ImGuiTextCenter("Failed to initialize any graphics backend"); + ImGuiTextCenter("Failed to initialize any graphics backend."); + ImGuiTextCenter("\nYour system may be misconfigured, or your hardware may not support the required versions of any of the available backends."); + ImGuiTextCenter("\nA clean reinstall of Dusk may help. For further assistance, please visit #tech-support on the Twilit Realm Discord server."); const auto& style = ImGui::GetStyle(); const auto retrySize = ImGui::CalcTextSize("Retry (Auto backend)"); const auto quitSize = ImGui::CalcTextSize("Quit"); From 7f6212f9b7718f20e19911a1f079e564ae1dc1da Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sun, 10 May 2026 18:52:47 +0200 Subject: [PATCH 50/53] Add some basic code conventions to the README (#831) --- README.md | 2 +- docs/code-conventions.md | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 docs/code-conventions.md diff --git a/README.md b/README.md index 54492e36d3..652eae2538 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d If you'd like to build Dusk from source, please read the [build instructions](docs/building.md). -Pull requests are welcomed! Note that we do not accept contributions that are primarily AI-generated and will close your PR if we suspect as much. +Pull requests are welcomed! Note that we do not accept contributions that are primarily AI-generated and will close your PR if we suspect as much. Please also see the [code conventions](docs/code-conventions.md). # Credits diff --git a/docs/code-conventions.md b/docs/code-conventions.md new file mode 100644 index 0000000000..9c2659ac02 --- /dev/null +++ b/docs/code-conventions.md @@ -0,0 +1,13 @@ +# Code conventions for Dusk + +## Upstream when appropriate + +Bug fixes, documentation improvements, code cleanup, etc that also apply to the [original decompilation project](https://github.com/zeldaret/tp) should preferably be PR'd there. + +## Properly indicate Dusk-modified code + +When modifying original game code (i.e. in decomp) for Dusk's purposes, please clearly delineate such code as being Dusk-specific. Generally, this can be done by using `#if TARGET_PC` and keeping the original code in place. Use `#if AVOID_UB` for Undefined Behavior fixes to the original codebase. + +## Miscellaneous things + +* The original codebase makes heavy use of global `operator new` and similar overloads to allocate into a strict tree of heaps. This would cause many linkage headaches for us, so effectively all uses of `new` or `delete` in the original game code have been replaced with `JKR_NEW`, `JKR_DELETE`, or similar macros. See `JKRHeap.h` for the full list. From bfd9917ca1b4d521bca4c59b8b75609c6495edf6 Mon Sep 17 00:00:00 2001 From: Krutonium <3945538+Krutonium@users.noreply.github.com> Date: Sun, 10 May 2026 12:53:07 -0400 Subject: [PATCH 51/53] Fix Flake to Build Successfully (#791) * Fix Flake to Build Successfully * Fix typo; Res Folder is now correctly placed --- flake.nix | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/flake.nix b/flake.nix index cd703bda64..c8e38a849b 100644 --- a/flake.nix +++ b/flake.nix @@ -5,9 +5,73 @@ outputs = { self, nixpkgs }: let pkgs = import nixpkgs { system = "x86_64-linux"; }; + + # Dependencies that are not packaged in nixpkgs: + aurora-src = pkgs.fetchFromGitHub { + owner = "encounter"; + repo = "aurora"; + rev = "63606a43265a3bc18dafd500ab4d7a2108f109e6"; + hash = "sha256-xBvnAwGwNzav67Ac6oUz7RqDUwqgL2bsME3OOMn8Tqw="; + }; + dawn-src = pkgs.fetchzip { + url = "https://github.com/encounter/dawn-build/releases/download/v20260423.175430/dawn-linux-x86_64.tar.gz"; + hash = "sha256-HXfKTLHtMPwupnFnaflCARtXVPuS/0PoCePXidjE5xs="; + stripRoot = false; + }; + nod-src = pkgs.fetchzip { + url = "https://github.com/encounter/nod/releases/download/v2.0.0-alpha.8/libnod-linux-x86_64.tar.gz"; + hash = "sha256-mUqvLsbsqaZ+HAjMmHYPYO+MgtanGRTw7Gzn5uXR5rE="; + stripRoot = false; + }; + # The version of imgui on nixpkgs does not map cleanly. + imgui-src = pkgs.fetchFromGitHub { + owner = "ocornut"; + repo = "imgui"; + rev = "v1.91.9b-docking"; + hash = "sha256-mQOJ6jCN+7VopgZ61yzaCnt4R1QLrW7+47xxMhFRHLQ="; + }; + sqlite-src = pkgs.fetchzip { + url = "https://sqlite.org/2026/sqlite-amalgamation-3510300.zip"; + hash = "sha256-pNMR8zxaaqfAzQ0AQBOXMct4usdjey1Q0Gnitg06UhM="; + }; + rmlui-src = pkgs.fetchzip { + url = "https://github.com/mikke89/RmlUi/archive/f9b8c9e2935d5df2c7dff2c190d3968e99b0c3dc.tar.gz"; + hash = "sha256-g4O/JZUrrcseOz8o2QJRt+2CeuiLnVeuDJc906xvuIg="; + }; + # Dusk Actual dusk = pkgs.stdenv.mkDerivation { name = "dusk"; src = ./.; + postUnpack = '' + mkdir -p $sourceRoot/extern/aurora + cp -r ${aurora-src}/. $sourceRoot/extern/aurora/ + chmod -R u+w $sourceRoot/extern/aurora + sed -i '/add_subdirectory(tests)/d' $sourceRoot/extern/aurora/CMakeLists.txt + ''; + # Remove last line to re-enable tests + cmakeFlags = [ + "-DFETCHCONTENT_FULLY_DISCONNECTED=ON" + "-DFETCHCONTENT_SOURCE_DIR_CXXOPTS=${pkgs.cxxopts.src}" + "-DFETCHCONTENT_SOURCE_DIR_JSON=${pkgs.nlohmann_json.src}" + "-DFETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT=${dawn-src}" + "-DFETCHCONTENT_SOURCE_DIR_XXHASH=${pkgs.xxHash.src}" + "-DFETCHCONTENT_SOURCE_DIR_FMT=${pkgs.fmt.src}" + "-DFETCHCONTENT_SOURCE_DIR_TRACY=${pkgs.tracy.src}" + "-DAURORA_SDL3_PROVIDER=system" + "-DFETCHCONTENT_SOURCE_DIR_NOD_PREBUILT=${nod-src}" + "-DAURORA_NOD_PROVIDER=package" + "-DFETCHCONTENT_SOURCE_DIR_FREETYPE=${pkgs.freetype.src}" + "-DFETCHCONTENT_SOURCE_DIR_ZSTD=${pkgs.zstd.src}" + "-DFETCHCONTENT_SOURCE_DIR_SQLITE3=${sqlite-src}" + "-DFETCHCONTENT_SOURCE_DIR_IMGUI=${imgui-src}" + "-DFETCHCONTENT_SOURCE_DIR_RMLUI=${rmlui-src}" + "-DCMAKE_CROSSCOMPILING=ON" # Tests are not working as I didn't want to work through getting google's test suite working as well. This is the only guard I could find to disable it. + ]; + installPhase = '' + mkdir -p $out/bin + cp dusk $out/bin/dusk + cp -r ./res $out/bin/res + ''; nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config @@ -25,6 +89,13 @@ pkgs.libjpeg8 pkgs.libxkbcommon pkgs.libglvnd + pkgs.cxxopts + pkgs.abseil-cpp + pkgs.sdl3 + pkgs.fmt + pkgs.tracy + pkgs.freetype + pkgs.zstd ]; }; in { From 4bcf4ca35436f279f630c9af55a6c5a19b328049 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 10 May 2026 10:49:54 -0600 Subject: [PATCH 52/53] Improve build-appimage.sh --- ci/build-appimage.sh | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ci/build-appimage.sh b/ci/build-appimage.sh index 078d6cabf0..cef4ddfaf0 100755 --- a/ci/build-appimage.sh +++ b/ci/build-appimage.sh @@ -1,18 +1,26 @@ #!/bin/bash -ex -shopt -s extglob + +if [[ -n "${GITHUB_WORKSPACE:-}" ]]; then + cd "$GITHUB_WORKSPACE" +fi + +build_dir="$PWD/build" +linuxdeploy="$build_dir/linuxdeploy-$(uname -m).AppImage" # Get linuxdeploy -cd "$RUNNER_WORKSPACE" -curl -fOL https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$(uname -m).AppImage -chmod +x linuxdeploy-$(uname -m).AppImage +mkdir -p "$build_dir" +curl -fL "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$(uname -m).AppImage" -o "$linuxdeploy" +chmod +x "$linuxdeploy" # Build AppImage -cd "$GITHUB_WORKSPACE" mkdir -p build/appdir/usr/{bin,share/{applications,icons/hicolor}} -cp -r build/install/!(*.*) build/appdir/usr/bin +for install_path in build/install/*; do + [[ "$(basename "$install_path")" == *.* ]] && continue + cp -r "$install_path" build/appdir/usr/bin +done cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,1024x1024} build/appdir/usr/share/icons/hicolor cp platforms/freedesktop/dusk.desktop build/appdir/usr/share/applications cd build/install -VERSION="$DUSK_VERSION" NO_STRIP=1 "$RUNNER_WORKSPACE"/linuxdeploy-$(uname -m).AppImage \ - -l /usr/lib/x86_64-linux-gnu/libusb-1.0.so --appdir "$GITHUB_WORKSPACE"/build/appdir --output appimage +VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \ + -l /usr/lib/x86_64-linux-gnu/libusb-1.0.so --appdir "$build_dir/appdir" --output appimage From c948bffd3217ea57e4e47a24de7c7ae589c448b8 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 10 May 2026 10:55:30 -0600 Subject: [PATCH 53/53] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 22c2e5e55a..8444ecdc64 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 22c2e5e55a7e00404149dd499e2639cd74fb1189 +Subproject commit 8444ecdc6411a2094badd44ebcc0d6eb6f70a244