diff --git a/.gitea/workflows/updateApp.yml b/.gitea/workflows/updateApp.yml
new file mode 100644
index 000000000..0c4e7429b
--- /dev/null
+++ b/.gitea/workflows/updateApp.yml
@@ -0,0 +1,49 @@
+name: Update apps.json on new release
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ update:
+ runs-on: debian-trixie
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get install -y jq
+
+ - name: Extract release data
+ id: release
+ run: |
+ echo "VERSION=${GITEA_REF_NAME}" >> $GITHUB_OUTPUT
+ echo "DESCRIPTION=$(echo '${GITEA_EVENT_RELEASE_BODY}' | jq -Rs .)" >> $GITHUB_OUTPUT
+ echo "DATE=$(date '+%Y-%m-%d')" >> $GITHUB_OUTPUT
+ IPA_URL=$(echo '${GITEA_EVENT_RELEASE_ASSETS}' | jq -r '.[0].browser_download_url')
+ echo "DOWNLOAD_URL=$IPA_URL" >> $GITHUB_OUTPUT
+
+ - name: Update apps.json
+ run: |
+ jq --arg version "${{ steps.release.outputs.VERSION }}" \
+ --arg buildVersion "1" \
+ --arg date "${{ steps.release.outputs.DATE }}" \
+ --arg localizedDescription "${{ steps.release.outputs.DESCRIPTION }}" \
+ --arg downloadURL "${{ steps.release.outputs.DOWNLOAD_URL }}" \
+ '.apps[0].versions |= [{"version": $version, "buildVersion": $buildVersion, "date": $date, "localizedDescription": $localizedDescription, "downloadURL": $downloadURL, "minOSVersion": "15.0"}]' \
+ apps.json > tmp.json && mv tmp.json apps.json
+
+ - name: Commit and push
+ run: |
+ git config user.name "gitea-actions"
+ git config user.email "gitea-actions@localhost"
+ git add apps.json
+ git commit -m "Update apps.json for release ${{ steps.release.outputs.VERSION }}"
+ git push
+ env:
+ GIT_AUTHOR_NAME: gitea-actions
+ GIT_AUTHOR_EMAIL: gitea-actions@localhost
+ GIT_COMMITTER_NAME: gitea-actions
+ GIT_COMMITTER_EMAIL: gitea-actions@localhost
diff --git a/LICENSE.txt b/LICENSE.txt
index db65d8553..05c6078dd 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,3 +1,12 @@
+Currently licensed under the GNU AFFERO GENERAL PUBLIC LICENSE version 3, or any later version, at your choice.
+You may obtain a copy of the license at
+
+Copyright (c) Rhajune Park and contributors, 2025
+
+For copyright infringement claims, please contact abuse@pythonplayer123.dev for expedited processing
+
+Previously licensed under the MeloNX License.
+
MeloNX License
Copyright (c) MeloNX Team and Contributors
diff --git a/README.md b/README.md
index 31304d61f..640527850 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,10 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
- Recommended Device: iPhone 15 Pro or newer.
- Low-End Recommended Device: iPhone 13 Pro.
+## Discord Server
+
+We have a discord server!
+ - https://discord.gg/melonx
## How to install
@@ -48,7 +52,7 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
4. **Enable JIT**
- Use your preferred method to enable Just-In-Time (JIT) compilation.
- - We reccomend using [JitStreamer](https://jkcoxson.com/jitstreamer)
+ - We reccomend using [StikDebug](https://apps.apple.com/us/app/stikdebug/id6744045754)
5. **Add Necessary Files**
@@ -90,7 +94,7 @@ If having Issues installing firmware (Make sure your keys are installed first)
9. **Enable JIT**
- Use your preferred method to enable Just-In-Time (JIT) compilation.
- - We recommend using [JitStreamer](https://jkcoxson.com/jitstreamer)
+ - We recommend using [StikDebug](https://apps.apple.com/us/app/stikdebug/id6744045754)
### TrollStore
@@ -141,12 +145,12 @@ If having Issues installing firmware (Make sure your keys are installed first)
- **GPU**
- The GPU emulator emulates the Switch's Maxwell GPU using Metal (via MoltenVK) APIs through a custom build of OpenTK or Silk.NET respectively.
+ The GPU emulator emulates the Switch's Maxwell GPU using Metal (via MoltenVK) APIs through a custom build of Silk.NET.
- **Input**
- We currently have support for keyboard, touch input, JoyCon input support, and nearly all controllers.
- Motion controls are natively supported in most cases.
+ We currently have support for keyboard, touch input, JoyCon input support, and nearly all MFI controllers.
+ Motion controls are natively supported in most cases, however JoyCons do not have motion support doe to an iOS limitation.
- **DLC & Modifications**
@@ -157,14 +161,13 @@ If having Issues installing firmware (Make sure your keys are installed first)
The emulator has settings for enabling or disabling some logging, remapping controllers, and more.
-## License
+# License
-This software is licensed under the terms of the [MeloNX license (Based on MIT License)](LICENSE.txt).
+This software is licensed under the terms of the [MeloNX license](LICENSE.txt).
This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3.
See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY.md) for more details.
-## Credits
-
+# Credits
- [Ryujinx](https://github.com/ryujinx-mirror/ryujinx) is used for the base of this emulator. (link is to ryujinx-mirror since they were supportive)
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
diff --git a/distribution/ios/compile.sh b/distribution/ios/compile.sh
index 658f6210b..22aa34495 100755
--- a/distribution/ios/compile.sh
+++ b/distribution/ios/compile.sh
@@ -3,11 +3,13 @@
# Define the destination directory (hardcoded)
DESTINATION_DIR="src/MeloNX/Dependencies/Dynamic\ Libraries/Ryujinx.Headless.SDL2.dylib"
+dotnet clean
+
# Restore the project
dotnet restore
# Build the project with the specified version
-dotnet build -c Release
+# dotnet build -c Release
# Publish the project with the specified runtime and settings
dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
diff --git a/distribution/ios/get_dotnet.sh b/distribution/ios/get_dotnet.sh
index f69969682..c5306e183 100755
--- a/distribution/ios/get_dotnet.sh
+++ b/distribution/ios/get_dotnet.sh
@@ -1,8 +1,7 @@
#!/bin/bash
-XCCONFIG_FILE="${SRCROOT}/MeloNX.xcconfig"
+# XCCONFIG_FILE="${SRCROOT}/MeloNX.xcconfig"
-# Define the common paths to search for dotnet, including user-specific directories
SEARCH_PATHS=(
"/usr/local/share/dotnet"
"/usr/local/bin"
@@ -14,10 +13,10 @@ SEARCH_PATHS=(
"$HOME/Developer"
)
-# Initialize DOTNET_PATH as empty
+
+
DOTNET_PATH=""
-# Search in the defined paths
for path in "${SEARCH_PATHS[@]}"; do
if [ -d "$path" ]; then
DOTNET_PATH=$(find "$path" -name dotnet -type f -print -quit 2>/dev/null)
@@ -27,20 +26,8 @@ for path in "${SEARCH_PATHS[@]}"; do
fi
done
-# Check if the path was found
if [ -z "$DOTNET_PATH" ]; then
- echo "Error: dotnet path not found."
exit 1
fi
-echo "dotnet path: $DOTNET_PATH"
-
-# Escape the path for sed
-ESCAPED_PATH=$(echo "$DOTNET_PATH" | sed 's/\//\\\//g')
-
-# Update the xcconfig file
-sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE"
-
-$DOTNET_PATH clean
-
-echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH"
+echo "$DOTNET_PATH"
diff --git a/distribution/ios/xc-compile.sh b/distribution/ios/xc-compile.sh
new file mode 100755
index 000000000..3075b8e94
--- /dev/null
+++ b/distribution/ios/xc-compile.sh
@@ -0,0 +1,21 @@
+dotnet_output=$(./distribution/ios/get_dotnet.sh)
+exit_code=$?
+
+if [ $exit_code -eq 0 ]; then
+ dotnet="$dotnet_output"
+else
+ echo "error: .NET not found, Please follow the compilation instructions on the gitea." >&2
+ exit 1
+fi
+
+$dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
+
+if [ $? -ne 0 ]; then
+ echo "warning: Compiling MeloNX failed! Running dotnet clean + restore then Retrying..."
+
+ $dotnet clean
+
+ $dotnet restore
+
+ $dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
+fi
diff --git a/source.json b/source.json
new file mode 100644
index 000000000..1b6967bb7
--- /dev/null
+++ b/source.json
@@ -0,0 +1,49 @@
+{
+ "name": "MeloNX",
+ "subtitle": "A source for the MeloNX Application",
+ "description": "Welcome to the MeloNX source! The latest download for MeloNX.",
+ "iconURL": "https://git.743378673.xyz/CycloKid/assets/media/branch/main/Melo/AppIcons/MeloNX.png",
+ "headerURL": "https://cdn.discordapp.com/attachments/1320760161836466257/1331670540447912090/melon-x-not-melo-nx-amiright-guys.png?ex=67f556d6&is=67f40556&hm=71be8f109a14f1c47d8f4965aa017bccb5617962b7a9f5cdfb936a5a8135dad7&",
+ "website": "https://MeloNX.org",
+ "tintColor": "#AE34EB",
+ "featuredApps": [
+ "com.stossy11.MeloNX"
+ ],
+ "apps": [
+ {
+ "name": "MeloNX",
+ "bundleIdentifier": "com.stossy11.MeloNX",
+ "developerName": "Stossy11",
+ "subtitle": "An NX Emulator.",
+ "localizedDescription": "MeloNX is an iOS Nintendo Switch emulator based on Ryujinx, written primarily in C#. Designed to bring accurate performance and a user-friendly interface to iOS, MeloNX makes Switch games accessible on Apple devices. Developed from the ground up, MeloNX is open-source and available on a custom Gitea server under the MeloNX license (Based on MIT) (requires increased memory limit)",
+ "iconURL": "https://git.743378673.xyz/CycloKid/assets/media/branch/main/Melo/AppIcons/MeloNX.png",
+ "tintColor": "#AE34EB",
+ "category": "games",
+ "screenshots": [
+ "https://git.743378673.xyz/stossy11/screenshots/raw/branch/main/IMG_0380.PNG",
+ "https://git.743378673.xyz/stossy11/screenshots/raw/branch/main/IMG_0381.PNG"
+ ],
+ "versions": [
+ {
+ "version": "1.7.0",
+ "buildVersion": "1",
+ "date": "2025-04-08",
+ "localizedDescription": "First AltStore release!",
+ "downloadURL": "https://git.743378673.xyz/MeloNX/MeloNX/releases/download/1.7.0/MeloNX.ipa",
+ "size": 79821,
+ "minOSVersion": "15.0"
+ }
+ ],
+ "appPermissions": {
+ "entitlements": [
+ "get-task-allow",
+ "com.apple.developer.kernel.increased-memory-limit"
+ ],
+ "privacy": {
+ "NSPhotoLibraryAddUsageDescription": "MeloNX needs access to your Photo Library in order to save screenshots."
+ }
+ }
+ }
+ ],
+ "news": []
+}
diff --git a/src/MeloNX/MeloNX.xcconfig b/src/MeloNX/MeloNX.xcconfig
index 9b97df59a..2adb67484 100644
--- a/src/MeloNX/MeloNX.xcconfig
+++ b/src/MeloNX/MeloNX.xcconfig
@@ -8,6 +8,4 @@
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
-VERSION = 1.7.0
-
-DOTNET = /usr/local/share/dotnet/dotnet
+VERSION = 2.0.1
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
index e4099b69e..b4c4648c6 100644
--- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
+++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
@@ -24,7 +24,7 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
- 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
+ 4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */ = {isa = PBXBuildFile; productRef = 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */; };
4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; };
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
@@ -32,13 +32,6 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
- 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = BD43C6212D1B248D003BBC42;
- remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
- };
4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@@ -53,6 +46,13 @@
remoteGlobalIDString = 4E80A98C2CD6F54500029585;
remoteInfo = MeloNX;
};
+ 4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = BD43C6212D1B248D003BBC42;
+ remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
+ };
BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@@ -120,6 +120,10 @@
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
+ "Dependencies/Dynamic Libraries/StosJIT.framework" = (
+ CodeSignOnCopy,
+ RemoveHeadersOnCopy,
+ );
"Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (
CodeSignOnCopy,
);
@@ -129,10 +133,6 @@
"Dependencies/Dynamic Libraries/libavutil.dylib" = (
CodeSignOnCopy,
);
- Dependencies/XCFrameworks/MoltenVK.xcframework = (
- CodeSignOnCopy,
- RemoveHeadersOnCopy,
- );
Dependencies/XCFrameworks/SDL2.xcframework = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
@@ -178,6 +178,7 @@
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
"Dependencies/Dynamic Libraries/RyujinxHelper.framework",
+ "Dependencies/Dynamic Libraries/StosJIT.framework",
Dependencies/XCFrameworks/libavcodec.xcframework,
Dependencies/XCFrameworks/libavfilter.xcframework,
Dependencies/XCFrameworks/libavformat.xcframework,
@@ -186,7 +187,6 @@
Dependencies/XCFrameworks/libswresample.xcframework,
Dependencies/XCFrameworks/libswscale.xcframework,
Dependencies/XCFrameworks/libteakra.xcframework,
- Dependencies/XCFrameworks/MoltenVK.xcframework,
Dependencies/XCFrameworks/SDL2.xcframework,
);
};
@@ -203,8 +203,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
+ 4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
);
@@ -264,12 +264,12 @@
/* Begin PBXLegacyTarget section */
BD43C61D2D1B23AB003BBC42 /* Ryujinx */ = {
isa = PBXLegacyTarget;
- buildArgumentsString = "publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true";
+ buildArgumentsString = "./distribution/ios/xc-compile.sh";
buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */;
buildPhases = (
);
- buildToolPath = "$(DOTNET)";
- buildWorkingDirectory = "$(SRCROOT)/../..";
+ buildToolPath = /bin/sh;
+ buildWorkingDirectory = "$(SRCROOT)/../../";
dependencies = (
);
name = Ryujinx;
@@ -294,15 +294,15 @@
buildRules = (
);
dependencies = (
- 4E2953AC2D803BC9000497CD /* PBXTargetDependency */,
+ 4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
4E80A98F2CD6F54500029585 /* MeloNX */,
);
name = MeloNX;
packageProductDependencies = (
- 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
+ 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */,
);
productName = MeloNX;
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
@@ -393,8 +393,8 @@
mainGroup = 4E80A9842CD6F54500029585;
minimizedProjectReferenceProxies = 1;
packageReferences = (
- 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
+ 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */,
);
preferredProjectObjectVersion = 56;
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
@@ -482,12 +482,6 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
- 4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- platformFilter = ios;
- target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
- targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */;
- };
4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
@@ -498,6 +492,11 @@
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
};
+ 4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
+ targetProxy = 4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */;
+ };
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
@@ -571,6 +570,7 @@
ONLY_ACTIVE_ARCH = NO;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
@@ -637,6 +637,7 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_DISABLE_SAFETY_CHECKS = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
VALIDATE_PRODUCT = YES;
};
@@ -646,13 +647,16 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
+ ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
+ EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -721,6 +725,72 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
@@ -733,10 +803,10 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
- INFOPLIST_KEY_UIRequiresFullScreen = YES;
+ INFOPLIST_KEY_UIRequiresFullScreen = NO;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -863,6 +933,119 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
@@ -880,13 +1063,16 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
+ ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
+ EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
@@ -955,6 +1141,72 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
@@ -967,10 +1219,10 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
- INFOPLIST_KEY_UIRequiresFullScreen = YES;
+ INFOPLIST_KEY_UIRequiresFullScreen = NO;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1097,6 +1349,119 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
@@ -1298,12 +1663,12 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
- 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
+ 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */ = {
isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
+ repositoryURL = "https://github.com/robbiehanson/CocoaAsyncSocket";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 1.5.0;
+ minimumVersion = 7.6.5;
};
};
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
@@ -1317,10 +1682,10 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
- 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
+ 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */ = {
isa = XCSwiftPackageProductDependency;
- package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;
- productName = SwiftUIJoystick;
+ package = 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */;
+ productName = CocoaAsyncSocket;
};
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
isa = XCSwiftPackageProductDependency;
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index af8dd513e..cb3a468df 100644
--- a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,6 +1,15 @@
{
- "originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b",
+ "originHash" : "b4a593815773c4e9eedb98cabe88f41620776314bffb6c39d5a41cb743e4d390",
"pins" : [
+ {
+ "identity" : "cocoaasyncsocket",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/robbiehanson/CocoaAsyncSocket",
+ "state" : {
+ "revision" : "dbdc00669c1ced63b27c3c5f052ee4d28f10150c",
+ "version" : "7.6.5"
+ }
+ },
{
"identity" : "swiftsvg",
"kind" : "remoteSourceControl",
@@ -9,15 +18,6 @@
"branch" : "master",
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
}
- },
- {
- "identity" : "swiftuijoystick",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/michael94ellis/SwiftUIJoystick",
- "state" : {
- "revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65",
- "version" : "1.5.0"
- }
}
],
"version" : 3
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index d95c76d65..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/IDEFindNavigatorScopes.plist b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/IDEFindNavigatorScopes.plist
deleted file mode 100644
index 5dd5da85f..000000000
--- a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/IDEFindNavigatorScopes.plist
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index 77a8ba399..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/brandon.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/june.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/june.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index ebc8d0112..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/june.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/ls.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/ls.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index d2a61f079..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/ls.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate
index 264afbacc..b4c024fa9 100644
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/techguy.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/techguy.xcuserdatad/UserInterfaceState.xcuserstate
deleted file mode 100644
index f234e2cfe..000000000
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/techguy.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme
index 131fd924a..f79f4ed54 100644
--- a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme
+++ b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme
@@ -1,7 +1,7 @@
+ version = "2.0">
+ structuredConsoleMode = "2"
+ disablePerformanceAntipatternChecker = "YES">
CFTypeRef?
+@_silgen_name("SecTaskCopyTeamIdentifier")
+func SecTaskCopyTeamIdentifier(
+ _ task: SecTaskRef,
+ _ error: NSErrorPointer
+) -> NSString?
+
@_silgen_name("SecTaskCreateFromSelf")
func SecTaskCreateFromSelf(
_ allocator: CFAllocator?
) -> SecTaskRef?
+@_silgen_name("CFRelease")
+func CFRelease(_ cf: CFTypeRef)
+
@_silgen_name("SecTaskCopyValuesForEntitlements")
func SecTaskCopyValuesForEntitlements(
_ task: SecTaskRef,
@@ -29,30 +38,43 @@ func SecTaskCopyValuesForEntitlements(
_ error: UnsafeMutablePointer?>?
) -> CFDictionary?
+func releaseSecTask(_ task: SecTaskRef) {
+ let cf = unsafeBitCast(task, to: CFTypeRef.self)
+ CFRelease(cf)
+}
+
func checkAppEntitlements(_ ents: [String]) -> [String: Any] {
guard let task = SecTaskCreateFromSelf(nil) else {
- print("Failed to create SecTask")
return [:]
}
-
+ defer {
+ releaseSecTask(task)
+ }
+
guard let entitlements = SecTaskCopyValuesForEntitlements(task, ents as CFArray, nil) else {
- print("Failed to get entitlements")
return [:]
}
-
- return (entitlements as? [String: Any]) ?? [:]
+
+ return (entitlements as NSDictionary) as? [String: Any] ?? [:]
}
func checkAppEntitlement(_ ent: String) -> Bool {
guard let task = SecTaskCreateFromSelf(nil) else {
- print("Failed to create SecTask")
return false
}
-
- guard let entitlements = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
- print("Failed to get entitlements")
+ defer {
+ releaseSecTask(task)
+ }
+
+ guard let entitlement = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
return false
}
-
- return entitlements.boolValue != nil && entitlements.boolValue
+
+ if let number = entitlement as? NSNumber {
+ return number.boolValue
+ } else if let bool = entitlement as? Bool {
+ return bool
+ }
+
+ return false
}
diff --git a/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h b/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h
index 847a0691d..1a1b194f1 100644
--- a/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h
+++ b/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h
@@ -14,11 +14,14 @@
#include
#include
+#include
+
#ifdef __cplusplus
extern "C" {
#endif
+
struct GameInfo {
long FileSize;
char TitleName[512];
@@ -40,6 +43,10 @@ struct DlcNcaList {
struct DlcNcaListItem* items;
};
+typedef void (^SwiftCallback)(NSString *result);
+
+void RegisterCallback(NSString *identifier, SwiftCallback callback);
+
extern struct GameInfo get_game_info(int, char*);
extern struct DlcNcaList get_dlc_nca_list(const char* titleIdPtr, const char* pathPtr);
@@ -50,12 +57,16 @@ char* installed_firmware_version();
void set_native_window(void *layerPtr);
+void pause_emulation(bool shouldPause);
+
void stop_emulation();
void initialize();
int main_ryujinx_sdl(int argc, char **argv);
+int update_settings_external(int argc, char **argv);
+
int get_current_fps();
void touch_began(float x, float y, int index);
diff --git a/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift b/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift
index 44fbf1a72..fd5b7c68e 100644
--- a/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift
+++ b/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift
@@ -20,6 +20,14 @@ func isJITEnabled() -> Bool {
return csops(pid: getpid(), ops: 0, useraddr: &flags, usersize: Int32(MemoryLayout.size(ofValue: flags))) == 0 && (flags & Int(CS_DEBUGGED)) != 0 ? allocateTest() : false
}
+func checkDebugged() -> Bool {
+ var flags: Int = 0
+ if checkAppEntitlement("dynamic-codesigning") {
+ return true
+ }
+ return csops(pid: getpid(), ops: 0, useraddr: &flags, usersize: Int32(MemoryLayout.size(ofValue: flags))) == 0 && (flags & Int(CS_DEBUGGED)) != 0
+}
+
func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
var region: vm_address_t = vm_address_t(UInt(bitPattern: address))
var regionSize: vm_size_t = 0
@@ -34,7 +42,7 @@ func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
}
if result != KERN_SUCCESS {
- print("Failed to reach \(address)")
+ // print("Failed to reach \(address)")
return false
}
diff --git a/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift b/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift
index 93d8b0b0f..d1eecfda4 100644
--- a/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift
+++ b/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift
@@ -23,7 +23,7 @@ func enableJITEB() {
func enableJITEBRequest() {
let pid = Int(getpid())
- print(pid)
+ // print(pid)
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
var request = URLRequest(url: address)
@@ -90,7 +90,7 @@ func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping
let task = session.dataTask(with: request) { _, response, error in
if let error = error {
- print("Ping failed: \(error.localizedDescription)")
+ // print("Ping failed: \(error.localizedDescription)")
completion(false)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
completion(true)
@@ -118,6 +118,7 @@ func presentAlert(title: String, message: String, completion: (() -> Void)? = ni
}
}
+
struct LaunchApp: Codable {
let success: Bool
let message: String
@@ -140,12 +141,12 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
viewController.present(alert, animated: true)
}
} else {
- print("Hopefully JIT is enabled now...")
+ // print("Hopefully JIT is enabled now...")
Ryujinx.shared.ryuIsJITEnabled()
}
} catch {
- print(String(data: jsonData, encoding: .utf8))
+ // print(String(data: jsonData, encoding: .utf8))
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
diff --git a/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift b/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift
new file mode 100644
index 000000000..2cde8343d
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift
@@ -0,0 +1,74 @@
+//
+// EnableJIT.swift
+// MeloNX
+//
+// Created by Stossy11 on 10/02/2025.
+//
+
+import Foundation
+import Network
+import UIKit
+
+func stikJITorStikDebug() -> Int {
+ let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil)
+
+ if checkifappinstalled("com.stik.sj") {
+ return 1 // StikDebug
+ }
+
+ if checkifappinstalled("com.stik.sj.\(String(teamid ?? ""))") {
+ return 2 // StikJIT
+ }
+
+ return 0 // Not Found
+}
+
+func checkforOld() -> Bool {
+ let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil)
+
+ if checkifappinstalled(changeAppUI("Y29tLnN0b3NzeTExLlBvbWVsbw==") ?? "") {
+ return true
+ }
+
+ if checkifappinstalled(changeAppUI("Y29tLnN0b3NzeTExLlBvbWVsbw==") ?? "" + ".\(String(teamid ?? ""))") {
+ return true
+ }
+
+ if checkifappinstalled((Bundle.main.bundleIdentifier ?? "").replacingOccurrences(of: "MeloNX", with: changeAppUI("UG9tZWxv") ?? "")) {
+ return true
+ }
+
+ return false
+}
+
+
+func checkifappinstalled(_ id: String) -> Bool {
+ guard let handle = dlopen("/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices", RTLD_LAZY) else {
+ return false
+ }
+
+ typealias SBSLaunchApplicationWithIdentifierFunc = @convention(c) (CFString, Bool) -> Int32
+ guard let sym = dlsym(handle, "SBSLaunchApplicationWithIdentifier") else {
+ if let error = dlerror() {
+ print(String(cString: error))
+ }
+ dlclose(handle)
+ return false
+ }
+
+ let bundleID: CFString = id as CFString
+ let suspended: Bool = false
+
+
+ let SBSLaunchApplicationWithIdentifier = unsafeBitCast(sym, to: SBSLaunchApplicationWithIdentifierFunc.self)
+ let result = SBSLaunchApplicationWithIdentifier(bundleID, suspended)
+
+ return result == 9
+}
+
+func enableJITStik() {
+ let urlScheme = "stikjit://enable-jit?bundle-id=\(Bundle.main.bundleIdentifier ?? "wow")"
+ if let launchURL = URL(string: urlScheme), !isJITEnabled() {
+ UIApplication.shared.open(launchURL, options: [:], completionHandler: nil)
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/BaseController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/BaseController.swift
new file mode 100644
index 000000000..34bbb79a5
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/BaseController.swift
@@ -0,0 +1,14 @@
+//
+// BaseController.swift
+// MeloNX
+//
+// Created by MediaMoots on 5/17/2025.
+//
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Base Controller Protocol
+
+/// Base Controller with motion related functions
+protocol BaseController: AnyObject {
+ func tryRegisterMotion(slot: UInt8)
+ func tryGetMotionProvider() -> DSUMotionProvider?
+}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUMotionProviders.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUMotionProviders.swift
new file mode 100644
index 000000000..601115809
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUMotionProviders.swift
@@ -0,0 +1,178 @@
+//
+// DSUMotionProviders.swift
+//
+// Multi-source Cemuhook-compatible DSU server.
+// Created by MediaMoots on 5/17/2025.
+//
+//
+
+import CoreMotion
+import GameController // GCController
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Providers
+
+/// iPhone / iPad IMU
+final class DeviceMotionProvider: DSUMotionProvider {
+
+ // ───── DSUMotionProvider conformance
+ let slot: UInt8
+ let mac: [UInt8] = [0xAB,0x12,0xCD,0x34,0xEF,0x56]
+ let connectionType: UInt8 = 2
+ let batteryLevel: UInt8 = 5
+ let motionRate: Double = 60.0 // 60 Hz
+
+ // ───── Internals
+ private let mm = CMMotionManager()
+
+ // Thread Safety
+ private let dataLock = NSLock()
+ private var _latest: CMDeviceMotion?
+ private var latest: CMDeviceMotion? {
+ get { dataLock.lock(); defer { dataLock.unlock() }; return _latest }
+ set { dataLock.lock(); _latest = newValue; dataLock.unlock() }
+ }
+
+ private var orientation: UIDeviceOrientation =
+ UIDevice.current.orientation == .unknown ? .landscapeLeft : UIDevice.current.orientation
+
+ init(slot: UInt8) {
+ precondition(slot < 8, "DSU only supports slots 0…7")
+ self.slot = slot
+
+ // ── start Core Motion
+ mm.deviceMotionUpdateInterval = 1.0 / motionRate
+ mm.startDeviceMotionUpdates(to: .main) { [weak self] m, _ in
+ guard let self = self, let m = m else { return }
+ self.latest = m
+ if let sample = self.nextSample() {
+ DSUServer.shared.pushSample(sample, from: self)
+ }
+ }
+
+ // ── track orientation changes (ignore flat)
+ UIDevice.current.beginGeneratingDeviceOrientationNotifications()
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(orientationDidChange),
+ name: UIDevice.orientationDidChangeNotification,
+ object: nil
+ )
+ }
+
+ @objc private func orientationDidChange() {
+ let o = UIDevice.current.orientation
+ if o.isFlat { return } // ignore face-up / face-down
+ orientation = o
+ }
+
+ func nextSample() -> DSUMotionSample? {
+ guard let m = latest else { return nil }
+
+ // Raw values
+ let gx = Float(m.rotationRate.x)
+ let gy = Float(m.rotationRate.y)
+ let gz = Float(m.rotationRate.z)
+ let ax = Float(m.gravity.x + m.userAcceleration.x)
+ let ay = Float(m.gravity.y + m.userAcceleration.y)
+ let az = Float(m.gravity.z + m.userAcceleration.z)
+
+ // Rotate axes to match Cemuhook's "landscape-left as neutral" convention
+ let a: SIMD3
+ let g: SIMD3
+
+ switch orientation {
+ case .portrait:
+ a = SIMD3( ax, az, -ay)
+ g = SIMD3( gx, -gz, gy)
+ case .landscapeRight:
+ a = SIMD3( ay, az, ax)
+ g = SIMD3( gy, -gz, -gx)
+ case .portraitUpsideDown:
+ a = SIMD3( -ax, az, ay)
+ g = SIMD3( -gx, -gz, -gy)
+ case .landscapeLeft, .unknown, .faceUp, .faceDown:
+ a = SIMD3( -ay, az, -ax)
+ g = SIMD3( -gy, -gz, gx)
+ @unknown default:
+ return nil
+ }
+
+ // Convert gyro rad/s → °/s here so the server doesn't have to.
+ let gDeg = g * (180 / .pi)
+
+ return DSUMotionSample(timestampUS: currentUS(),
+ accel: a,
+ gyroDeg: gDeg)
+ }
+}
+
+// Any Switch Pro / DualSense controller that exposes `GCMotion`
+final class ControllerMotionProvider: DSUMotionProvider {
+
+ // DSUMotionProvider
+ let slot: UInt8
+ let mac: [UInt8] = [0xAB,0x12,0xCD,0x34,0xEF,0x56]
+ let connectionType: UInt8 = 2
+ var batteryLevel: UInt8 {
+ UInt8((pad.battery?.batteryLevel ?? 0.3) * 5).clamped(to: 0...5)
+ }
+
+ private let pad: GCController
+
+ // Thread Safety
+ private let dataLock = NSLock()
+ private var _latest: GCMotion?
+ private var latest: GCMotion? {
+ get { dataLock.lock(); defer { dataLock.unlock() }; return _latest }
+ set { dataLock.lock(); _latest = newValue; dataLock.unlock() }
+ }
+
+ init(controller: GCController, slot: UInt8) {
+ self.pad = controller
+ self.slot = slot
+ pad.motion?.sensorsActive = true
+ pad.motion?.valueChangedHandler = { [weak self] motion in
+ guard let self = self else { return }
+ self.latest = motion
+ if let sample = self.nextSample() {
+ DSUServer.shared.pushSample(sample, from: self)
+ }
+ }
+ }
+
+ func nextSample() -> DSUMotionSample? {
+ guard let m = latest else { return nil }
+
+ // Extract and convert acceleration to SIMD3
+ let a = SIMD3(
+ Float(m.acceleration.x),
+ Float(m.acceleration.z),
+ -Float(m.acceleration.y)
+ )
+
+ // Extract, transform, and convert rotation rate to SIMD3 (in radians/s)
+ let g = SIMD3(
+ Float(m.rotationRate.x),
+ -Float(m.rotationRate.z),
+ Float(m.rotationRate.y)
+ )
+
+ // Convert gyro rotation rate from rad/s to degrees/s
+ let gDeg = g * (180 / .pi)
+
+ return DSUMotionSample(
+ timestampUS: currentUS(),
+ accel: a,
+ gyroDeg: gDeg
+ )
+ }
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Helper funcs / ext
+
+private func uint64US(_ time: TimeInterval) -> UInt64 { UInt64(time * 1_000_000) }
+private func currentUS() -> UInt64 { uint64US(CACurrentMediaTime()) }
+
+private extension Comparable {
+ func clamped(to r: ClosedRange) -> Self { min(max(self, r.lowerBound), r.upperBound) }
+}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUServer.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUServer.swift
new file mode 100644
index 000000000..9947fae5e
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Motion/DSUServer.swift
@@ -0,0 +1,217 @@
+//
+// DSUServer.swift
+//
+// Multi-source Cemuhook-compatible DSU server.
+// Created by MediaMoots on 5/17/2025.
+//
+//
+
+import Foundation
+import CocoaAsyncSocket // GCDAsyncUdpSocket
+import zlib // CRC-32
+
+//──────────────────────────────────────────────────────────────────────── MARK:- DSU Motion protocol
+
+/// One motion source == one DSU *slot* (0-7).
+protocol DSUMotionProvider: AnyObject {
+ var slot: UInt8 { get } // unique, 0-7
+ var mac: [UInt8] { get } // 6-byte ID
+ var connectionType: UInt8 { get } // 0 = USB, 2 = BT
+ var batteryLevel: UInt8 { get } // 0-5 (Cemuhook)
+
+ func nextSample() -> DSUMotionSample?
+}
+
+/// Raw motion payload returned by providers.
+struct DSUMotionSample {
+ var timestampUS: UInt64 // µs
+ var accel: SIMD3 // G's
+ var gyroDeg: SIMD3 // °/s
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Server constants
+
+private enum C {
+ static let port: UInt16 = 26_760
+ static let protocolVersion: UInt16 = 1_001
+ static let headerMagic = "DSUS"
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Server core
+
+final class DSUServer: NSObject {
+
+ // Singleton for convenience
+ static let shared = DSUServer()
+ private override init() {
+ serverID = UInt32.random(in: .min ... .max)
+ super.init()
+ configureSocket()
+ }
+
+ // MARK: Public API ─────────────────────────────────────────────
+ func register(_ provider: DSUMotionProvider) { providers[provider.slot] = provider }
+ func unregister(slot: UInt8) { providers.removeValue(forKey: slot) }
+
+ /// 🔸 providers push fresh samples here.
+ func pushSample(_ sample: DSUMotionSample, from provider: DSUMotionProvider) {
+ guard let addr = lastClientAddress else { return } // no subscriber → drop
+ sendPadData(sample: sample, from: provider, to: addr)
+ }
+
+ // MARK: Private
+ private let serverID: UInt32
+ private var socket: GCDAsyncUdpSocket?
+ private var lastClientAddress: Data?
+
+ private var providers = [UInt8 : DSUMotionProvider]() // slot→provider
+ private var packetNumber = [UInt8 : UInt32]() // per-slot counter
+
+ // ───────── UDP setup
+ private func configureSocket() {
+ socket = GCDAsyncUdpSocket(delegate: self, delegateQueue: .main)
+ do {
+ try socket?.bind(toPort: C.port)
+ try socket?.beginReceiving()
+ //print("🟢 DSU server listening on UDP \(C.port)")
+ } catch {
+ //print("❌ DSU socket error:", error)
+ }
+ }
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- UDP delegate
+
+extension DSUServer: GCDAsyncUdpSocketDelegate {
+
+ func udpSocket(_ sock: GCDAsyncUdpSocket,
+ didReceive data: Data,
+ fromAddress addr: Data,
+ withFilterContext ctx: Any?) {
+
+ lastClientAddress = addr
+
+ // Light validation
+ guard data.count >= 20,
+ String(decoding: data[0..<4], as: UTF8.self) == C.headerMagic,
+ data.readUInt16LE(at: 4) == C.protocolVersion
+ else { return }
+
+ let type = data.readUInt32LE(at: 16)
+ switch type {
+ case 0x100001: sendPortInfo(to: addr) // client asks for port list
+ case 0x100002: break // subscription acknowledged
+ default: break
+ }
+ }
+
+ func udpSocketDidClose(_ sock: GCDAsyncUdpSocket, withError err: Error?) {
+ //print("UDP closed:", err?.localizedDescription ?? "nil")
+ lastClientAddress = nil
+ }
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Packet helpers
+
+private extension DSUServer {
+
+ // ── Header (16 bytes)
+ func appendHeader(into d: inout Data, payloadSize: UInt16) {
+ d.append(C.headerMagic.data(using: .utf8)!) // "DSUS"
+ d.append(C.protocolVersion.leData) // Protocol Version
+ d.append(payloadSize.leData) // Payload Size
+ d.append(Data(repeating: 0, count: 4)) // CRC-stub
+ d.append(serverID.leData) // Server ID
+ }
+ func patchCRC32(of packet: inout Data) {
+ let crc = packet.withUnsafeBytes { ptr in
+ crc32(0, ptr.baseAddress, uInt(packet.count))
+ }.littleEndian
+ let crcLE = UInt32(crc).littleEndian
+ let crcData = withUnsafeBytes(of: crcLE) { Data($0) }
+ packet.replaceSubrange(8..<12, with: crcData)
+ }
+
+ // ── 0x100001 DSUSPortInfo
+ func sendPortInfo(to addr: Data) {
+ for p in providers.values {
+ var pkt = Data()
+ appendHeader(into: &pkt, payloadSize: 12)
+ pkt.append(UInt32(0x100001).leData)
+
+ pkt.append(p.slot)
+ pkt.append(UInt8(2)) // connected
+ pkt.append(UInt8(2)) // full gyro
+ pkt.append(p.connectionType)
+ pkt.append(p.mac, count: 6)
+ pkt.append(p.batteryLevel)
+ pkt.append(UInt8(0)) // padding
+
+ patchCRC32(of: &pkt)
+ socket?.send(pkt, toAddress: addr, withTimeout: -1, tag: 0)
+ }
+ }
+
+ // ── 0x100002 DSUSPadDataRsp
+ func sendPadData(sample s: DSUMotionSample,
+ from p: DSUMotionProvider,
+ to addr: Data) {
+
+ var pkt = Data()
+ appendHeader(into: &pkt, payloadSize: 84)
+ pkt.append(UInt32(0x100002).leData)
+
+ pkt.append(p.slot)
+ pkt.append(UInt8(2)) // connected
+ pkt.append(UInt8(2)) // full gyro
+ pkt.append(p.connectionType)
+ pkt.append(p.mac, count: 6)
+ pkt.append(p.batteryLevel)
+ pkt.append(UInt8(1)) // is connected
+
+ let num = packetNumber[p.slot, default: 0]
+ pkt.append(num.leData)
+ packetNumber[p.slot] = num &+ 1
+
+ pkt.append(UInt16(0).leData) // buttons
+ pkt.append(contentsOf: [0,0]) // HOME / Touch
+ pkt.append(contentsOf: [128,128,128,128]) // sticks
+ pkt.append(Data(repeating: 0, count: 12)) // d-pad / face / trig
+ pkt.append(Data(repeating: 0, count: 12)) // touch 1 & 2
+ pkt.append(s.timestampUS.leData)
+
+ pkt.append(s.accel.x.leData)
+ pkt.append(s.accel.y.leData)
+ pkt.append(s.accel.z.leData)
+
+ pkt.append(s.gyroDeg.x.leData)
+ pkt.append(s.gyroDeg.y.leData)
+ pkt.append(s.gyroDeg.z.leData)
+
+ patchCRC32(of: &pkt)
+ socket?.send(pkt, toAddress: addr, withTimeout: -1, tag: 0)
+ }
+}
+
+//──────────────────────────────────────────────────────────────────────── MARK:- Helper funcs / ext
+
+private extension FixedWidthInteger {
+ var leData: Data {
+ var v = self.littleEndian
+ return Data(bytes: &v, count: MemoryLayout.size)
+ }
+}
+private extension Float {
+ var leData: Data {
+ var v = self
+ return Data(bytes: &v, count: MemoryLayout.size)
+ }
+}
+private extension Data {
+ func readUInt16LE(at offset: Int) -> UInt16 {
+ self[offset.. UInt32 {
+ self[offset.. DSUMotionProvider? { return controllerMotionProvider }
private func setupHandheldController() {
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
@@ -49,50 +82,51 @@ class NativeController: Hashable {
// Update joystick state here
},
SetPlayerIndex: { userdata, playerIndex in
- print("Player index set to \(playerIndex)")
+ // print("Player index set to \(playerIndex)")
+ guard let userdata, let player = GCControllerPlayerIndex(rawValue: Int(playerIndex)) else { return }
+ let _self = Unmanaged.fromOpaque(userdata).takeUnretainedValue()
+ _self.nativeController.playerIndex = player
},
Rumble: { userdata, lowFreq, highFreq in
- print("Rumble with \(lowFreq), \(highFreq)")
guard let userdata else { return 0 }
let _self = Unmanaged.fromOpaque(userdata).takeUnretainedValue()
- VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics)
+ _self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
return 0
},
RumbleTriggers: { userdata, leftRumble, rightRumble in
- print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0
},
SetLED: { userdata, red, green, blue in
- print("Set LED to RGB(\(red), \(green), \(blue))")
+ guard let userdata else { return 0 }
+ let _self = Unmanaged.fromOpaque(userdata).takeUnretainedValue()
+ guard let light = _self.nativeController.light else { return 0 }
+ light.color = .init(red: Float(red), green: Float(green), blue: Float(blue))
return 0
},
SendEffect: { userdata, data, size in
- print("Effect sent with size \(size)")
return 0
}
)
- instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
+ instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)
if instanceID < 0 {
- print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return
}
controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil {
- print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return
}
if #available(iOS 16, *) {
guard let gamepad = nativeController.extendedGamepad
else { return }
-
- setupButtonChangeListener(gamepad.buttonA, for: .A)
- setupButtonChangeListener(gamepad.buttonB, for: .B)
- setupButtonChangeListener(gamepad.buttonX, for: .X)
- setupButtonChangeListener(gamepad.buttonY, for: .Y)
+
+ setupButtonChangeListener(gamepad.buttonA, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .B : .A)
+ setupButtonChangeListener(gamepad.buttonB, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .A : .B)
+ setupButtonChangeListener(gamepad.buttonX, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .Y : .X)
+ setupButtonChangeListener(gamepad.buttonY, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .X : .Y)
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
@@ -139,49 +173,13 @@ class NativeController: Hashable {
func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) {
button.valueChangedHandler = { [unowned self] _, value, pressed in
-// print("Value: \(value), Is pressed: \(pressed)")
+// // print("Value: \(value), Is pressed: \(pressed)")
let axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let scaledValue = Sint16(value * 32767.0)
updateAxisValue(value: scaledValue, forAxis: axis)
}
}
- static func rumble(lowFreq: Float, highFreq: Float) {
- do {
- // Low-frequency haptic pattern
- let lowFreqPattern = try CHHapticPattern(events: [
- CHHapticEvent(eventType: .hapticTransient, parameters: [
- CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
- ], relativeTime: 0, duration: 0.2)
- ], parameters: [])
-
- // High-frequency haptic pattern
- let highFreqPattern = try CHHapticPattern(events: [
- CHHapticEvent(eventType: .hapticTransient, parameters: [
- CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
- ], relativeTime: 0.2, duration: 0.2)
- ], parameters: [])
-
- // Create and start the haptic engine
- let engine = try CHHapticEngine()
- try engine.start()
-
- // Create and play the low-frequency player
- let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
- try lowFreqPlayer.start(atTime: 0)
-
- // Create and play the high-frequency player after a short delay
- let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
- try highFreqPlayer.start(atTime: 0.2)
-
- } catch {
- print("Error creating haptic patterns: \(error)")
- }
- }
-
-
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID)
@@ -206,7 +204,6 @@ class NativeController: Hashable {
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return }
-// print("Button: \(button.rawValue) {state: \(state)}")
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Rumble/RumbleController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Rumble/RumbleController.swift
new file mode 100644
index 000000000..2f7793af7
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Rumble/RumbleController.swift
@@ -0,0 +1,132 @@
+//
+// RumbleController.swift
+// MeloNX
+//
+// Created by MediaMoots on 2025/5/24.
+//
+
+import CoreHaptics
+import Foundation
+
+class RumbleController {
+
+ private var engine: CHHapticEngine?
+ private var lowHapticPlayer: CHHapticPatternPlayer?
+ private var highHapticPlayer: CHHapticPatternPlayer?
+ private var rumbleMultiplier: Float = 1.0
+
+ // The duration of each continuous haptic event.
+ // We'll restart the players before this duration expires.
+ private let hapticEventDuration: TimeInterval = 20
+
+ // Timer to schedule player restarts
+ private var playerRestartTimer: Timer?
+
+ // Interval before the haptic event duration runs out to restart
+ private let restartGracePeriod: TimeInterval = 1.0
+
+ init (engine: CHHapticEngine?, rumbleMultiplier: Float) {
+ self.engine = engine
+ self.rumbleMultiplier = rumbleMultiplier
+
+ createPlayers()
+ setupPlayerRestartTimer()
+ }
+
+ // Deinitializer to clean up the timer and stop players when the controller is deallocated
+ deinit {
+ playerRestartTimer?.invalidate() // Stop the timer
+ playerRestartTimer = nil
+
+ // Optionally stop the haptic players immediately
+ try? lowHapticPlayer?.stop(atTime: CHHapticTimeImmediate)
+ try? highHapticPlayer?.stop(atTime: CHHapticTimeImmediate)
+
+ // print("RumbleController deinitialized.")
+ }
+
+ // MARK: - Private Methods for Player Management
+ private func createPlayers() {
+ // Ensure the engine is available before proceeding
+ guard let engine = self.engine else {
+ // print("CHHapticEngine is nil. Cannot initialize RumbleController.")
+ return
+ }
+
+ do {
+ let baseIntensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
+
+ let lowSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.0)
+ let highSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1)
+
+ // Create continuous haptic events with the defined duration
+ let lowContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, lowSharpness], relativeTime: 0, duration: hapticEventDuration)
+ let highContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, highSharpness], relativeTime: 0, duration: hapticEventDuration)
+
+ // Create patterns from the continuous haptic events.
+ let lowPattern = try CHHapticPattern(events: [lowContinuousEvent], parameters: [])
+ let highPattern = try CHHapticPattern(events: [highContinuousEvent], parameters: [])
+
+ // Make players from the patterns
+ lowHapticPlayer = try engine.makePlayer(with: lowPattern)
+ highHapticPlayer = try engine.makePlayer(with: highPattern)
+
+ rumble(lowFreq: 0, highFreq: 0)
+
+ // Start players initially
+ try lowHapticPlayer?.start(atTime: 0)
+ try highHapticPlayer?.start(atTime: 0)
+ } catch {
+ // print("Error initializing RumbleController or setting up haptic player: \(error.localizedDescription)")
+
+ // Clean up if setup fails
+ lowHapticPlayer = nil
+ highHapticPlayer = nil
+ playerRestartTimer?.invalidate()
+ playerRestartTimer = nil
+ }
+ }
+
+ private func setupPlayerRestartTimer() {
+ // Invalidate any existing timer to prevent multiple timers if init is called multiple times
+ playerRestartTimer?.invalidate()
+
+ // Calculate the interval for restarting: 1 second before the haptic event duration ends
+ let restartInterval = hapticEventDuration - restartGracePeriod
+
+ guard restartInterval > 0 else {
+ // print("Warning: hapticEventDuration (\(hapticEventDuration)s) is too short for scheduled restart with grace period (\(restartGracePeriod)s). Timer will not be set.")
+ return
+ }
+
+ // Schedule a repeating timer that calls restartPlayers()
+ playerRestartTimer = Timer.scheduledTimer(withTimeInterval: restartInterval, repeats: true) { [weak self] _ in
+ self?.createPlayers()
+ }
+ // Ensure the timer is added to the current run loop in its default mode
+ RunLoop.current.add(playerRestartTimer!, forMode: .default)
+
+ // print("Haptic Players restart timer scheduled to fire every \(restartInterval) seconds.")
+ }
+
+ // MARK: - Public Rumble Control
+
+ public func rumble(lowFreq: Float, highFreq: Float) {
+
+ // Normalize SDL values (0-65535) to CoreHaptics range (0.0-1.0)
+ let normalizedLow = min(1.0, max(0.0, lowFreq * rumbleMultiplier / 65535.0))
+ let normalizedHigh = min(1.0, max(0.0, highFreq * rumbleMultiplier / 65535.0))
+
+ // Create dynamic parameters to control intensity
+ let lowIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedLow, relativeTime: 0)
+ let highIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedHigh, relativeTime: 0)
+
+ // Send parameters to the players
+ do {
+ try lowHapticPlayer?.sendParameters([lowIntensityParameter], atTime: 0)
+ try highHapticPlayer?.sendParameters([highIntensityParameter], atTime: 0)
+ } catch {
+ // print("Error sending haptic parameters: \(error.localizedDescription)")
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift
index 743b843e4..9ffbcf85d 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift
@@ -9,16 +9,49 @@ import Foundation
import CoreHaptics
import UIKit
-class VirtualController {
+class VirtualController : BaseController {
private var instanceID: SDL_JoystickID = -1
private var controller: OpaquePointer?
+ private let hapticEngine: CHHapticEngine?
+ private let rumbleController: RumbleController?
+ private var deviceMotionProvider: DeviceMotionProvider?
public let controllername = "MeloNX Touch Controller"
init() {
+ // Setup Haptics
+ hapticEngine = try? CHHapticEngine()
+ if let hapticsEngine = hapticEngine {
+ do {
+ try hapticsEngine.start()
+ rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: 2.0)
+
+ // print("CHHapticEngine started and RumbleController initialized.")
+ } catch {
+ // print("Error starting CHHapticEngine: \(error.localizedDescription)")
+ rumbleController = nil
+ }
+ } else {
+ // print("CHHapticEngine is nil. Cannot initialize RumbleController.")
+ rumbleController = nil
+ }
setupVirtualController()
}
+ internal func tryRegisterMotion(slot: UInt8) {
+ // Setup Motion
+ let dsuServer = DSUServer.shared
+
+ deviceMotionProvider = DeviceMotionProvider(slot: slot)
+ if let provider = deviceMotionProvider {
+ dsuServer.register(provider)
+ }
+ }
+
+ internal func tryGetMotionProvider() -> DSUMotionProvider? {
+ return deviceMotionProvider
+ }
+
private func setupVirtualController() {
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
@@ -36,94 +69,50 @@ class VirtualController {
button_mask: 0,
axis_mask: 0,
name: controllername.withCString { $0 },
- userdata: nil,
+ userdata: Unmanaged.passUnretained(self).toOpaque(),
Update: { userdata in
// Update joystick state here
},
SetPlayerIndex: { userdata, playerIndex in
- print("Player index set to \(playerIndex)")
+ // print("Player index set to \(playerIndex)")
},
Rumble: { userdata, lowFreq, highFreq in
- print("Rumble with \(lowFreq), \(highFreq)")
+ // print("Rumble with \(lowFreq), \(highFreq)")
if UIDevice.current.userInterfaceIdiom == .phone {
- VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
+ guard let userdata else { return 0 }
+ let _self = Unmanaged.fromOpaque(userdata).takeUnretainedValue()
+ _self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
}
return 0
},
RumbleTriggers: { userdata, leftRumble, rightRumble in
- print("Trigger rumble with \(leftRumble), \(rightRumble)")
+ // print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0
},
SetLED: { userdata, red, green, blue in
- print("Set LED to RGB(\(red), \(green), \(blue))")
+ // print("Set LED to RGB(\(red), \(green), \(blue))")
return 0
},
SendEffect: { userdata, data, size in
- print("Effect sent with size \(size)")
+ // print("Effect sent with size \(size)")
return 0
}
)
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
if instanceID < 0 {
- print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
+ // print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return
}
controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil {
- print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
+ // print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return
}
}
- static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
- do {
- let lowFreqPattern = try CHHapticPattern(events: [
- CHHapticEvent(eventType: .hapticTransient, parameters: [
- CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
- ], relativeTime: 0, duration: 0.2)
- ], parameters: [])
-
-
- let highFreqPattern = try CHHapticPattern(events: [
- CHHapticEvent(eventType: .hapticTransient, parameters: [
- CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
- ], relativeTime: 0.2, duration: 0.2)
- ], parameters: [])
-
- var engine = engine
-
- if engine == nil {
- if hapticEngine == nil {
- hapticEngine = try CHHapticEngine()
- try hapticEngine?.start()
- }
-
- engine = hapticEngine
- }
-
- guard let engine else {
- return print("Error creating haptic patterns: hapticEngine is nil")
- }
-
- let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
- try lowFreqPlayer.start(atTime: 0)
-
- let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
- try highFreqPlayer.start(atTime: 0)
-
- } catch {
- print("Error creating haptic patterns: \(error)")
- }
- }
-
- private static var hapticEngine: CHHapticEngine?
-
-
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID)
@@ -131,10 +120,8 @@ class VirtualController {
}
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
- let scaleFactor = 32767.0 / 160
-
- let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
- let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
+ let scaledX = Int16(min(32767.0, max(-32768.0, x * 32767.0)))
+ let scaledY = Int16(min(32767.0, max(-32768.0, y * 32767.0)))
if stick == .right {
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
@@ -148,7 +135,7 @@ class VirtualController {
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return }
- print("Button: \(button.rawValue) {state: \(state)}")
+ // // print("Button: \(button.rawValue) {state: \(state)}")
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift
index 06070a38b..22cdebc25 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift
@@ -13,7 +13,7 @@ class MemoryUsageMonitor: ObservableObject {
private var timer: Timer?
init() {
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
+ timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] _ in
self?.updateMemoryUsage()
}
}
@@ -32,11 +32,12 @@ class MemoryUsageMonitor: ObservableObject {
}
if result == KERN_SUCCESS {
+ memoryUsage = 0
memoryUsage = taskInfo.phys_footprint
}
else {
- print("Error with task_info(): " +
- (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
+ // print("Error with task_info(): " +
+ // (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
}
}
@@ -46,7 +47,6 @@ class MemoryUsageMonitor: ObservableObject {
formatter.countStyle = .memory
return formatter.string(fromByteCount: Int64(bytes))
}
-
}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/PerformanceDisplay/PerformanceOverlay.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/PerformanceDisplay/PerformanceOverlay.swift
deleted file mode 100644
index ac014ff4a..000000000
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/PerformanceDisplay/PerformanceOverlay.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-//
-// Untitled.swift
-// MeloNX
-//
-// Created by Stossy11 on 21/12/2024.
-//
-
-import SwiftUI
-
-struct PerformanceOverlayView: View {
- @StateObject private var memorymonitor = MemoryUsageMonitor()
-
- @StateObject private var fpsmonitor = FPSMonitor()
-
- var body: some View {
- VStack {
- Text("\(fpsmonitor.formatFPS())")
- Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage))
- }
- }
-}
-
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift
index 27280051b..8985973a6 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift
@@ -6,34 +6,28 @@
//
import Foundation
+import SwiftUI
-
-class MTLHud {
-
+class MTLHud: ObservableObject {
@Published var canMetalHud: Bool = false
- var isEnabled: Bool {
- if let getenv = getenv("MTL_HUD_ENABLED") {
- return String(cString: getenv).contains("1")
+ @AppStorage("MTL_HUD_ENABLED") var metalHudEnabled: Bool = false {
+ didSet {
+ if metalHudEnabled {
+ enable()
+ } else {
+ disable()
+ }
}
- return false
}
+
static let shared = MTLHud()
private init() {
- let _ = openMetalDylib() // i'm fixing the warnings just because you said i suck at coding Autumn (propenchiefer,
- https://youtu.be/tc65SNOTMz4 7:23)
- if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
- enable()
- } else {
- disable()
- }
- }
-
- func toggle() {
- print(UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED"))
- if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
+ canMetalHud = openMetalDylib() // i'm fixing the warnings just because you said i suck at coding Autumn (propenchiefer, https://youtu.be/tc65SNOTMz4 7:23)
+
+ if metalHudEnabled {
enable()
} else {
disable()
@@ -44,14 +38,8 @@ class MTLHud {
let path = "/usr/lib/libMTLHud.dylib"
if dlopen(path, RTLD_NOW) != nil {
- print("Library loaded from \(path)")
- canMetalHud = true
return true
} else {
- if let error = String(validatingUTF8: dlerror()) {
- print("Error loading library: \(error)")
- }
- canMetalHud = false
return false
}
}
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
index cd6819eb9..3c09d1905 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
@@ -10,6 +10,94 @@ import SwiftUI
import GameController
import MetalKit
import Metal
+import Darwin
+
+class LogCapture {
+ static let shared = LogCapture()
+
+ private var stdoutPipe: Pipe?
+ private var stderrPipe: Pipe?
+ private let originalStdout: Int32
+ private let originalStderr: Int32
+
+ var capturedLogs: [String] = [] {
+ didSet {
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(name: .newLogCaptured, object: nil)
+ }
+ }
+ }
+
+ private init() {
+ originalStdout = dup(STDOUT_FILENO)
+ originalStderr = dup(STDERR_FILENO)
+ startCapturing()
+ }
+
+ func startCapturing() {
+ stdoutPipe = Pipe()
+ stderrPipe = Pipe()
+
+ redirectOutput(to: stdoutPipe!, fileDescriptor: STDOUT_FILENO)
+ redirectOutput(to: stderrPipe!, fileDescriptor: STDERR_FILENO)
+
+ setupReadabilityHandler(for: stdoutPipe!, isStdout: true)
+ setupReadabilityHandler(for: stderrPipe!, isStdout: false)
+ }
+
+ func stopCapturing() {
+ dup2(originalStdout, STDOUT_FILENO)
+ dup2(originalStderr, STDERR_FILENO)
+
+ stdoutPipe?.fileHandleForReading.readabilityHandler = nil
+ stderrPipe?.fileHandleForReading.readabilityHandler = nil
+ }
+
+ private func redirectOutput(to pipe: Pipe, fileDescriptor: Int32) {
+ dup2(pipe.fileHandleForWriting.fileDescriptor, fileDescriptor)
+ }
+
+ private func setupReadabilityHandler(for pipe: Pipe, isStdout: Bool) {
+ pipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
+ let data = fileHandle.availableData
+ let originalFD = isStdout ? self?.originalStdout : self?.originalStderr
+ write(originalFD ?? STDOUT_FILENO, (data as NSData).bytes, data.count)
+
+ if let logString = String(data: data, encoding: .utf8),
+ let cleanedLog = self?.cleanLog(logString), !cleanedLog.isEmpty {
+ self?.capturedLogs.append(cleanedLog)
+ }
+ }
+ }
+
+ private func cleanLog(_ raw: String) -> String? {
+ let lines = raw.split(separator: "\n")
+ let filteredLines = lines.filter { line in
+ !line.contains("SwiftUI") &&
+ !line.contains("ForEach") &&
+ !line.contains("VStack") &&
+ !line.contains("Invalid frame dimension (negative or non-finite).")
+ }
+
+ let cleaned = filteredLines.map { line -> String in
+ if let tabRange = line.range(of: "\t") {
+ return line[tabRange.upperBound...].trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+ return line.trimmingCharacters(in: .whitespacesAndNewlines)
+ }.joined(separator: "\n")
+
+ return cleaned.isEmpty ? nil : cleaned.replacingOccurrences(of: "\n\n", with: "\n")
+ }
+
+ deinit {
+ stopCapturing()
+ }
+}
+
+
+extension Notification.Name {
+ static let newLogCaptured = Notification.Name("newLogCaptured")
+}
struct Controller: Identifiable, Hashable {
var id: String
@@ -30,22 +118,28 @@ struct iOSNav: View {
}
}
+func threadEntry(_ arg: () -> Void) -> UnsafeMutableRawPointer? {
+ arg()
+ return nil
+}
+
class Ryujinx : ObservableObject {
- private var isRunning = false
+ @Published var isRunning = false
let virtualController = VirtualController()
@Published var controllerMap: [Controller] = []
@Published var metalLayer: CAMetalLayer? = nil
+ @Published var isPortrait = false
@Published var firmwareversion = "0"
@Published var emulationUIView: MeloMTKView? = nil
- @Published var config: Ryujinx.Configuration? = nil
+ @Published var config: Ryujinx.Arguments? = nil
@Published var games: [Game] = []
@Published var defMLContentSize: CGFloat?
- var thread: Thread!
+ var thread: pthread_t? = nil
@Published var jitenabled = false
@@ -54,41 +148,76 @@ class Ryujinx : ObservableObject {
}
static let shared = Ryujinx()
-
- private init() {
+
+ func addGames() {
self.games = loadGames()
}
- public struct Configuration : Codable, Equatable {
+ func runloop(_ cool: @escaping () -> Void) {
+ if UserDefaults.standard.bool(forKey: "runOnMainThread") {
+ RunLoop.main.perform {
+ cool()
+ }
+ } else {
+ // Box the closure
+ let boxed = Unmanaged.passRetained(ClosureBox(cool)).toOpaque()
+
+ var thread: pthread_t?
+ let result = pthread_create(&thread, nil, { arg in
+ let unmanaged = Unmanaged.fromOpaque(arg)
+ let box = unmanaged.takeRetainedValue()
+ box.closure()
+ return nil
+ }, boxed)
+
+ if result == 0 {
+ pthread_detach(thread!)
+ } else {
+ print("Failed to create thread: \(result)")
+ Unmanaged.fromOpaque(boxed).release()
+ }
+ }
+ }
+
+ private class ClosureBox {
+ let closure: () -> Void
+ init(_ closure: @escaping () -> Void) {
+ self.closure = closure
+ }
+ }
+
+ public class Arguments : Observable, Codable, Equatable {
var gamepath: String
var inputids: [String]
- var resscale: Float
- var debuglogs: Bool
- var tracelogs: Bool
- var nintendoinput: Bool
- var enableInternet: Bool
- var listinputids: Bool
- var aspectRatio: AspectRatio
- var memoryManagerMode: String
- var disableShaderCache: Bool
- var hypervisor: Bool
- var disableDockedMode: Bool
- var enableTextureRecompression: Bool
- var additionalArgs: [String]
- var maxAnisotropy: Float
- var macroHLE: Bool
- var ignoreMissingServices: Bool
- var expandRam: Bool
- var dfsIntegrityChecks: Bool
- var disablePTC: Bool
- var disablevsync: Bool
- var language: SystemLanguage
- var regioncode: SystemRegionCode
- var handHeldController: Bool
+ var inputDSUServers: [String]
+ var resscale: Float = 1.0
+ var debuglogs: Bool = false
+ var tracelogs: Bool = false
+ var nintendoinput: Bool = true
+ var enableInternet: Bool = false
+ var listinputids: Bool = false
+ var aspectRatio: AspectRatio = .fixed16x9
+ var memoryManagerMode: String = "HostMappedUnsafe"
+ var disableShaderCache: Bool = false
+ var hypervisor: Bool = false
+ var disableDockedMode: Bool = false
+ var enableTextureRecompression: Bool = true
+ var additionalArgs: [String] = []
+ var maxAnisotropy: Float = 1.0
+ var macroHLE: Bool = true
+ var ignoreMissingServices: Bool = false
+ var expandRam: Bool = false
+ var dfsIntegrityChecks: Bool = false
+ var disablePTC: Bool = false
+ var disablevsync: Bool = false
+ var language: SystemLanguage = .americanEnglish
+ var regioncode: SystemRegionCode = .usa
+ var handHeldController: Bool = true
-
- init(gamepath: String,
+
+ init(gamepath: String = "",
inputids: [String] = [],
+ inputDSUServers: [String] = [],
debuglogs: Bool = false,
tracelogs: Bool = false,
listinputids: Bool = false,
@@ -111,10 +240,11 @@ class Ryujinx : ObservableObject {
disablevsync: Bool = false,
language: SystemLanguage = .americanEnglish,
regioncode: SystemRegionCode = .usa,
- handHeldController: Bool = false
+ handHeldController: Bool = false,
) {
self.gamepath = gamepath
self.inputids = inputids
+ self.inputDSUServers = inputDSUServers
self.debuglogs = debuglogs
self.tracelogs = tracelogs
self.listinputids = listinputids
@@ -139,17 +269,71 @@ class Ryujinx : ObservableObject {
self.regioncode = regioncode
self.handHeldController = handHeldController
}
+
+
+ static func == (lhs: Arguments, rhs: Arguments) -> Bool {
+ return lhs.resscale == rhs.resscale &&
+ lhs.debuglogs == rhs.debuglogs &&
+ lhs.tracelogs == rhs.tracelogs &&
+ lhs.nintendoinput == rhs.nintendoinput &&
+ lhs.enableInternet == rhs.enableInternet &&
+ lhs.listinputids == rhs.listinputids &&
+ lhs.aspectRatio == rhs.aspectRatio &&
+ lhs.memoryManagerMode == rhs.memoryManagerMode &&
+ lhs.disableShaderCache == rhs.disableShaderCache &&
+ lhs.hypervisor == rhs.hypervisor &&
+ lhs.disableDockedMode == rhs.disableDockedMode &&
+ lhs.enableTextureRecompression == rhs.enableTextureRecompression &&
+ lhs.additionalArgs == rhs.additionalArgs &&
+ lhs.maxAnisotropy == rhs.maxAnisotropy &&
+ lhs.macroHLE == rhs.macroHLE &&
+ lhs.ignoreMissingServices == rhs.ignoreMissingServices &&
+ lhs.expandRam == rhs.expandRam &&
+ lhs.dfsIntegrityChecks == rhs.dfsIntegrityChecks &&
+ lhs.disablePTC == rhs.disablePTC &&
+ lhs.disablevsync == rhs.disablevsync &&
+ lhs.language == rhs.language &&
+ lhs.regioncode == rhs.regioncode &&
+ lhs.handHeldController == rhs.handHeldController
+ }
}
- func start(with config: Configuration) throws {
+ func start(with config: Arguments) throws {
guard !isRunning else {
throw RyujinxError.alreadyRunning
}
self.config = config
- thread = Thread { [self] in
+
+ if UserDefaults.standard.bool(forKey: "lockInApp") {
+ let cool = Thread {
+ while true {
+ if UserDefaults.standard.bool(forKey: "lockInApp") {
+ if let workspaceClass = NSClassFromString("LSApplicationWorkspace") as? NSObject.Type,
+ let workspace = workspaceClass.perform(NSSelectorFromString("defaultWorkspace"))?.takeUnretainedValue() {
+
+ let selector = NSSelectorFromString("openApplicationWithBundleID:")
+
+ if workspace.responds(to: selector) {
+ workspace.perform(selector, with: Bundle.main.bundleIdentifier ?? "")
+ } else {
+ print("Selector not found or not responding.")
+ }
+ } else {
+ print("Could not get LSApplicationWorkspace class.")
+ }
+ }
+ }
+ }
+
+ cool.qualityOfService = .userInteractive
+ cool.start()
+ }
+
+
+ runloop { [self] in
isRunning = true
@@ -169,7 +353,9 @@ class Ryujinx : ObservableObject {
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
if result != 0 {
- self.isRunning = false
+ DispatchQueue.main.async {
+ self.isRunning = false
+ }
if let accessing, accessing {
url!.stopAccessingSecurityScopedResource()
}
@@ -178,17 +364,103 @@ class Ryujinx : ObservableObject {
}
}
} catch {
- self.isRunning = false
- Self.log("Emulation failed to start: \(error)")
+ DispatchQueue.main.async {
+ self.isRunning = false
+ }
+ Thread.sleep(forTimeInterval: 0.3)
+ let logs = LogCapture.shared.capturedLogs
+ let parsedLogs = extractExceptionInfo(logs)
+ if let parsedLogs {
+ DispatchQueue.main.async {
+ let result = Array(logs.suffix(from: parsedLogs.lineIndex))
+
+ LogCapture.shared.capturedLogs = Array(LogCapture.shared.capturedLogs.prefix(upTo: parsedLogs.lineIndex))
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
+ let currentDate = Date()
+ let dateString = dateFormatter.string(from: currentDate)
+ let path = URL.documentsDirectory.appendingPathComponent("StackTrace").appendingPathComponent("StackTrace-\(dateString).txt").path
+
+ self.saveArrayAsTextFile(strings: result, filePath: path)
+
+
+ presentAlert(title: "MeloNX Crashed!", message: parsedLogs.exceptionType + ": " + parsedLogs.message) {
+ UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ exit(0)
+ }
+ }
+ }
+ } else {
+ DispatchQueue.main.async {
+ presentAlert(title: "MeloNX Crashed!", message: "Unknown Error") {
+ UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ exit(0)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ func saveArrayAsTextFile(strings: [String], filePath: String) {
+ let text = strings.joined(separator: "\n")
+
+ let path = URL.documentsDirectory.appendingPathComponent("StackTrace").path
+
+ do {
+ try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false)
+ } catch {
+
+ }
+
+ do {
+ try text.write(to: URL(fileURLWithPath: filePath), atomically: true, encoding: .utf8)
+ print("File saved successfully.")
+ } catch {
+ print("Error saving file: \(error)")
+ }
+ }
+
+ struct ExceptionInfo {
+ let exceptionType: String
+ let message: String
+ let lineIndex: Int
+ }
+
+ func extractExceptionInfo(_ logs: [String]) -> ExceptionInfo? {
+ for i in (0.. [Game] {
let fileManager = FileManager.default
@@ -218,7 +487,7 @@ class Ryujinx : ObservableObject {
do {
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
- print("Failed to create roms directory: \(error)")
+ // print("Failed to create roms directory: \(error)")
}
}
var games: [Game] = []
@@ -243,19 +512,18 @@ class Ryujinx : ObservableObject {
games.append(game)
} catch {
- print(error)
+ // print(error)
}
}
return games
} catch {
- print("Error loading games from roms folder: \(error)")
+ // print("Error loading games from roms folder: \(error)")
return games
}
-
}
- private func buildCommandLineArgs(from config: Configuration) -> [String] {
+ func buildCommandLineArgs(from config: Arguments) -> [String] {
var args: [String] = []
// Add the game path
@@ -267,18 +535,41 @@ class Ryujinx : ObservableObject {
args.append(contentsOf: ["--memory-manager-mode", config.memoryManagerMode])
- // args.append(contentsOf: ["--exclusive-fullscreen", String(true)])
- // args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"])
- // args.append(contentsOf: ["--exclusive-fullscreen-height", "\(Int(UIScreen.main.bounds.height))"])
+ args.append(contentsOf: ["--exclusive-fullscreen", String(true)])
+ args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"])
+ args.append(contentsOf: ["--exclusive-fullscreen-height", "\(Int(UIScreen.main.bounds.height))"])
// We don't need this. Ryujinx should handle it fine :3
// this also causes crashes in some games :3
+ var model = ""
+
+ var systemInfo = utsname()
+ uname(&systemInfo)
+ let machineMirror = Mirror(reflecting: systemInfo.machine)
+ model = machineMirror.children.reduce("") { identifier, element in
+ guard let value = element.value as? Int8, value != 0 else { return identifier }
+ return identifier + String(UnicodeScalar(UInt8(value)))
+ }
+
+ args.append(contentsOf: ["--device-model", model])
+
+ args.append(contentsOf: ["--device-display-name", UIDevice.modelName])
+
+ if checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") {
+ args.append("--has-memory-entitlement")
+ }
+
args.append(contentsOf: ["--system-language", config.language.rawValue])
args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
+ args.append(contentsOf: ["--system-timezone", TimeZone.current.identifier])
+
+ // args.append(contentsOf: ["--system-time-offset", String(TimeZone.current.secondsFromGMT())])
+
+
if config.nintendoinput {
args.append("--correct-controller")
}
@@ -356,6 +647,16 @@ class Ryujinx : ObservableObject {
}
}
+ // Append the input dsu servers (limit to 8 (used to be 4) just in case)
+ if !config.inputDSUServers.isEmpty {
+ config.inputDSUServers.prefix(8).enumerated().forEach { index, inputDSUServer in
+ if index == 0 {
+ args.append(contentsOf: ["--input-dsu-server-handheld", inputDSUServer])
+ }
+ args.append(contentsOf: ["--input-dsu-server-\(index + 1)", inputDSUServer])
+ }
+ }
+
args.append(contentsOf: config.additionalArgs)
return args
@@ -383,7 +684,7 @@ class Ryujinx : ObservableObject {
func installFirmware(firmwarePath: String) {
guard let cString = firmwarePath.cString(using: .utf8) else {
- print("Invalid firmware path")
+ // print("Invalid firmware path")
return
}
@@ -399,12 +700,12 @@ class Ryujinx : ObservableObject {
guard let titleIdCString = titleId.cString(using: .utf8),
let pathCString = path.cString(using: .utf8)
else {
- print("Invalid path")
+ // print("Invalid path")
return []
}
let listPointer = get_dlc_nca_list(titleIdCString, pathCString)
- print("DLC parcing success: \(listPointer.success)")
+ // print("DLC parcing success: \(listPointer.success)")
guard listPointer.success else { return [] }
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
@@ -456,7 +757,7 @@ class Ryujinx : ObservableObject {
let guid = generateGamepadId(joystickIndex: i)
let name = String(cString: SDL_GameControllerName(controller))
- print("Controller \(i): \(name), GUID: \(guid ?? "")")
+ // print("Controller \(i): \(name), GUID: \(guid ?? "")")
guard let guid else {
SDL_GameControllerClose(controller)
@@ -487,33 +788,163 @@ class Ryujinx : ObservableObject {
do {
if fileManager.fileExists(atPath: registeredFolder) {
try fileManager.removeItem(atPath: registeredFolder)
- print("Folder removed successfully.")
+ // print("Folder removed successfully.")
let version = fetchFirmwareVersion()
if version.isEmpty {
self.firmwareversion = "0"
} else {
- print("Firmware eeeeee \(version)")
+ // print("Firmware eeeeee \(version)")
}
} else {
- print("Folder does not exist.")
+ // print("Folder does not exist.")
}
} catch {
- print("Error removing folder: \(error)")
+ // print("Error removing folder: \(error)")
}
}
static func log(_ message: String) {
- print("[Ryujinx] \(message)")
+ // print("[Ryujinx] \(message)")
+ }
+
+ public func updateOrientation() -> Bool {
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let window = windowScene.windows.first {
+ return (window.bounds.size.height > window.bounds.size.width)
+ }
+ return false
}
func ryuIsJITEnabled() {
jitenabled = isJITEnabled()
- print("JIT \(jitenabled)")
}
}
+public extension UIDevice {
+ static let modelName: String = {
+ var systemInfo = utsname()
+ uname(&systemInfo)
+ let machineMirror = Mirror(reflecting: systemInfo.machine)
+ let identifier = machineMirror.children.reduce("") { identifier, element in
+ guard let value = element.value as? Int8, value != 0 else { return identifier }
+ return identifier + String(UnicodeScalar(UInt8(value)))
+ }
+
+ func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity
+ #if os(iOS)
+ switch identifier {
+ case "iPod5,1": return "iPod touch (5th generation)"
+ case "iPod7,1": return "iPod touch (6th generation)"
+ case "iPod9,1": return "iPod touch (7th generation)"
+ case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4"
+ case "iPhone4,1": return "iPhone 4s"
+ case "iPhone5,1", "iPhone5,2": return "iPhone 5"
+ case "iPhone5,3", "iPhone5,4": return "iPhone 5c"
+ case "iPhone6,1", "iPhone6,2": return "iPhone 5s"
+ case "iPhone7,2": return "iPhone 6"
+ case "iPhone7,1": return "iPhone 6 Plus"
+ case "iPhone8,1": return "iPhone 6s"
+ case "iPhone8,2": return "iPhone 6s Plus"
+ case "iPhone9,1", "iPhone9,3": return "iPhone 7"
+ case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus"
+ case "iPhone10,1", "iPhone10,4": return "iPhone 8"
+ case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus"
+ case "iPhone10,3", "iPhone10,6": return "iPhone X"
+ case "iPhone11,2": return "iPhone XS"
+ case "iPhone11,4", "iPhone11,6": return "iPhone XS Max"
+ case "iPhone11,8": return "iPhone XR"
+ case "iPhone12,1": return "iPhone 11"
+ case "iPhone12,3": return "iPhone 11 Pro"
+ case "iPhone12,5": return "iPhone 11 Pro Max"
+ case "iPhone13,1": return "iPhone 12 mini"
+ case "iPhone13,2": return "iPhone 12"
+ case "iPhone13,3": return "iPhone 12 Pro"
+ case "iPhone13,4": return "iPhone 12 Pro Max"
+ case "iPhone14,4": return "iPhone 13 mini"
+ case "iPhone14,5": return "iPhone 13"
+ case "iPhone14,2": return "iPhone 13 Pro"
+ case "iPhone14,3": return "iPhone 13 Pro Max"
+ case "iPhone14,7": return "iPhone 14"
+ case "iPhone14,8": return "iPhone 14 Plus"
+ case "iPhone15,2": return "iPhone 14 Pro"
+ case "iPhone15,3": return "iPhone 14 Pro Max"
+ case "iPhone15,4": return "iPhone 15"
+ case "iPhone15,5": return "iPhone 15 Plus"
+ case "iPhone16,1": return "iPhone 15 Pro"
+ case "iPhone16,2": return "iPhone 15 Pro Max"
+ case "iPhone17,3": return "iPhone 16"
+ case "iPhone17,4": return "iPhone 16 Plus"
+ case "iPhone17,1": return "iPhone 16 Pro"
+ case "iPhone17,2": return "iPhone 16 Pro Max"
+ case "iPhone17,5": return "iPhone 16e"
+ case "iPhone8,4": return "iPhone SE"
+ case "iPhone12,8": return "iPhone SE (2nd generation)"
+ case "iPhone14,6": return "iPhone SE (3rd generation)"
+ case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2"
+ case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)"
+ case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)"
+ case "iPad6,11", "iPad6,12": return "iPad (5th generation)"
+ case "iPad7,5", "iPad7,6": return "iPad (6th generation)"
+ case "iPad7,11", "iPad7,12": return "iPad (7th generation)"
+ case "iPad11,6", "iPad11,7": return "iPad (8th generation)"
+ case "iPad12,1", "iPad12,2": return "iPad (9th generation)"
+ case "iPad13,18", "iPad13,19": return "iPad (10th generation)"
+ case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air"
+ case "iPad5,3", "iPad5,4": return "iPad Air 2"
+ case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)"
+ case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
+ case "iPad13,16", "iPad13,17": return "iPad Air (5th generation)"
+ case "iPad14,8", "iPad14,9": return "iPad Air (11-inch) (M2)"
+ case "iPad14,10", "iPad14,11": return "iPad Air (13-inch) (M2)"
+ case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini"
+ case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2"
+ case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3"
+ case "iPad5,1", "iPad5,2": return "iPad mini 4"
+ case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)"
+ case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)"
+ case "iPad16,1", "iPad16,2": return "iPad mini (A17 Pro)"
+ case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)"
+ case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)"
+ case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)"
+ case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)"
+ case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)"
+ case "iPad14,3", "iPad14,4": return "iPad Pro (11-inch) (4th generation)"
+ case "iPad16,3", "iPad16,4": return "iPad Pro (11-inch) (M4)"
+ case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)"
+ case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)"
+ case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)"
+ case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)"
+ case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)"
+ case "iPad14,5", "iPad14,6": return "iPad Pro (12.9-inch) (6th generation)"
+ case "iPad16,5", "iPad16,6": return "iPad Pro (13-inch) (M4)"
+ case "AppleTV5,3": return "Apple TV"
+ case "AppleTV6,2": return "Apple TV 4K"
+ case "AudioAccessory1,1": return "HomePod"
+ case "AudioAccessory5,1": return "HomePod mini"
+ case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
+ default: return identifier
+ }
+ #elseif os(tvOS)
+ switch identifier {
+ case "AppleTV5,3": return "Apple TV 4"
+ case "AppleTV6,2", "AppleTV11,1", "AppleTV14,1": return "Apple TV 4K"
+ case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))"
+ default: return identifier
+ }
+ #elseif os(visionOS)
+ switch identifier {
+ case "RealityDevice14,1": return "Apple Vision Pro"
+ default: return identifier
+ }
+ #endif
+ }
+
+ return mapToDevice(identifier: identifier)
+ }()
+
+}
diff --git a/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift b/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift
index 481dd7724..33b8407e2 100644
--- a/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift
+++ b/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift
@@ -35,7 +35,7 @@ struct LaunchGameIntentDef: AppIntent {
let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName))
let urlString = "melonx://game?name=\(name ?? gameName)"
- print(urlString)
+ // print(urlString)
if let url = URL(string: urlString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
diff --git a/src/MeloNX/MeloNX/App/Models/Game.swift b/src/MeloNX/MeloNX/App/Models/Game.swift
index 1a154e869..af5751e44 100644
--- a/src/MeloNX/MeloNX/App/Models/Game.swift
+++ b/src/MeloNX/MeloNX/App/Models/Game.swift
@@ -57,7 +57,7 @@ public struct Game: Identifiable, Equatable, Hashable {
gameTemp.icon = UIImage(data: imageData)
} else {
- print("Invalid image size.")
+ // print("Invalid image size.")
}
return gameTemp
}
@@ -67,7 +67,7 @@ public struct Game: Identifiable, Equatable, Hashable {
let imageSize = Int(gameInfoValue.ImageSize)
guard imageSize > 0, imageSize <= 1024 * 1024 else {
- print("Invalid image size.")
+ // print("Invalid image size.")
return nil
}
diff --git a/src/MeloNX/MeloNX/App/Models/ToggleButtonsState.swift b/src/MeloNX/MeloNX/App/Models/ToggleButtonsState.swift
new file mode 100644
index 000000000..08e6d9310
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Models/ToggleButtonsState.swift
@@ -0,0 +1,27 @@
+//
+// ToggleButtonsState.swift
+// MeloNX
+//
+// Created by Stossy11 on 12/04/2025.
+//
+
+
+struct ToggleButtonsState: Codable, Equatable {
+ var toggle1: Bool
+ var toggle2: Bool
+ var toggle3: Bool
+ var toggle4: Bool
+
+ init() {
+ self = .default
+ }
+
+ init(toggle1: Bool, toggle2: Bool, toggle3: Bool, toggle4: Bool) {
+ self.toggle1 = toggle1
+ self.toggle2 = toggle2
+ self.toggle3 = toggle3
+ self.toggle4 = toggle4
+ }
+
+ static let `default` = ToggleButtonsState(toggle1: false, toggle2: false, toggle3: false, toggle4: false)
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Extensions/AppCodableStorage.swift b/src/MeloNX/MeloNX/App/Views/Extensions/AppCodableStorage.swift
new file mode 100644
index 000000000..a85f19f94
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Extensions/AppCodableStorage.swift
@@ -0,0 +1,47 @@
+//
+// AppCodableStorage.swift
+// MeloNX
+//
+// Created by Stossy11 on 12/04/2025.
+//
+
+import SwiftUI
+
+@propertyWrapper
+struct AppCodableStorage: DynamicProperty {
+ @State private var value: Value
+
+ private let key: String
+ private let defaultValue: Value
+ private let storage: UserDefaults
+
+ init(wrappedValue defaultValue: Value, _ key: String, store: UserDefaults = .standard) {
+ self._value = State(initialValue: {
+ if let data = store.data(forKey: key),
+ let decoded = try? JSONDecoder().decode(Value.self, from: data) {
+ return decoded
+ }
+ return defaultValue
+ }())
+ self.key = key
+ self.defaultValue = defaultValue
+ self.storage = store
+ }
+
+ var wrappedValue: Value {
+ get { value }
+ nonmutating set {
+ value = newValue
+ if let data = try? JSONEncoder().encode(newValue) {
+ storage.set(data, forKey: key)
+ }
+ }
+ }
+
+ var projectedValue: Binding {
+ Binding(
+ get: { self.wrappedValue },
+ set: { newValue in self.wrappedValue = newValue }
+ )
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift
deleted file mode 100644
index 077a3aac1..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift
+++ /dev/null
@@ -1,372 +0,0 @@
-//
-// ControllerView.swift
-// Pomelo-V2
-//
-// Created by Stossy11 on 16/7/2024.
-//
-
-import SwiftUI
-import GameController
-import SwiftUIJoystick
-import CoreMotion
-
-struct ControllerView: View {
- // MARK: - Properties
- @AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0
- @AppStorage("stick-button") private var stickButton = false
- @State private var isPortrait = true
- @Environment(\.verticalSizeClass) var verticalSizeClass
-
-
- // MARK: - Body
- var body: some View {
- Group {
- let isPad = UIDevice.current.userInterfaceIdiom == .pad
-
- if isPortrait && !isPad {
- portraitLayout
- } else {
- landscapeLayout
- }
- }
- .padding()
- .onChange(of: verticalSizeClass) { _ in
- updateOrientation()
- }
- .onAppear(perform: updateOrientation)
- }
-
- // MARK: - Layouts
- private var portraitLayout: some View {
- VStack {
- Spacer()
- VStack(spacing: 20) {
- HStack(spacing: 30) {
- VStack(spacing: 15) {
- ShoulderButtonsViewLeft()
- ZStack {
- Joystick()
- DPadView()
- }
- }
-
- VStack(spacing: 15) {
- ShoulderButtonsViewRight()
- ZStack {
- Joystick(iscool: true)
- ABXYView()
- }
- }
- }
-
- HStack(spacing: 60) {
- HStack {
- ButtonView(button: .leftStick)
- .padding()
- ButtonView(button: .start)
- }
-
- HStack {
- ButtonView(button: .back)
- ButtonView(button: .rightStick)
- .padding()
- }
- }
- }
- }
- }
-
- private var landscapeLayout: some View {
- VStack {
- Spacer()
-
- HStack {
- VStack(spacing: 15) {
- ShoulderButtonsViewLeft()
- ZStack {
- Joystick()
- DPadView()
- }
- }
-
- Spacer()
-
- centerButtons
-
- Spacer()
-
- VStack(spacing: 15) {
- ShoulderButtonsViewRight()
- ZStack {
- Joystick(iscool: true)
- ABXYView()
- }
- }
- }
- }
- }
-
- private var centerButtons: some View {
- Group {
- if stickButton {
- VStack {
- HStack(spacing: 50) {
- ButtonView(button: .leftStick)
- .padding()
- Spacer()
- ButtonView(button: .rightStick)
- .padding()
- }
- .padding(.top, 30)
-
- HStack(spacing: 50) {
- ButtonView(button: .back)
- Spacer()
- ButtonView(button: .start)
- }
- }
- .padding(.bottom, 20)
- } else {
- HStack(spacing: 50) {
- ButtonView(button: .back)
- Spacer()
- ButtonView(button: .start)
- }
- .padding(.bottom, 20)
- }
- }
- }
-
- // MARK: - Methods
-
- private func updateOrientation() {
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
- let window = windowScene.windows.first {
- isPortrait = window.bounds.size.height > window.bounds.size.width
- }
- }
-}
-
-
-struct ShoulderButtonsViewLeft: View {
- @State private var width: CGFloat = 160
- @State private var height: CGFloat = 20
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
- var body: some View {
- HStack(spacing: 20) {
- ButtonView(button: .leftTrigger)
- ButtonView(button: .leftShoulder)
- }
- .frame(width: width, height: height)
- .onAppear {
- if UIDevice.current.systemName.contains("iPadOS") {
- width *= 1.2
- height *= 1.2
- }
-
- width *= CGFloat(controllerScale)
- height *= CGFloat(controllerScale)
- }
- }
-}
-
-struct ShoulderButtonsViewRight: View {
- @State private var width: CGFloat = 160
- @State private var height: CGFloat = 20
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
- var body: some View {
- HStack(spacing: 20) {
- ButtonView(button: .rightShoulder)
- ButtonView(button: .rightTrigger)
- }
- .frame(width: width, height: height)
- .onAppear {
- if UIDevice.current.systemName.contains("iPadOS") {
- width *= 1.2
- height *= 1.2
- }
-
- width *= CGFloat(controllerScale)
- height *= CGFloat(controllerScale)
- }
- }
-}
-
-struct DPadView: View {
- @State private var size: CGFloat = 145
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
- var body: some View {
- VStack(spacing: 5) {
- ButtonView(button: .dPadUp)
- HStack(spacing: 20) {
- ButtonView(button: .dPadLeft)
- Spacer(minLength: 20)
- ButtonView(button: .dPadRight)
- }
- ButtonView(button: .dPadDown)
- }
- .frame(width: size, height: size)
- .onAppear {
- if UIDevice.current.systemName.contains("iPadOS") {
- size *= 1.2
- }
-
- size *= CGFloat(controllerScale)
- }
- }
-}
-
-struct ABXYView: View {
- @State private var size: CGFloat = 145
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
- var body: some View {
- VStack(spacing: 5) {
- ButtonView(button: .X)
- HStack(spacing: 20) {
- ButtonView(button: .Y)
- Spacer(minLength: 20)
- ButtonView(button: .A)
- }
- ButtonView(button: .B)
- }
- .frame(width: size, height: size)
- .onAppear {
- if UIDevice.current.systemName.contains("iPadOS") {
- size *= 1.2
- }
-
- size *= CGFloat(controllerScale)
- }
- }
-}
-
-struct ButtonView: View {
- var button: VirtualControllerButton
- @State private var width: CGFloat = 45
- @State private var height: CGFloat = 45
- @State private var isPressed = false
- @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
- @Environment(\.presentationMode) var presentationMode
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
- @State private var debounceTimer: Timer?
-
- var body: some View {
- Image(systemName: buttonText)
- .resizable()
- .scaledToFit()
- .frame(width: width, height: height)
- .foregroundColor(true ? Color.white.opacity(0.9) : Color.black.opacity(0.9))
- .background(
- Group {
- if !button.isTrigger {
- Circle()
- .fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
- .frame(width: width * 1.25, height: height * 1.25)
- } else {
- Image(systemName: buttonText)
- .resizable()
- .scaledToFit()
- .frame(width: width * 1.25, height: height * 1.25)
- .foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
- }
- }
- )
- .opacity(isPressed ? 0.6 : 1.0)
- .gesture(
- DragGesture(minimumDistance: 0)
- .onChanged { _ in
- handleButtonPress()
- }
- .onEnded { _ in
- handleButtonRelease()
- }
- )
- .onAppear {
- configureSizeForButton()
- }
- }
-
- private func handleButtonPress() {
- if !isPressed {
- isPressed = true
-
- debounceTimer?.invalidate()
-
- Ryujinx.shared.virtualController.setButtonState(1, for: button)
-
- Haptics.shared.play(.medium)
- }
- }
-
- private func handleButtonRelease() {
- if isPressed {
- isPressed = false
-
- debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { _ in
- Ryujinx.shared.virtualController.setButtonState(0, for: button)
- }
- }
- }
-
- private func configureSizeForButton() {
- if button.isTrigger {
- width = 70
- height = 40
- } else if button.isSmall {
- width = 35
- height = 35
- }
-
- // Adjust for iPad
- if UIDevice.current.systemName.contains("iPadOS") {
- width *= 1.2
- height *= 1.2
- }
-
- width *= CGFloat(controllerScale)
- height *= CGFloat(controllerScale)
- }
-
- private var buttonText: String {
- switch button {
- case .A:
- return "a.circle.fill"
- case .B:
- return "b.circle.fill"
- case .X:
- return "x.circle.fill"
- case .Y:
- return "y.circle.fill"
- case .leftStick:
- return "l.joystick.press.down.fill"
- case .rightStick:
- return "r.joystick.press.down.fill"
- case .dPadUp:
- return "arrowtriangle.up.circle.fill"
- case .dPadDown:
- return "arrowtriangle.down.circle.fill"
- case .dPadLeft:
- return "arrowtriangle.left.circle.fill"
- case .dPadRight:
- return "arrowtriangle.right.circle.fill"
- case .leftTrigger:
- return "zl.rectangle.roundedtop.fill"
- case .rightTrigger:
- return "zr.rectangle.roundedtop.fill"
- case .leftShoulder:
- return "l.rectangle.roundedbottom.fill"
- case .rightShoulder:
- return "r.rectangle.roundedbottom.fill"
- case .start:
- return "plus.circle.fill"
- case .back:
- return "minus.circle.fill"
- case .guide:
- return "house.circle.fill"
- }
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift
deleted file mode 100644
index 3bc838b97..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift
+++ /dev/null
@@ -1,61 +0,0 @@
-//
-// JoystickView.swift
-// Pomelo
-//
-// Created by Stossy11 on 30/9/2024.
-// Copyright © 2024 Stossy11. All rights reserved.
-//
-
-import SwiftUI
-import SwiftUIJoystick
-
-public struct Joystick: View {
- @State var iscool: Bool? = nil
- @Environment(\.colorScheme) var colorScheme
- @ObservedObject public var joystickMonitor = JoystickMonitor()
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
- var dragDiameter: CGFloat {
- var selfs = CGFloat(160)
- selfs *= controllerScale
- if UIDevice.current.systemName.contains("iPadOS") {
- return selfs * 1.2
- }
-
- return selfs
- }
- private let shape: JoystickShape = .circle
-
- public var body: some View {
- VStack{
- JoystickBuilder(
- monitor: self.joystickMonitor,
- width: self.dragDiameter,
- shape: .circle,
- background: {
- Text("")
- .hidden()
- },
- foreground: {
- Circle()
- .fill(colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.7))
- .background(
- Circle()
- .fill(colorScheme == .dark ? Color.gray.opacity(0.3) : Color.gray.opacity(0.2))
- .frame(width: (dragDiameter / 4) * 1.2, height: (dragDiameter / 4) * 1.2)
- )
- },
- locksInPlace: false)
- .onChange(of: self.joystickMonitor.xyPoint) { newValue in
- let scaledX = Float(newValue.x)
- let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/
- print("Joystick Position: (\(scaledX), \(scaledY))")
-
- if iscool != nil {
- Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
- } else {
- Ryujinx.shared.virtualController.thumbstickMoved(.left, x: newValue.x, y: newValue.y)
- }
- }
- }
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Elements/FileImporter.swift b/src/MeloNX/MeloNX/App/Views/Main/Elements/FileImporter.swift
new file mode 100644
index 000000000..bfae480a2
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Elements/FileImporter.swift
@@ -0,0 +1,125 @@
+//
+// FileImporter.swift
+// MeloNX
+//
+// Created by Stossy11 on 17/04/2025.
+//
+
+
+import SwiftUI
+import UniformTypeIdentifiers
+
+class FileImporterManager: ObservableObject {
+ static let shared = FileImporterManager()
+
+ private init() {}
+
+ func importFiles(types: [UTType], allowMultiple: Bool = false, completion: @escaping (Result<[URL], Error>) -> Void) {
+ let id = "\(Unmanaged.passUnretained(completion as AnyObject).toOpaque())"
+
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(
+ name: .importFiles,
+ object: nil,
+ userInfo: [
+ "id": id,
+ "types": types,
+ "allowMultiple": allowMultiple,
+ "completion": completion
+ ]
+ )
+ }
+ }
+}
+
+extension Notification.Name {
+ static let importFiles = Notification.Name("importFiles")
+}
+
+struct FileImporterView: ViewModifier {
+ @State private var isImporterPresented: [String: Bool] = [:]
+ @State private var activeImporters: [String: ImporterConfig] = [:]
+
+ struct ImporterConfig {
+ let types: [UTType]
+ let allowMultiple: Bool
+ let completion: (Result<[URL], Error>) -> Void
+ }
+
+ func body(content: Content) -> some View {
+ content
+ .background(
+ ForEach(Array(activeImporters.keys), id: \.self) { id in
+ if let config = activeImporters[id] {
+ FileImporterWrapper(
+ isPresented: Binding(
+ get: { isImporterPresented[id] ?? false },
+ set: { isImporterPresented[id] = $0 }
+ ),
+ id: id,
+ config: config,
+ onCompletion: { success in
+ if success {
+ DispatchQueue.main.async {
+ activeImporters.removeValue(forKey: id)
+ }
+ }
+ }
+ )
+ }
+ }
+ )
+ .onReceive(NotificationCenter.default.publisher(for: .importFiles)) { notification in
+ guard let userInfo = notification.userInfo,
+ let id = userInfo["id"] as? String,
+ let types = userInfo["types"] as? [UTType],
+ let allowMultiple = userInfo["allowMultiple"] as? Bool,
+ let completion = userInfo["completion"] as? ((Result<[URL], Error>) -> Void) else {
+ return
+ }
+
+ let config = ImporterConfig(
+ types: types,
+ allowMultiple: allowMultiple,
+ completion: completion
+ )
+
+ activeImporters[id] = config
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ isImporterPresented[id] = true
+ }
+ }
+ }
+}
+
+struct FileImporterWrapper: View {
+ @Binding var isPresented: Bool
+ let id: String
+ let config: FileImporterView.ImporterConfig
+ let onCompletion: (Bool) -> Void
+
+ var body: some View {
+ Text("wow")
+ .hidden()
+ .fileImporter(
+ isPresented: $isPresented,
+ allowedContentTypes: config.types,
+ allowsMultipleSelection: config.allowMultiple
+ ) { result in
+ switch result {
+ case .success(let urls):
+ config.completion(.success(urls))
+ case .failure(let error):
+ config.completion(.failure(error))
+ }
+ onCompletion(true)
+ }
+ }
+}
+
+extension View {
+ func withFileImporter() -> some View {
+ self.modifier(FileImporterView())
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift
index 8842c50ea..4230ffae1 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift
@@ -58,7 +58,7 @@ public class Air {
}
@objc func didConnect(sender: NSNotification) {
- print("AirKit - Connect")
+ // print("AirKit - Connect")
self.connected = true
guard let screen: UIScreen = sender.object as? UIScreen else { return }
add(screen: screen) { success in
@@ -69,35 +69,35 @@ public class Air {
func add(screen: UIScreen, completion: @escaping (Bool) -> ()) {
- print("AirKit - Add Screen")
+ // print("AirKit - Add Screen")
airScreen = screen
airWindow = UIWindow(frame: airScreen!.bounds)
guard let viewController: UIViewController = hostingController else {
- print("AirKit - Add - Failed: Hosting Controller Not Found")
+ // print("AirKit - Add - Failed: Hosting Controller Not Found")
completion(false)
return
}
findWindowScene(for: airScreen!) { windowScene in
guard let airWindowScene: UIWindowScene = windowScene else {
- print("AirKit - Add - Failed: Window Scene Not Found")
+ // print("AirKit - Add - Failed: Window Scene Not Found")
completion(false)
return
}
self.airWindow?.rootViewController = viewController
self.airWindow?.windowScene = airWindowScene
self.airWindow?.isHidden = false
- print("AirKit - Add Screen - Done")
+ // print("AirKit - Add Screen - Done")
completion(true)
}
}
func findWindowScene(for screen: UIScreen, shouldRecurse: Bool = true, completion: @escaping (UIWindowScene?) -> ()) {
- print("AirKit - Find Window Scene")
+ // print("AirKit - Find Window Scene")
var matchingWindowScene: UIWindowScene? = nil
let scenes = UIApplication.shared.connectedScenes
for scene in scenes {
@@ -120,23 +120,23 @@ public class Air {
}
@objc func didDisconnect() {
- print("AirKit - Disconnect")
+ // print("AirKit - Disconnect")
remove()
connected = false
}
func remove() {
- print("AirKit - Remove")
+ // print("AirKit - Remove")
airWindow = nil
airScreen = nil
}
@objc func didBecomeActive() {
- print("AirKit - App Active")
+ // print("AirKit - App Active")
}
@objc func willResignActive() {
- print("AirKit - App Inactive")
+ // print("AirKit - App Inactive")
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift
index a3c90b241..0eeb7c835 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift
@@ -4,7 +4,7 @@ import SwiftUI
public extension View {
func airPlay() -> some View {
- print("AirKit - airPlay")
+ // print("AirKit - airPlay")
Air.play(AnyView(self))
return self
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/ControllerView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/ControllerView.swift
new file mode 100644
index 000000000..aaf5f758a
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/ControllerView.swift
@@ -0,0 +1,558 @@
+//
+// ControllerView.swift
+// Pomelo-V2
+//
+// Created by Stossy11 on 16/7/2024.
+//
+
+import SwiftUI
+import GameController
+import CoreMotion
+
+struct ControllerView: View {
+ // MARK: - Properties
+ @AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0
+ @AppStorage("stick-button") private var stickButton = false
+ @State private var isPortrait = true
+ @State var hideDpad = false
+ @State var hideABXY = false
+ @Environment(\.verticalSizeClass) var verticalSizeClass
+
+
+ // MARK: - Body
+ var body: some View {
+ Group {
+ let isPad = UIDevice.current.userInterfaceIdiom == .pad
+
+ if isPortrait && !isPad {
+ portraitLayout
+ } else {
+ landscapeLayout
+ }
+ }
+ .padding()
+ .onChange(of: verticalSizeClass) { _ in
+ updateOrientation()
+ }
+ .onAppear(perform: updateOrientation)
+ }
+
+ // MARK: - Layouts
+ private var portraitLayout: some View {
+ VStack {
+ Spacer()
+ VStack(spacing: 20) {
+ HStack(spacing: 30) {
+ VStack(spacing: 15) {
+ ShoulderButtonsViewLeft()
+ .padding(.vertical)
+ ZStack {
+ JoystickController(showBackground: $hideDpad)
+ DPadView()
+ .opacity(hideDpad ? 0 : 1)
+ .allowsHitTesting(!hideDpad)
+ .animation(.easeInOut(duration: 0.2), value: hideDpad)
+ }
+ }
+
+ VStack(spacing: 15) {
+ ShoulderButtonsViewRight()
+ .padding(.vertical)
+ ZStack {
+ JoystickController(iscool: true, showBackground: $hideABXY)
+ ABXYView()
+ .opacity(hideABXY ? 0 : 1)
+ .allowsHitTesting(!hideABXY)
+ .animation(.easeInOut(duration: 0.2), value: hideABXY)
+ }
+ }
+ }
+
+ HStack(spacing: 60) {
+ HStack {
+ ButtonView(button: .leftStick)
+ .padding()
+ ButtonView(button: .back)
+ }
+
+ HStack {
+ ButtonView(button: .start)
+ ButtonView(button: .rightStick)
+ .padding()
+ }
+ }
+ }
+ }
+ }
+
+ private var landscapeLayout: some View {
+ VStack {
+ Spacer()
+
+ HStack {
+ VStack(spacing: 20) {
+ ShoulderButtonsViewLeft()
+ .padding(.vertical)
+ ZStack {
+ JoystickController(showBackground: $hideDpad)
+ DPadView()
+ .opacity(hideDpad ? 0 : 1)
+ .allowsHitTesting(!hideDpad)
+ .animation(.easeInOut(duration: 0.2), value: hideDpad)
+ }
+ }
+
+ Spacer()
+
+ centerButtons
+
+ Spacer()
+
+ VStack(spacing: 20) {
+ ShoulderButtonsViewRight()
+ .padding(.vertical)
+ ZStack {
+ JoystickController(iscool: true, showBackground: $hideABXY)
+ ABXYView()
+ .opacity(hideABXY ? 0 : 1)
+ .allowsHitTesting(!hideABXY)
+ .animation(.easeInOut(duration: 0.2), value: hideABXY)
+ }
+ }
+ }
+ }
+ }
+
+ private var centerButtons: some View {
+ Group {
+ if stickButton {
+ VStack {
+ HStack(spacing: 50) {
+ ButtonView(button: .leftStick)
+ .padding()
+ Spacer()
+ ButtonView(button: .rightStick)
+ .padding()
+ }
+ .padding(.top, 30)
+
+ HStack(spacing: 50) {
+ ButtonView(button: .back)
+ Spacer()
+ ButtonView(button: .start)
+ }
+ }
+ .padding(.bottom, 20)
+ } else {
+ HStack(spacing: 50) {
+ ButtonView(button: .back)
+ Spacer()
+ ButtonView(button: .start)
+ }
+ .padding(.bottom, 20)
+ }
+ }
+ }
+
+ // MARK: - Methods
+
+ private func updateOrientation() {
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let window = windowScene.windows.first {
+ isPortrait = window.bounds.size.height > window.bounds.size.width
+ }
+ }
+}
+
+
+struct ShoulderButtonsViewLeft: View {
+ @State private var width: CGFloat = 160
+ @State private var height: CGFloat = 20
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ var body: some View {
+ HStack(spacing: 20) {
+ ButtonView(button: .leftTrigger)
+ ButtonView(button: .leftShoulder)
+ }
+ .frame(width: width, height: height)
+ .onAppear {
+ if UIDevice.current.systemName.contains("iPadOS") {
+ width *= 1.2
+ height *= 1.2
+ }
+
+ width *= CGFloat(controllerScale)
+ height *= CGFloat(controllerScale)
+ }
+ }
+}
+
+struct ShoulderButtonsViewRight: View {
+ @State private var width: CGFloat = 160
+ @State private var height: CGFloat = 20
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ var body: some View {
+ HStack(spacing: 20) {
+ ButtonView(button: .rightShoulder)
+ ButtonView(button: .rightTrigger)
+ }
+ .frame(width: width, height: height)
+ .onAppear {
+ if UIDevice.current.systemName.contains("iPadOS") {
+ width *= 1.2
+ height *= 1.2
+ }
+
+ width *= CGFloat(controllerScale)
+ height *= CGFloat(controllerScale)
+ }
+ }
+}
+
+struct DPadView: View {
+ @State private var size: CGFloat = 145
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ var body: some View {
+ VStack(spacing: 7) {
+ ButtonView(button: .dPadUp)
+ HStack(spacing: 22) {
+ ButtonView(button: .dPadLeft)
+ Spacer(minLength: 22)
+ ButtonView(button: .dPadRight)
+ }
+ ButtonView(button: .dPadDown)
+ }
+ .frame(width: size, height: size)
+ .onAppear {
+ if UIDevice.current.systemName.contains("iPadOS") {
+ size *= 1.2
+ }
+
+ size *= CGFloat(controllerScale)
+ }
+ }
+}
+
+struct ABXYView: View {
+ @State private var size: CGFloat = 145
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ var body: some View {
+ VStack(spacing: 7) {
+ ButtonView(button: .X)
+ HStack(spacing: 22) {
+ ButtonView(button: .Y)
+ Spacer(minLength: 22)
+ ButtonView(button: .A)
+ }
+ ButtonView(button: .B)
+ }
+ .frame(width: size, height: size)
+ .onAppear {
+ if UIDevice.current.systemName.contains("iPadOS") {
+ size *= 1.2
+ }
+
+ size *= CGFloat(controllerScale)
+ }
+ }
+}
+
+
+struct ButtonView: View {
+ var button: VirtualControllerButton
+
+ @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+ @Environment(\.presentationMode) var presentationMode
+
+ @AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState()
+ @State private var istoggle = false
+
+ @State private var isPressed = false
+ @State private var toggleState = false
+
+ @State private var size: CGSize = .zero
+
+ var body: some View {
+ Circle()
+ .foregroundStyle(.clear.opacity(0))
+ .overlay {
+ Image(systemName: buttonConfig.iconName)
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width, height: size.height)
+ .foregroundStyle(.white)
+ .opacity(isPressed ? 0.6 : 0.8)
+ .allowsHitTesting(false)
+ }
+ .frame(width: size.width, height: size.height)
+ .background(
+ buttonBackground
+ )
+ .gesture(
+ DragGesture(minimumDistance: 0)
+ .onChanged { _ in handleButtonPress() }
+ .onEnded { _ in handleButtonRelease() }
+ )
+ .onAppear {
+ istoggle = (toggleButtons.toggle1 && button == .A) || (toggleButtons.toggle2 && button == .B) || (toggleButtons.toggle3 && button == .X) || (toggleButtons.toggle4 && button == .Y)
+ size = calculateButtonSize()
+ }
+ .onChange(of: controllerScale) { _ in
+ size = calculateButtonSize()
+ }
+ }
+
+ private var buttonBackground: some View {
+ Group {
+ if !button.isTrigger && button != .leftStick && button != .rightStick {
+ Circle()
+ .fill(Color.gray.opacity(0.4))
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ } else if button == .leftStick || button == .rightStick {
+ Image(systemName: buttonConfig.iconName)
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ .foregroundColor(Color.gray.opacity(0.4))
+ } else if button.isTrigger {
+ Image(systemName: convertTriggerIconToButton(buttonConfig.iconName))
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ .foregroundColor(Color.gray.opacity(0.4))
+ }
+ }
+ }
+
+ private func convertTriggerIconToButton(_ iconName: String) -> String {
+ if iconName.hasPrefix("zl") || iconName.hasPrefix("zr") {
+ var converted = String(iconName.dropFirst(3))
+ converted = converted.replacingOccurrences(of: "rectangle", with: "button")
+ converted = converted.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
+ return converted
+ } else {
+ var converted = String(iconName.dropFirst(2))
+ converted = converted.replacingOccurrences(of: "rectangle", with: "button")
+ converted = converted.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
+ return converted
+ }
+ }
+
+ private func handleButtonPress() {
+ guard !isPressed || istoggle else { return }
+
+ if istoggle {
+ toggleState.toggle()
+ isPressed = toggleState
+ let value = toggleState ? 1 : 0
+ Ryujinx.shared.virtualController.setButtonState(Uint8(value), for: button)
+ Haptics.shared.play(.medium)
+ } else {
+ isPressed = true
+ Ryujinx.shared.virtualController.setButtonState(1, for: button)
+ Haptics.shared.play(.medium)
+ }
+ }
+
+ private func handleButtonRelease() {
+ if istoggle { return }
+
+ guard isPressed else { return }
+
+ isPressed = false
+ DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.05) {
+ Ryujinx.shared.virtualController.setButtonState(0, for: button)
+ }
+ }
+
+ private func calculateButtonSize() -> CGSize {
+ let baseWidth: CGFloat
+ let baseHeight: CGFloat
+
+ if button.isTrigger {
+ baseWidth = 70
+ baseHeight = 40
+ } else if button.isSmall {
+ baseWidth = 35
+ baseHeight = 35
+ } else {
+ baseWidth = 45
+ baseHeight = 45
+ }
+
+ let deviceMultiplier = UIDevice.current.userInterfaceIdiom == .pad ? 1.2 : 1.0
+ let scaleMultiplier = CGFloat(controllerScale)
+
+ return CGSize(
+ width: baseWidth * deviceMultiplier * scaleMultiplier,
+ height: baseHeight * deviceMultiplier * scaleMultiplier
+ )
+ }
+
+ // Centralized button configuration
+ private var buttonConfig: ButtonConfiguration {
+ switch button {
+ case .A:
+ return ButtonConfiguration(iconName: "a.circle.fill")
+ case .B:
+ return ButtonConfiguration(iconName: "b.circle.fill")
+ case .X:
+ return ButtonConfiguration(iconName: "x.circle.fill")
+ case .Y:
+ return ButtonConfiguration(iconName: "y.circle.fill")
+ case .leftStick:
+ return ButtonConfiguration(iconName: "l.joystick.press.down.fill")
+ case .rightStick:
+ return ButtonConfiguration(iconName: "r.joystick.press.down.fill")
+ case .dPadUp:
+ return ButtonConfiguration(iconName: "arrowtriangle.up.circle.fill")
+ case .dPadDown:
+ return ButtonConfiguration(iconName: "arrowtriangle.down.circle.fill")
+ case .dPadLeft:
+ return ButtonConfiguration(iconName: "arrowtriangle.left.circle.fill")
+ case .dPadRight:
+ return ButtonConfiguration(iconName: "arrowtriangle.right.circle.fill")
+ case .leftTrigger:
+ return ButtonConfiguration(iconName: "zl.rectangle.roundedtop.fill")
+ case .rightTrigger:
+ return ButtonConfiguration(iconName: "zr.rectangle.roundedtop.fill")
+ case .leftShoulder:
+ return ButtonConfiguration(iconName: "l.rectangle.roundedbottom.fill")
+ case .rightShoulder:
+ return ButtonConfiguration(iconName: "r.rectangle.roundedbottom.fill")
+ case .start:
+ return ButtonConfiguration(iconName: "plus.circle.fill")
+ case .back:
+ return ButtonConfiguration(iconName: "minus.circle.fill")
+ case .guide:
+ return ButtonConfiguration(iconName: "house.circle.fill")
+ }
+ }
+
+ struct ButtonConfiguration {
+ let iconName: String
+ }
+}
+
+
+struct ExtButtonIconView: View {
+ var button: VirtualControllerButton
+ var opacity = 0.8
+
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+ @State private var size: CGSize = .zero
+
+ var body: some View {
+ Circle()
+ .foregroundStyle(.clear.opacity(0))
+ .overlay {
+ Image(systemName: buttonConfig.iconName)
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width / 1.5, height: size.height / 1.5)
+ .foregroundStyle(.white)
+ .opacity(opacity)
+ .allowsHitTesting(false)
+ }
+ .frame(width: size.width, height: size.height)
+ .background(
+ buttonBackground
+ )
+ .onAppear {
+ size = calculateButtonSize()
+ }
+ .onChange(of: controllerScale) { _ in
+ size = calculateButtonSize()
+ }
+ }
+
+ private var buttonBackground: some View {
+ Group {
+ if !button.isTrigger && button != .leftStick && button != .rightStick {
+ Circle()
+ .fill(Color.gray.opacity(0.3))
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ } else if button == .leftStick || button == .rightStick {
+ Image(systemName: buttonConfig.iconName)
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ .foregroundColor(Color.gray.opacity(0.4))
+ } else if button.isTrigger {
+ Image(systemName: convertTriggerIconToButton(buttonConfig.iconName))
+ .resizable()
+ .scaledToFit()
+ .frame(width: size.width * 1.25, height: size.height * 1.25)
+ .foregroundColor(Color.gray.opacity(0.4))
+ }
+ }
+ }
+
+ private func convertTriggerIconToButton(_ iconName: String) -> String {
+ var converted = iconName
+ if iconName.hasPrefix("zl") || iconName.hasPrefix("zr") {
+ converted = String(iconName.dropFirst(3))
+ } else {
+ converted = String(iconName.dropFirst(2))
+ }
+ converted = converted
+ .replacingOccurrences(of: "rectangle", with: "button")
+ .replacingOccurrences(of: ".fill", with: ".horizontal.fill")
+ return converted
+ }
+
+ private func calculateButtonSize() -> CGSize {
+ let baseWidth: CGFloat
+ let baseHeight: CGFloat
+
+ if button.isTrigger {
+ baseWidth = 70
+ baseHeight = 40
+ } else if button.isSmall {
+ baseWidth = 35
+ baseHeight = 35
+ } else {
+ baseWidth = 45
+ baseHeight = 45
+ }
+
+ let deviceMultiplier = UIDevice.current.userInterfaceIdiom == .pad ? 1.2 : 1.0
+ let scaleMultiplier = CGFloat(controllerScale)
+
+ return CGSize(
+ width: baseWidth * deviceMultiplier * scaleMultiplier,
+ height: baseHeight * deviceMultiplier * scaleMultiplier
+ )
+ }
+
+ private var buttonConfig: ButtonConfiguration {
+ switch button {
+ case .A: return .init(iconName: "a.circle.fill")
+ case .B: return .init(iconName: "b.circle.fill")
+ case .X: return .init(iconName: "x.circle.fill")
+ case .Y: return .init(iconName: "y.circle.fill")
+ case .leftStick: return .init(iconName: "l.joystick.press.down.fill")
+ case .rightStick: return .init(iconName: "r.joystick.press.down.fill")
+ case .dPadUp: return .init(iconName: "arrowtriangle.up.circle.fill")
+ case .dPadDown: return .init(iconName: "arrowtriangle.down.circle.fill")
+ case .dPadLeft: return .init(iconName: "arrowtriangle.left.circle.fill")
+ case .dPadRight: return .init(iconName: "arrowtriangle.right.circle.fill")
+ case .leftTrigger: return .init(iconName: "zl.rectangle.roundedtop.fill")
+ case .rightTrigger: return .init(iconName: "zr.rectangle.roundedtop.fill")
+ case .leftShoulder: return .init(iconName: "l.rectangle.roundedbottom.fill")
+ case .rightShoulder: return .init(iconName: "r.rectangle.roundedbottom.fill")
+ case .start: return .init(iconName: "plus.circle.fill")
+ case .back: return .init(iconName: "minus.circle.fill")
+ case .guide: return .init(iconName: "gearshape.fill")
+ }
+ }
+
+ struct ButtonConfiguration {
+ let iconName: String
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Haptics/Haptics.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Haptics/Haptics.swift
similarity index 95%
rename from src/MeloNX/MeloNX/App/Views/Main/ControllerView/Haptics/Haptics.swift
rename to src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Haptics/Haptics.swift
index 5dd555815..4409a4da2 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Haptics/Haptics.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Haptics/Haptics.swift
@@ -15,7 +15,6 @@ class Haptics {
private init() { }
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
- print("haptics")
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/Joystick.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/Joystick.swift
new file mode 100644
index 000000000..67a7615f5
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/Joystick.swift
@@ -0,0 +1,93 @@
+//
+// Joystick.swift
+// MeloNX
+//
+// Created by Stossy11 on 21/03/2025.
+//
+
+
+import SwiftUI
+
+struct Joystick: View {
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ @Binding var position: CGPoint
+ @State var joystickSize: CGFloat
+ var boundarySize: CGFloat
+
+ @State private var offset: CGSize = .zero
+ @Binding var showBackground: Bool
+
+ let sensitivity: CGFloat = 1.2
+
+
+ var dragGesture: some Gesture {
+ DragGesture()
+ .onChanged { value in
+ withAnimation(.easeIn) {
+ showBackground = true
+ }
+
+ let translation = value.translation
+ let distance = sqrt(translation.width * translation.width + translation.height * translation.height)
+ let maxRadius = (boundarySize - joystickSize) / 2
+ let extendedRadius = maxRadius + (joystickSize / 2)
+
+ if distance <= extendedRadius {
+ offset = translation
+ } else {
+ let angle = atan2(translation.height, translation.width)
+ offset = CGSize(width: cos(angle) * extendedRadius, height: sin(angle) * extendedRadius)
+ }
+
+ position = CGPoint(
+ x: max(-1, min(1, (offset.width / extendedRadius) * sensitivity)),
+ y: max(-1, min(1, (offset.height / extendedRadius) * sensitivity))
+ )
+ }
+ .onEnded { _ in
+ offset = .zero
+ position = .zero
+ withAnimation(.easeOut) {
+ showBackground = false
+ }
+ }
+ }
+
+ var body: some View {
+ ZStack {
+ Circle()
+ .fill(Color.clear.opacity(0))
+ .frame(width: boundarySize, height: boundarySize)
+ .scaleEffect(controllerScale)
+
+ if showBackground {
+ Circle()
+ .fill(Color.gray.opacity(0.4))
+ .frame(width: boundarySize, height: boundarySize)
+ .animation(.easeInOut(duration: 0.1), value: showBackground)
+ .scaleEffect(controllerScale)
+ }
+
+ Circle()
+ .fill(Color.white.opacity(0.5))
+ .frame(width: joystickSize, height: joystickSize)
+ .background(
+ Circle()
+ .fill(Color.gray.opacity(0.3))
+ .frame(width: joystickSize * 1.25, height: joystickSize * 1.25)
+ )
+ .offset(offset)
+ .gesture(dragGesture)
+ .scaleEffect(controllerScale)
+ }
+ .frame(width: boundarySize, height: boundarySize)
+ .onChange(of: showBackground) { newValue in
+ if newValue {
+ joystickSize *= 1.4
+ } else {
+ joystickSize = (boundarySize * 0.2)
+ }
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/JoystickView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/JoystickView.swift
new file mode 100644
index 000000000..2459c6a7c
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/ControllerView/Joystick/JoystickView.swift
@@ -0,0 +1,39 @@
+//
+// JoystickView.swift
+// Pomelo
+//
+// Created by Stossy11 on 30/9/2024.
+// Copyright © 2024 Stossy11. All rights reserved.
+//
+
+import SwiftUI
+
+struct JoystickController: View {
+ @State var iscool: Bool? = nil
+ @Environment(\.colorScheme) var colorScheme
+ @Binding var showBackground: Bool
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+ @State var position: CGPoint = CGPoint(x: 0, y: 0)
+ var dragDiameter: CGFloat {
+ var selfs = CGFloat(160)
+ // selfs *= controllerScale
+ if UIDevice.current.systemName.contains("iPadOS") {
+ return selfs * 1.2
+ }
+
+ return selfs
+ }
+
+ public var body: some View {
+ VStack {
+ Joystick(position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground)
+ .onChange(of: position) { newValue in
+ if iscool != nil {
+ Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
+ } else {
+ Ryujinx.shared.virtualController.thumbstickMoved(.left, x: newValue.x, y: newValue.y)
+ }
+ }
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift
index 5e1a3279e..58a2b6d49 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift
@@ -14,11 +14,20 @@ struct EmulationView: View {
@AppStorage("showScreenShotButton") var ssb: Bool = false
@AppStorage("showlogsgame") var showlogsgame: Bool = false
+ @AppStorage("On-ScreenControllerOpacity") var controllerOpacity: Double = 1.0
+
+ @AppStorage("disableTouch") var blackScreen = false
+
@State var isPresentedThree: Bool = false
@State var isAirplaying = Air.shared.connected
@Binding var startgame: Game?
@Environment(\.scenePhase) var scenePhase
+ @State private var isInBackground = false
+ @State var showSettings = false
+ @State var pauseEmu = true
+ @AppStorage("location-enabled") var locationenabled: Bool = false
+
var body: some View {
ZStack {
if isAirplaying {
@@ -26,8 +35,13 @@ struct EmulationView: View {
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
.onAppear {
- Air.play(AnyView(MetalView().ignoresSafeArea()))
+ Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
}
+
+ Color.black
+ .ignoresSafeArea()
+ .edgesIgnoringSafeArea(.all)
+ .allowsHitTesting(false)
} else {
MetalView() // The Emulation View
.ignoresSafeArea()
@@ -38,6 +52,8 @@ struct EmulationView: View {
if isVCA {
ControllerView() // Virtual Controller
+ .opacity(controllerOpacity)
+ .allowsHitTesting(true)
}
Group {
@@ -62,38 +78,98 @@ struct EmulationView: View {
Spacer()
}
- Spacer()
if ssb {
HStack {
- Button {
- if let screenshot = Ryujinx.shared.emulationUIView?.screenshot() {
- UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
+ Menu {
+
+ /*
+ Button {
+ showSettings.toggle()
+
+ } label: {
+ Label {
+ Text("Game Settings")
+ } icon: {
+ Image(systemName: "gearshape.circle")
+ }
+ }
+ */
+
+ Button {
+ pause_emulation(pauseEmu)
+ pauseEmu.toggle()
+ } label: {
+ Label {
+ Text(pauseEmu ? "Pause" : "Play")
+ } icon: {
+ Image(systemName: pauseEmu ? "pause.circle" : "play.circle")
+ }
+ }
+
+ Button(role: .destructive) {
+ startgame = nil
+ stop_emulation()
+ try? Ryujinx.shared.stop()
+ } label: {
+ Label {
+ Text("Exit (Unstable)")
+ } icon: {
+ Image(systemName: "x.circle")
+ }
}
} label: {
- Image(systemName: "square.and.arrow.up")
+ ExtButtonIconView(button: .guide, opacity: 0.4)
}
- .frame(width: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45, height: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45)
.padding()
Spacer()
-
-
-
+
}
}
+ Spacer()
+
}
}
}
.onAppear {
+ LocationManager.sharedInstance.startUpdatingLocation()
Air.shared.connectionCallbacks.append { cool in
DispatchQueue.main.async {
isAirplaying = cool
+ // print(cool)
+ }
+ }
+
+ RegisterCallback("exit-emulation") { cool in
+ DispatchQueue.main.async {
print(cool)
+ startgame = nil
+ stop_emulation()
+ try? Ryujinx.shared.stop()
}
}
}
+ .onChange(of: scenePhase) { newPhase in
+ // Detect when the app enters the background
+ if newPhase == .background {
+ pause_emulation(true)
+ isInBackground = true
+ } else if newPhase == .active {
+ pause_emulation(false)
+ isInBackground = false
+ } else if newPhase == .inactive {
+ pause_emulation(true)
+ isInBackground = true
+ }
+ }
+ .sheet(isPresented: $showSettings) {
+ // PerGameSettingsView(titleId: startgame?.titleId ?? "", manager: InGameSettingsManager.shared)
+ // .onDisappear() {
+ // InGameSettingsManager.shared.saveSettings()
+ // }
+ }
}
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/InGameSettingsManager/InGameSettingsManager.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/InGameSettingsManager/InGameSettingsManager.swift
new file mode 100644
index 000000000..acbb15195
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/InGameSettingsManager/InGameSettingsManager.swift
@@ -0,0 +1,61 @@
+//
+// InGameSettingsManager.swift
+// MeloNX
+//
+// Created by Stossy11 on 12/06/2025.
+//
+
+import Foundation
+
+class InGameSettingsManager: PerGameSettingsManaging {
+ @Published var config: [String: Ryujinx.Arguments]
+
+ private var saveWorkItem: DispatchWorkItem?
+
+ public static var shared = InGameSettingsManager()
+
+ private init() {
+ self.config = PerGameSettingsManager.loadSettings() ?? [:]
+ }
+
+ func debouncedSave() {
+ saveWorkItem?.cancel()
+
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ self.saveSettings()
+ }
+
+ saveWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
+ }
+
+ func saveSettings() {
+ if let currentgame = Ryujinx.shared.games.first(where: { $0.fileURL == URL(string: Ryujinx.shared.config?.gamepath ?? "") }) {
+ Ryujinx.shared.config = config[currentgame.titleId]
+ let args = Ryujinx.shared.buildCommandLineArgs(from: config[currentgame.titleId] ?? Ryujinx.Arguments())
+
+ let cArgs = args.map { strdup($0) }
+ defer { cArgs.forEach { free($0) } }
+ var argvPtrs = cArgs
+
+ let result = update_settings_external(Int32(args.count), &argvPtrs)
+
+ print(result)
+ }
+ }
+
+ static func loadSettings() -> [String: Ryujinx.Arguments]? {
+ var cool: [String: Ryujinx.Arguments] = [:]
+ if let currentgame = Ryujinx.shared.games.first(where: { $0.fileURL == URL(string: Ryujinx.shared.config?.gamepath ?? "") }) {
+ cool[currentgame.titleId] = Ryujinx.shared.config
+ return cool
+ } else {
+ return nil
+ }
+ }
+
+ func loadSettings() {
+ self.config = PerGameSettingsManager.loadSettings() ?? [:]
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/PerformanceDisplay/PerformanceOverlay.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/PerformanceDisplay/PerformanceOverlay.swift
new file mode 100644
index 000000000..5573e1282
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/PerformanceDisplay/PerformanceOverlay.swift
@@ -0,0 +1,70 @@
+//
+// Untitled.swift
+// MeloNX
+//
+// Created by Stossy11 on 21/12/2024.
+//
+
+import SwiftUI
+
+struct PerformanceOverlayView: View {
+ @StateObject private var memorymonitor = MemoryUsageMonitor()
+
+ @StateObject private var fpsmonitor = FPSMonitor()
+
+ var body: some View {
+ VStack {
+ Text("\(fpsmonitor.formatFPS())")
+ .foregroundStyle(.white)
+ .stroke(color: .black, width: 2)
+ Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage))
+ .foregroundStyle(.white)
+ .stroke(color: .black, width: 2)
+ }
+ }
+}
+
+extension View {
+ func stroke(color: Color, width: CGFloat = 1) -> some View {
+ modifier(StrokeModifier(strokeSize: width, strokeColor: color))
+ }
+}
+
+struct StrokeModifier: ViewModifier {
+ private let id = UUID()
+ var strokeSize: CGFloat = 1
+ var strokeColor: Color = .blue
+
+ func body(content: Content) -> some View {
+ if strokeSize > 0 {
+ appliedStrokeBackground(content: content)
+ } else {
+ content
+ }
+ }
+
+ private func appliedStrokeBackground(content: Content) -> some View {
+ content
+ .padding(strokeSize*2)
+ .background(
+ Rectangle()
+ .foregroundColor(strokeColor)
+ .mask(alignment: .center) {
+ mask(content: content)
+ }
+ )
+ }
+
+ func mask(content: Content) -> some View {
+ Canvas { context, size in
+ context.addFilter(.alphaThreshold(min: 0.01))
+ if let resolvedView = context.resolveSymbol(id: id) {
+ context.draw(resolvedView, at: .init(x: size.width/2, y: size.height/2))
+ }
+ } symbols: {
+ content
+ .tag(id)
+ .blur(radius: strokeSize)
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift
index 6c4625e19..25d296012 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift
@@ -9,10 +9,10 @@ import MetalKit
import UIKit
class MeloMTKView: MTKView {
-
private var activeTouches: [UITouch] = []
private var ignoredTouches: Set = []
-
+ private var touchIndexMap: [UITouch: Int] = [:]
+
private let baseWidth: CGFloat = 1280
private let baseHeight: CGFloat = 720
private var aspectRatio: AspectRatio = .fixed16x9
@@ -84,71 +84,112 @@ class MeloMTKView: MTKView {
return CGPoint(x: scaledX, y: scaledY)
}
+ private func getNextAvailableIndex() -> Int {
+ for i in 0.., with event: UIEvent?) {
super.touchesBegan(touches, with: event)
+
+ let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
+ guard !disabled else { return }
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
-
+
for touch in touches {
let location = touch.location(in: self)
- if scaleToTargetResolution(location) == nil {
+ guard let scaledLocation = scaleToTargetResolution(location) else {
ignoredTouches.insert(touch)
continue
}
+ let index = getNextAvailableIndex()
+ touchIndexMap[touch] = index
activeTouches.append(touch)
- let index = activeTouches.firstIndex(of: touch)!
- let scaledLocation = scaleToTargetResolution(location)!
- print("Touch began at: \(scaledLocation) and \(self.aspectRatio)")
touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
}
}
override func touchesEnded(_ touches: Set, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
-
- setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
-
- for touch in touches {
- if ignoredTouches.contains(touch) {
+
+ let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
+ guard !disabled else {
+ for touch in touches {
ignoredTouches.remove(touch)
+ if let index = activeTouches.firstIndex(of: touch) {
+ activeTouches.remove(at: index)
+ }
+ touchIndexMap.removeValue(forKey: touch)
+ }
+ return
+ }
+
+ for touch in touches {
+ if ignoredTouches.remove(touch) != nil {
continue
}
- if let index = activeTouches.firstIndex(of: touch) {
- activeTouches.remove(at: index)
+ if let touchIndex = touchIndexMap[touch] {
+ touch_ended(Int32(touchIndex))
- print("Touch ended for index \(index)")
- touch_ended(Int32(index))
+ if let arrayIndex = activeTouches.firstIndex(of: touch) {
+ activeTouches.remove(at: arrayIndex)
+ }
+ touchIndexMap.removeValue(forKey: touch)
}
}
}
override func touchesMoved(_ touches: Set, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
+
+ let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
+ guard !disabled else { return }
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
-
+
for touch in touches {
if ignoredTouches.contains(touch) {
continue
}
- let location = touch.location(in: self)
- guard let scaledLocation = scaleToTargetResolution(location) else {
- if let index = activeTouches.firstIndex(of: touch) {
- activeTouches.remove(at: index)
- print("Touch left active area, removed index \(index)")
- touch_ended(Int32(index))
- }
+ guard let touchIndex = touchIndexMap[touch] else {
continue
}
-
- if let index = activeTouches.firstIndex(of: touch) {
- print("Touch moved to: \(scaledLocation)")
- touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
+
+ let location = touch.location(in: self)
+ guard let scaledLocation = scaleToTargetResolution(location) else {
+ touch_ended(Int32(touchIndex))
+
+ if let arrayIndex = activeTouches.firstIndex(of: touch) {
+ activeTouches.remove(at: arrayIndex)
+ }
+ touchIndexMap.removeValue(forKey: touch)
+ ignoredTouches.insert(touch)
+ continue
}
+
+ touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(touchIndex))
}
}
+
+ override func touchesCancelled(_ touches: Set, with event: UIEvent?) {
+ super.touchesCancelled(touches, with: event)
+ touchesEnded(touches, with: event)
+ }
+
+
+ func resetTouchTracking() {
+ activeTouches.removeAll()
+ ignoredTouches.removeAll()
+ touchIndexMap.removeAll()
+ }
}
+
diff --git a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift
deleted file mode 100644
index 6670b20f9..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift
+++ /dev/null
@@ -1,512 +0,0 @@
-//
-// GameListView.swift
-// MeloNX
-//
-// Created by Stossy11 on 3/11/2024.
-//
-
-import SwiftUI
-import UniformTypeIdentifiers
-
-extension UTType {
- static let nsp = UTType(exportedAs: "com.nintendo.switch-package")
- static let xci = UTType(exportedAs: "com.nintendo.switch-cartridge")
-}
-
-struct GameLibraryView: View {
- @Binding var startemu: Game?
- // @State var importDLCs = false
- @State private var searchText = ""
- @State private var isSearching = false
- @AppStorage("recentGames") private var recentGamesData: Data = Data()
- @State private var recentGames: [Game] = []
- @Environment(\.colorScheme) var colorScheme
- @State var firmwareInstaller = false
- @State var firmwareversion = "0"
- @State var isImporting: Bool = false
- @State var startgame = false
- @State var isSelectingGameFile = false
- @State var isViewingGameInfo: Bool = false
- @State var isSelectingGameUpdate: Bool = false
- @State var isSelectingGameDLC: Bool = false
- @StateObject var ryujinx = Ryujinx.shared
- @State var gameInfo: Game?
- var games: Binding<[Game]> {
- Binding(
- get: { Ryujinx.shared.games },
- set: { Ryujinx.shared.games = $0 }
- )
- }
-
- var filteredGames: [Game] {
- if searchText.isEmpty {
- return Ryujinx.shared.games.filter { game in
- !realRecentGames.contains(where: { $0.fileURL == game.fileURL })
- }
- }
- return Ryujinx.shared.games.filter {
- $0.titleName.localizedCaseInsensitiveContains(searchText) ||
- $0.developer.localizedCaseInsensitiveContains(searchText)
- }
- }
-
- var realRecentGames: [Game] {
- let games = Ryujinx.shared.games
- return recentGames.compactMap { recentGame in
- games.first(where: { $0.fileURL == recentGame.fileURL })
- }
- }
-
- var body: some View {
- iOSNav {
- List {
- if Ryujinx.shared.games.isEmpty {
- VStack(spacing: 16) {
- Image(systemName: "gamecontroller.fill")
- .font(.system(size: 64))
- .foregroundColor(.secondary.opacity(0.7))
- .padding(.top, 60)
- Text("No Games Found")
- .font(.title2.bold())
- .foregroundColor(.primary)
- Text("Add ROM, Keys and Firmware to get started")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- .frame(maxWidth: .infinity)
- .padding(.top, 40)
- } else {
- if !isSearching && !realRecentGames.isEmpty {
- Section {
- ForEach(realRecentGames) { game in
- GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
- .swipeActions(edge: .trailing, allowsFullSwipe: true) {
- Button(role: .destructive) {
- removeFromRecentGames(game)
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- }
- } header: {
- Text("Recent")
- }
-
- Section {
- ForEach(filteredGames) { game in
- GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
- }
- } header: {
- Text("Others")
- }
- } else {
- ForEach(filteredGames) { game in
- GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
- }
- }
- }
- }
- .navigationTitle("Games")
- .navigationBarTitleDisplayMode(.large)
- .onAppear {
- loadRecentGames()
-
- let firmware = Ryujinx.shared.fetchFirmwareVersion()
- firmwareversion = (firmware == "" ? "0" : firmware)
- }
- .fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
- switch result {
- case .success(let url):
- do {
- let fun = url.startAccessingSecurityScopedResource()
- let path = url.path
-
- Ryujinx.shared.installFirmware(firmwarePath: path)
-
- firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
- if fun {
- url.stopAccessingSecurityScopedResource()
- }
- }
- case .failure(let error):
- print(error)
- }
- }
- .toolbar {
- ToolbarItem(placement: .topBarTrailing) {
- Button {
- isSelectingGameFile = true
-
- isImporting = true
- } label: {
- Image(systemName: "plus")
- }
- }
-
- ToolbarItem(placement: .topBarLeading) {
- Menu {
- Text("Firmware Version: \(firmwareversion)")
- .tint(.white)
-
- if firmwareversion == "0" {
- Button {
- DispatchQueue.main.async {
- firmwareInstaller.toggle()
- }
- } label: {
- Text("Install Firmware")
- }
-
- } else {
- Menu("Firmware") {
- Button {
- Ryujinx.shared.removeFirmware()
- let firmware = Ryujinx.shared.fetchFirmwareVersion()
- firmwareversion = (firmware == "" ? "0" : firmware)
- } label: {
- Text("Remove Firmware")
- }
-
- Button {
- let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
-
- self.startemu = game
- } label: {
- Text("Mii Maker")
- }
- }
- }
-
- Button {
- isSelectingGameFile = false
-
- isImporting = true
- } label: {
- Text("Open Game")
- }
-
- Button {
- let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
- var sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
- if ProcessInfo.processInfo.isiOSAppOnMac {
- sharedurl = documentsUrl.absoluteString
- }
- print(sharedurl)
- let furl = URL(string: sharedurl)!
- if UIApplication.shared.canOpenURL(furl) {
- UIApplication.shared.open(furl, options: [:])
- }
- } label: {
- Text("Show MeloNX Folder")
- }
- } label: {
- Image(systemName: "ellipsis.circle")
- .foregroundColor(.blue)
- }
- }
-
- ToolbarItem(placement: .topBarLeading) {
- if ryujinx.jitenabled {
- Image(systemName: "checkmark")
- .foregroundStyle(.green)
- }
- }
- }
- .onChange(of: startemu) { game in
- guard let game else { return }
- addToRecentGames(game)
- }
- }
- .searchable(text: $searchText)
- .animation(.easeInOut, value: searchText)
- .onChange(of: searchText) { _ in
- isSearching = !searchText.isEmpty
- }
- .fileImporter(isPresented: $isImporting, allowedContentTypes: [.folder, .nsp, .xci, .zip, .item]) { result in
- if isSelectingGameFile {
- switch result {
- case .success(let url):
- guard url.startAccessingSecurityScopedResource() else {
- print("Failed to access security-scoped resource")
- return
- }
- defer { url.stopAccessingSecurityScopedResource() }
-
- do {
- let fileManager = FileManager.default
- let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
- let romsDirectory = documentsDirectory.appendingPathComponent("roms")
-
- if !fileManager.fileExists(atPath: romsDirectory.path) {
- try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
- }
-
- let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
- try fileManager.copyItem(at: url, to: destinationURL)
-
- Ryujinx.shared.games = Ryujinx.shared.loadGames()
- } catch {
- print("Error copying game file: \(error)")
- }
- case .failure(let err):
- print("File import failed: \(err.localizedDescription)")
- }
-
- } else {
-
- switch result {
- case .success(let url):
- guard url.startAccessingSecurityScopedResource() else {
- print("Failed to access security-scoped resource")
- return
- }
-
- do {
- let handle = try FileHandle(forReadingFrom: url)
- let fileExtension = (url.pathExtension as NSString).utf8String
- let extensionPtr = UnsafeMutablePointer(mutating: fileExtension)
-
- let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
-
- let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
-
- DispatchQueue.main.async {
- startemu = game
- }
- } catch {
- print(error)
- }
-
- case .failure(let err):
- print("File import failed: \(err.localizedDescription)")
- }
- }
- }
- .sheet(isPresented: $isSelectingGameUpdate) {
- UpdateManagerSheet(game: $gameInfo)
- }
- .sheet(isPresented: $isSelectingGameDLC) {
- DLCManagerSheet(game: $gameInfo)
- }
- .sheet(isPresented: Binding(
- get: { isViewingGameInfo && gameInfo != nil },
- set: { newValue in
- if !newValue {
- isViewingGameInfo = false
- gameInfo = nil
- }
- }
- )) {
- if let game = gameInfo {
- GameInfoSheet(game: game)
- }
- }
- }
-
- private func addToRecentGames(_ game: Game) {
- recentGames.removeAll { $0.titleId == game.titleId }
-
- recentGames.insert(game, at: 0)
-
- if recentGames.count > 5 {
- recentGames = Array(recentGames.prefix(5))
- }
-
- saveRecentGames()
- }
-
- private func removeFromRecentGames(_ game: Game) {
- recentGames.removeAll { $0.titleId == game.titleId }
- saveRecentGames()
- }
-
- private func saveRecentGames() {
- do {
- let encoder = JSONEncoder()
- let data = try encoder.encode(recentGames)
- recentGamesData = data
- } catch {
- print("Error saving recent games: \(error)")
- }
- }
-
- private func loadRecentGames() {
- do {
- let decoder = JSONDecoder()
- recentGames = try decoder.decode([Game].self, from: recentGamesData)
- } catch {
- print("Error loading recent games: \(error)")
- recentGames = []
- }
- }
-
- // MARK: - Delete Game Function
- func deleteGame(game: Game) {
- let fileManager = FileManager.default
- do {
- try fileManager.removeItem(at: game.fileURL)
- Ryujinx.shared.games.removeAll { $0.id == game.id }
- Ryujinx.shared.games = Ryujinx.shared.loadGames()
- } catch {
- print("Error deleting game: \(error)")
- }
- }
-}
-
-// MARK: - Game Model
-extension Game: Codable {
- enum CodingKeys: String, CodingKey {
- case titleName, titleId, developer, version, fileURL
- }
-
- public init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
- titleName = try container.decode(String.self, forKey: .titleName)
- titleId = try container.decode(String.self, forKey: .titleId)
- developer = try container.decode(String.self, forKey: .developer)
- version = try container.decode(String.self, forKey: .version)
- fileURL = try container.decode(URL.self, forKey: .fileURL)
-
- // Initialize other properties
- self.containerFolder = fileURL.deletingLastPathComponent()
- self.fileType = .item
- }
-
- public func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(titleName, forKey: .titleName)
- try container.encode(titleId, forKey: .titleId)
- try container.encode(developer, forKey: .developer)
- try container.encode(version, forKey: .version)
- try container.encode(fileURL, forKey: .fileURL)
- }
-}
-
-// MARK: - Game List Item
-struct GameListRow: View {
- let game: Game
- @Binding var startemu: Game?
- @Binding var games: [Game] // Add this binding
- @Binding var isViewingGameInfo: Bool
- @Binding var isSelectingGameUpdate: Bool
- @Binding var isSelectingGameDLC: Bool
- @Binding var gameInfo: Game?
- @State var gametoDelete: Game?
- @State var showGameDeleteConfirmation: Bool = false
- @Environment(\.colorScheme) var colorScheme
-
- @AppStorage("portal") var gamepo = false
-
- var body: some View {
- Button(action: {
- startemu = game
- }) {
- HStack(spacing: 16) {
- // Game Icon
- if let icon = game.icon {
- Image(uiImage: icon)
- .resizable()
- .aspectRatio(contentMode: .fill)
- .frame(width: 45, height: 45)
- .cornerRadius(8)
- } else {
- ZStack {
- RoundedRectangle(cornerRadius: 8)
- .fill(colorScheme == .dark ?
- Color(.systemGray5) : Color(.systemGray6))
- .frame(width: 45, height: 45)
-
- Image(systemName: "gamecontroller.fill")
- .font(.system(size: 20))
- .foregroundColor(.gray)
- }
- }
-
- // Game Info
- VStack(alignment: .leading, spacing: 2) {
- Text(game.titleName)
- .font(.body)
- .foregroundColor(.primary)
-
- Text(game.developer)
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
-
- Spacer()
-
- Image(systemName: "play.circle.fill")
- .font(.title2)
- .foregroundColor(.accentColor)
- .opacity(0.8)
- }
- }
- .contextMenu {
- Section {
- Button {
- startemu = game
- } label: {
- Label("Play Now", systemImage: "play.fill")
- }
-
- Button {
- gameInfo = game
- isViewingGameInfo.toggle()
-
- if game.titleName.lowercased() == "portal" {
- gamepo = true
- } else if game.titleName.lowercased() == "portal 2" {
- gamepo = true
- }
- } label: {
- Label("Game Info", systemImage: "info.circle")
- }
- }
-
- Section {
- Button {
- gameInfo = game
- isSelectingGameUpdate.toggle()
- } label: {
- Label("Game Update Manager", systemImage: "chevron.up.circle")
- }
-
- Button {
- gameInfo = game
- isSelectingGameDLC.toggle()
- } label: {
- Label("Game DLC Manager", systemImage: "plus.viewfinder")
- }
- }
-
- Section {
- Button(role: .destructive) {
- gametoDelete = game
- showGameDeleteConfirmation.toggle()
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- }
- .confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
- Button("Delete", role: .destructive) {
- if let game = gametoDelete {
- deleteGame(game: game)
- }
- }
- Button("Cancel", role: .cancel) {}
- } message: {
- Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?")
- }
- }
-
- private func deleteGame(game: Game) {
- let fileManager = FileManager.default
- do {
- try fileManager.removeItem(at: game.fileURL)
- games.removeAll { $0.id == game.id }
- } catch {
- print("Error deleting game: \(error)")
- }
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift b/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift
deleted file mode 100644
index d7951c1ee..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift
+++ /dev/null
@@ -1,118 +0,0 @@
-//
-// LogEntry.swift
-// MeloNX
-//
-// Created by Stossy11 on 09/02/2025.
-//
-
-import SwiftUI
-
-struct LogFileView: View {
- @State private var logs: [String] = []
- @State private var showingLogs = false
-
- public var isfps: Bool
-
- private let fileManager = FileManager.default
- private let maxDisplayLines = 10
-
- private var dateFormatter: DateFormatter {
- let formatter = DateFormatter()
- formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
- return formatter
- }
-
- var body: some View {
- VStack(alignment: .leading, spacing: 4) {
- ForEach(logs.suffix(maxDisplayLines), id: \.self) { log in
- Text(log)
- .font(.caption)
- .foregroundColor(.white)
- .padding(4)
- .background(Color.black.opacity(0.7))
- .cornerRadius(4)
- .transition(.opacity)
- }
- }
- .onAppear {
- startLogFileWatching()
- }
- .onChange(of: logs) { newLogs in
- print("Logs updated: \(newLogs.count) entries")
- }
- }
-
- private func getLatestLogFile() -> URL? {
- let logsDirectory = URL.documentsDirectory.appendingPathComponent("Logs")
- let currentDate = Date()
-
- do {
- try fileManager.createDirectory(at: logsDirectory, withIntermediateDirectories: true)
-
- let logFiles = try fileManager.contentsOfDirectory(at: logsDirectory, includingPropertiesForKeys: [.creationDateKey])
- .filter {
- let filename = $0.lastPathComponent
- guard filename.hasPrefix("MeloNX_") && filename.hasSuffix(".log") else {
- return false
- }
-
- let dateString = filename.replacingOccurrences(of: "MeloNX_", with: "").replacingOccurrences(of: ".log", with: "")
- guard let logDate = dateFormatter.date(from: dateString) else {
- return false
- }
-
- return Calendar.current.isDate(logDate, inSameDayAs: currentDate)
- }
-
- let sortedLogFiles = logFiles.sorted {
- $0.lastPathComponent > $1.lastPathComponent
- }
-
- return sortedLogFiles.first
- } catch {
- print("Error finding log files: \(error)")
- return nil
- }
- }
-
- private func readLatestLogFile() {
- guard let logFileURL = getLatestLogFile() else {
- print("no logs?")
- return
- }
- print(logFileURL)
-
- do {
- let logContents = try String(contentsOf: logFileURL)
- let allLines = logContents.components(separatedBy: .newlines)
-
- DispatchQueue.global(qos: .userInteractive).async {
- self.logs = Array(allLines)
- }
- } catch {
- print("Error reading log file: \(error)")
- }
- }
-
- private func startLogFileWatching() {
- showingLogs = true
-
- Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
- if showingLogs {
- self.readLatestLogFile()
- }
-
- if isfps {
- sleep(1)
- if get_current_fps() != 0 {
- stopLogFileWatching()
- timer.invalidate()
- }
- }
- }
- }
-
- private func stopLogFileWatching() {
- showingLogs = false
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift
deleted file mode 100644
index c6a789233..000000000
--- a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift
+++ /dev/null
@@ -1,799 +0,0 @@
-//
-// SettingsView.swift
-// MeloNX
-//
-// Created by Stossy11 on 25/11/2024.
-//
-
-import SwiftUI
-import SwiftSVG
-
-struct SettingsView: View {
- @Binding var config: Ryujinx.Configuration
- @Binding var MoltenVKSettings: [MoltenVKSettings]
-
- @Binding var controllersList: [Controller]
- @Binding var currentControllers: [Controller]
-
- @Binding var onscreencontroller: Controller
- @AppStorage("useTrollStore") var useTrollStore: Bool = false
-
- @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
-
- @AppStorage("ignoreJIT") var ignoreJIT: Bool = false
-
- var memoryManagerModes = [
- ("HostMapped", "Host (fast)"),
- ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
- ("SoftwarePageTable", "Software (slow)"),
- ]
-
- @AppStorage("RyuDemoControls") var ryuDemo: Bool = false
- @AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false
-
- @AppStorage("showScreenShotButton") var ssb: Bool = false
-
- @AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = false
- @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
-
- @AppStorage("performacehud") var performacehud: Bool = false
-
- @AppStorage("oldWindowCode") var windowCode: Bool = false
-
- @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
-
- @AppStorage("hasbeenfinished") var finishedStorage: Bool = false
-
- @AppStorage("showlogsloading") var showlogsloading: Bool = true
-
- @AppStorage("showlogsgame") var showlogsgame: Bool = false
-
- @AppStorage("stick-button") var stickButton = false
- @AppStorage("waitForVPN") var waitForVPN = false
-
- @State private var showResolutionInfo = false
- @State private var showAnisotropicInfo = false
- @State private var showControllerInfo = false
- @State private var searchText = ""
- @AppStorage("portal") var gamepo = false
- @StateObject var ryujinx = Ryujinx.shared
-
- var filteredMemoryModes: [(String, String)] {
- guard !searchText.isEmpty else { return memoryManagerModes }
- return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
- }
-
-
- var body: some View {
- iOSNav {
- List {
-
-
- // Graphics & Performance
- Section {
- Picker(selection: $config.aspectRatio) {
- ForEach(AspectRatio.allCases, id: \.self) { ratio in
- Text(ratio.displayName).tag(ratio)
- }
- } label: {
- labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.disableShaderCache) {
- labelWithIcon("Shader Cache", iconName: "memorychip")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.disablevsync) {
- labelWithIcon("Disable VSync", iconName: "arrow.triangle.2.circlepath")
- }
- .tint(.blue)
-
-
- Toggle(isOn: $config.enableTextureRecompression) {
- labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.disableDockedMode) {
- labelWithIcon("Docked Mode", iconName: "dock.rectangle")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.macroHLE) {
- labelWithIcon("Macro HLE", iconName: "gearshape")
- }.tint(.blue)
-
-
- VStack(alignment: .leading, spacing: 10) {
- HStack {
- labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
- .font(.headline)
- Spacer()
- Button {
- showResolutionInfo.toggle()
- } label: {
- Image(systemName: "info.circle")
- .symbolRenderingMode(.hierarchical)
- .foregroundStyle(.secondary)
- }
- .buttonStyle(.plain)
- .help("Learn more about Resolution Scale")
- .alert(isPresented: $showResolutionInfo) {
- Alert(
- title: Text("Resolution Scale"),
- message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
- dismissButton: .default(Text("OK"))
- )
- }
- }
-
- Slider(value: $config.resscale, in: 0.1...3.0, step: 0.05) {
- Text("Resolution Scale")
- } minimumValueLabel: {
- Text("0.1x")
- .font(.footnote)
- .foregroundColor(.secondary)
- } maximumValueLabel: {
- Text("3.0x")
- .font(.footnote)
- .foregroundColor(.secondary)
- }
- Text("\(config.resscale, specifier: "%.2f")x")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- .padding(.vertical, 8)
-
- VStack(alignment: .leading, spacing: 10) {
- HStack {
- labelWithIcon("Max Anisotropic Scale", iconName: "magnifyingglass")
- .font(.headline)
- Spacer()
- Button {
- showAnisotropicInfo.toggle()
- } label: {
- Image(systemName: "info.circle")
- .symbolRenderingMode(.hierarchical)
- .foregroundStyle(.secondary)
- }
- .buttonStyle(.plain)
- .help("Learn more about Max Anisotropic Scale")
- .alert(isPresented: $showAnisotropicInfo) {
- Alert(
- title: Text("Max Anisotripic Scale"),
- message: Text("Adjust the internal Anisotropic resolution. Higher values improve visuals but may reduce performance. Default at 0 lets game decide."),
- dismissButton: .default(Text("OK"))
- )
- }
- }
-
- Slider(value: $config.maxAnisotropy, in: 0...16.0, step: 0.1) {
- Text("Resolution Scale")
- } minimumValueLabel: {
- Text("0x")
- .font(.footnote)
- .foregroundColor(.secondary)
- } maximumValueLabel: {
- Text("16.0x")
- .font(.footnote)
- .foregroundColor(.secondary)
- }
- Text("\(config.maxAnisotropy, specifier: "%.2f")x")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- .padding(.vertical, 8)
-
- Toggle(isOn: $performacehud) {
- labelWithIcon("Custom Performance Overlay", iconName: "speedometer")
- }
- .tint(.blue)
- } header: {
- Text("Graphics & Performance")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Fine-tune graphics and performance to suit your device and preferences.")
- }
-
- // Input Selector
- Section {
- if !controllersList.filter({ !currentControllers.contains($0) }).isEmpty {
- DisclosureGroup("Unselected Controllers") {
- ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in
- var customBinding: Binding {
- Binding(
- get: { currentControllers.contains(controller) },
- set: { bool in
- if !bool {
- currentControllers.removeAll(where: { $0.id == controller.id })
- } else {
- currentControllers.append(controller)
- }
- }
- )
- }
-
- Toggle(isOn: customBinding) {
- Text(controller.name)
- .font(.body)
- }
- .tint(.blue)
- }
- }
- }
-
-
-
- ForEach(currentControllers) { controller in
-
- var customBinding: Binding {
- Binding(
- get: { currentControllers.contains(controller) },
- set: { bool in
- if !bool {
- currentControllers.removeAll(where: { $0.id == controller.id })
- } else {
- currentControllers.append(controller)
- }
- // toggleController(controller)
- }
- )
- }
-
-
- if customBinding.wrappedValue {
- DisclosureGroup {
- Toggle(isOn: customBinding) {
- Text(controller.name)
- .font(.body)
- }
- .tint(.blue)
- .onDrag({ NSItemProvider() })
- } label: {
-
- if let controller = currentControllers.firstIndex(where: { $0.id == controller.id } ) {
- Text("Player \(controller + 1)")
- .onAppear() {
- // print(currentControllers.firstIndex(where: { $0.id == controller.id }) ?? 0)
- print(currentControllers.count)
-
- if currentControllers.count > 2 {
- print(currentControllers[1])
- print(currentControllers[2])
- }
- }
- }
- }
-
- }
- }
- .onMove { from, to in
- currentControllers.move(fromOffsets: from, toOffset: to)
- }
- } header: {
- Text("Input Selector")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Select input devices and on-screen controls to play with. ")
- }
-
- // Input Settings
- Section {
- Toggle(isOn: $config.handHeldController) {
- labelWithIcon("Player 1 to Handheld Input", iconName: "formfitting.gamecontroller")
- }.tint(.blue)
-
-
- Toggle(isOn: $stickButton) {
- labelWithIcon("Show Stick Buttons", iconName: "l.joystick.press.down")
- }.tint(.blue)
-
-
- Toggle(isOn: $ryuDemo) {
- labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
- }
- .tint(.blue)
- .disabled(true)
-
- VStack(alignment: .leading, spacing: 10) {
- HStack {
- labelWithIcon("On-Screen Controller Scale", iconName: "magnifyingglass")
- .font(.headline)
- Spacer()
- Button {
- showControllerInfo.toggle()
- } label: {
- Image(systemName: "info.circle")
- .symbolRenderingMode(.hierarchical)
- .foregroundStyle(.secondary)
- }
- .buttonStyle(.plain)
- .help("Learn more about On-Screen Controller Scale")
- .alert(isPresented: $showControllerInfo) {
- Alert(
- title: Text("On-Screen Controller Scale"),
- message: Text("Adjust the On-Screen Controller size."),
- dismissButton: .default(Text("OK"))
- )
- }
- }
-
- Slider(value: $controllerScale, in: 0.1...3.0, step: 0.05) {
- Text("Resolution Scale")
- } minimumValueLabel: {
- Text("0.1x")
- .font(.footnote)
- .foregroundColor(.secondary)
- } maximumValueLabel: {
- Text("3.0x")
- .font(.footnote)
- .foregroundColor(.secondary)
- }
- Text("\(controllerScale, specifier: "%.2f")x")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- .padding(.vertical, 8)
- } header: {
- Text("Input Settings")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Configure input devices and on-screen controls for easier navigation and play.")
- }
-
- // Language and Region Settings
- Section {
- Picker(selection: $config.language) {
- ForEach(SystemLanguage.allCases, id: \.self) { ratio in
- Text(ratio.displayName).tag(ratio)
- }
- } label: {
- labelWithIcon("Language", iconName: "character.bubble")
- }
-
- Picker(selection: $config.regioncode) {
- ForEach(SystemRegionCode.allCases, id: \.self) { ratio in
- Text(ratio.displayName).tag(ratio)
- }
- } label: {
- labelWithIcon("Region", iconName: "globe")
- }
-
-
- // globe
- } header: {
- Text("Language and Region Settings")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Configure the System Language and the Region.")
- }
-
- // CPU Mode
- Section {
- if filteredMemoryModes.isEmpty {
- Text("No matches for \"\(searchText)\"")
- .foregroundColor(.secondary)
- } else {
- Picker(selection: $config.memoryManagerMode) {
- ForEach(filteredMemoryModes, id: \.0) { key, displayName in
- Text(displayName).tag(key)
- }
- } label: {
- labelWithIcon("Memory Manager Mode", iconName: "gearshape")
- }
- }
-
- Toggle(isOn: $config.disablePTC) {
- labelWithIcon("Disable PTC", iconName: "cpu")
- }.tint(.blue)
-
- if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
- if #available (iOS 16.4, *) {
- Toggle(isOn: .constant(false)) {
- labelWithIcon("Hypervisor", iconName: "bolt")
- }
- .tint(.blue)
- .disabled(true)
- .onAppear() {
- print("CPU Info: \(gpuInfo)")
- }
- } else if checkAppEntitlement("com.apple.private.hypervisor") {
- Toggle(isOn: $config.hypervisor) {
- labelWithIcon("Hypervisor", iconName: "bolt")
- }
- .tint(.blue)
- .onAppear() {
- print("CPU Info: \(gpuInfo)")
- }
- }
- }
- } header: {
- Text("CPU")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Select how memory is managed. 'Host (fast)' is best for most users.")
- }
-
-
- Section {
-
-
- Toggle(isOn: $config.expandRam) {
- labelWithIcon("Expand Guest Ram (6GB)", iconName: "exclamationmark.bubble")
- }
- .tint(.red)
-
- Toggle(isOn: $config.ignoreMissingServices) {
- labelWithIcon("Ignore Missing Services", iconName: "waveform.path")
- }
- .tint(.red)
- } header: {
- Text("Hacks")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- }
-
- // Other Settings
- Section {
-
- Toggle(isOn: $ssb) {
- labelWithIcon("Screenshot Button", iconName: "square.and.arrow.up")
- }
- .tint(.blue)
-
- if #available(iOS 17.0.1, *) {
- Toggle(isOn: $jitStreamerEB) {
- labelWithIcon("JitStreamer EB", iconName: "bolt.heart")
- }
- .tint(.blue)
- .contextMenu {
- Button {
- waitForVPN.toggle()
- } label: {
- Label {
- Text("Wait for VPN")
- } icon: {
- if waitForVPN {
- Image(systemName: "checkmark")
- }
- }
-
- }
- Button {
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
- let mainWindow = windowScene.windows.last {
- let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best, most kind, helpful and nice developers of all time jkcoxson <3", preferredStyle: .alert)
-
- let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
- UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
- }
- alertController.addAction(learnMoreButton)
-
- let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil)
- alertController.addAction(doneButton)
-
- mainWindow.rootViewController?.present(alertController, animated: true)
- }
- } label: {
- Text("About")
- }
- }
- } else {
- Toggle(isOn: $useTrollStore) {
- labelWithIcon("TrollStore JIT", iconName: "troll.svg")
- }
- .tint(.blue)
- }
-
- Toggle(isOn: $syncqsubmits) {
- labelWithIcon("MVK: Synchronous Queue Submits", iconName: "line.diagonal")
- }.tint(.blue)
- .contextMenu() {
- Button {
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
- let mainWindow = windowScene.windows.last {
- let alertController = UIAlertController(title: "About MVK: Synchronous Queue Submits", message: "Enable this option if Mario Kart 8 is crashing at Grand Prix mode.", preferredStyle: .alert)
-
- let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil)
- alertController.addAction(doneButton)
-
- mainWindow.rootViewController?.present(alertController, animated: true)
- }
- } label: {
- Text("About")
- }
- }
-
- DisclosureGroup {
- Toggle(isOn: $showlogsloading) {
- labelWithIcon("Show logs while loading", iconName: "text.alignleft")
- }.tint(.blue)
-
- Toggle(isOn: $showlogsgame) {
- labelWithIcon("Show logs in-game", iconName: "text.line.magnify")
- }.tint(.blue)
-
- Toggle(isOn: $config.debuglogs) {
- labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble")
- }
- .tint(.blue)
-
- Toggle(isOn: $config.tracelogs) {
- labelWithIcon("Trace Logs", iconName: "waveform.path")
- }
- .tint(.blue)
- } label: {
- Text("Logs")
- }
-
- } header: {
- Text("Miscellaneous Options")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Enable trace and debug logs for advanced troubleshooting (Note: This degrades performance),\nEnable Screenshot Button for better screenshots\nand Enable TrollStore for automatic TrollStore JIT.")
- }
-
- // Info
- Section {
- let totalMemory = ProcessInfo.processInfo.physicalMemory
- let model = getDeviceModel()
- let deviceType = model.hasPrefix("iPad") ? "iPadOS" :
- model.hasPrefix("iPhone") ? "iOS" :
- "macOS"
-
- let iconName = model.hasPrefix("iPad") ? "ipad.landscape" :
- model.hasPrefix("iPhone") ? "iphone" :
- "macwindow"
-
- labelWithIcon("JIT Acquisition: \(ryujinx.jitenabled ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
- .onAppear() {
- print("JIY ;(((((")
- ryujinx.ryuIsJITEnabled()
- }
-
- labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
-
- labelWithIcon("Device: \(getDeviceModel())", iconName: iconName)
-
- if ProcessInfo.processInfo.isiOSAppOnMac {
- labelWithIcon("Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill")
- } else {
- labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill")
- }
-
- labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo")
-
- } header: {
- Text("Information")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("Shows info about Memory, Entitlement and JIT.")
- }
-
- // Advanced
- Section {
- DisclosureGroup {
-
- Toggle(isOn: $config.dfsIntegrityChecks) {
- labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
- }.tint(.blue)
-
- HStack {
- labelWithIcon("Page Size", iconName: "textformat.size")
- Spacer()
- Text("\(String(Int(getpagesize())))")
- .foregroundColor(.secondary)
- }
-
- if MTLHud.shared.canMetalHud {
- Toggle(isOn: $metalHUDEnabled) {
- labelWithIcon("Metal Performance HUD", iconName: "speedometer")
- }
- .tint(.blue)
- .onChange(of: metalHUDEnabled) { newValue in
- MTLHud.shared.toggle()
- }
- }
-
- Toggle(isOn: $ignoreJIT) {
- labelWithIcon("Ignore JIT Popup", iconName: "cpu")
- }.tint(.blue)
-
- TextField("Additional Arguments", text: Binding(
- get: {
- config.additionalArgs.joined(separator: " ")
- },
- set: { newValue in
- config.additionalArgs = newValue
- .split(separator: ",")
- .map { $0.trimmingCharacters(in: .whitespaces) }
- }
- ))
- .textInputAutocapitalization(.none)
- .disableAutocorrection(true)
-
-
-
- Button {
- finishedStorage = false
-
- } label: {
- Text("Show Setup")
- .font(.body)
- }
-
-
- } label: {
- Text("Advanced Options")
- }
- } header: {
- Text("Advanced")
- .font(.title3.weight(.semibold))
- .textCase(nil)
- .headerProminence(.increased)
- } footer: {
- Text("For advanced users. See page size or add custom arguments for experimental features, \"Metal Performance HUD\" is not needed if you have it enabled in settings. \n \n\(gamepo ? "the cake is a lie" : "")")
- }
-
- }
- .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
- .navigationTitle("Settings")
- .navigationBarTitleDisplayMode(.inline)
- .listStyle(.insetGrouped)
- .onAppear {
- mVKPreFillBuffer = false
-
- if let configs = loadSettings() {
- self.config = configs
- } else {
- saveSettings()
- }
- }
- .onChange(of: config) { _ in
- saveSettings()
- }
- }
- .navigationViewStyle(.stack)
- }
-
- private func toggleController(_ controller: Controller) {
- if currentControllers.contains(where: { $0.id == controller.id }) {
- currentControllers.removeAll(where: { $0.id == controller.id })
- } else {
- currentControllers.append(controller)
- }
- }
-
- func saveSettings() {
- MeloNX.saveSettings(config: config)
- }
-
- func getDeviceModel() -> String {
- var systemInfo = utsname()
- uname(&systemInfo)
- let machineMirror = Mirror(reflecting: systemInfo.machine)
- let identifier = machineMirror.children.reduce("") { identifier, element in
- guard let value = element.value as? Int8, value != 0 else { return identifier }
- return identifier + String(UnicodeScalar(UInt8(value)))
- }
- return identifier
- }
-
-
- func getGPUInfo() -> String? {
- let device = MTLCreateSystemDefaultDevice()
-
- let gpu = device?.name
- print("GPU: " + (gpu ?? ""))
- return gpu
- }
-
-
- @ViewBuilder
- private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
- HStack(spacing: 8) {
- if iconName.hasSuffix(".svg"){
- if let flipimage, flipimage {
- SVGView(svgName: iconName, color: .blue)
- .symbolRenderingMode(.hierarchical)
- .frame(width: 20, height: 20)
- .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
- } else {
- SVGView(svgName: iconName, color: .blue)
- .symbolRenderingMode(.hierarchical)
- .frame(width: 20, height: 20)
- }
- } else if !iconName.isEmpty {
- Image(systemName: iconName)
- .symbolRenderingMode(.hierarchical)
- .foregroundStyle(.blue)
- }
- Text(text)
- }
- .font(.body)
- }
-}
-
-
-struct SVGView: UIViewRepresentable {
- var svgName: String
- var color: Color = Color.black
-
- func makeUIView(context: Context) -> UIView {
- var svgName = svgName
- let hammock = UIView()
-
- if svgName.hasSuffix(".svg") {
- svgName.removeLast(4)
- }
-
-
-
- _ = UIView(svgNamed: svgName) { svgLayer in
- svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color
- svgLayer.resizeToFit(hammock.frame)
- hammock.layer.addSublayer(svgLayer)
- }
-
- return hammock
- }
-
- func updateUIView(_ uiView: UIView, context: Context) {
- // Update the SVG view's fill color when the color changes
- if let svgLayer = uiView.layer.sublayers?.first as? CAShapeLayer {
- svgLayer.fillColor = UIColor(color).cgColor
- }
- }
-}
-
-func saveSettings(config: Ryujinx.Configuration) {
- do {
- let encoder = JSONEncoder()
- encoder.outputFormatting = .prettyPrinted
- let data = try encoder.encode(config)
-
- let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
-
- try data.write(to: fileURL)
- print("Settings saved to: \(fileURL.path)")
- } catch {
- print("Failed to save settings: \(error)")
- }
-}
-
-func loadSettings() -> Ryujinx.Configuration? {
- do {
- let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
-
- guard FileManager.default.fileExists(atPath: fileURL.path) else {
- print("Config file does not exist at: \(fileURL.path)")
- return nil
- }
-
- let data = try Data(contentsOf: fileURL)
-
- let decoder = JSONDecoder()
- let configs = try decoder.decode(Ryujinx.Configuration.self, from: data)
- return configs
- } catch {
- print("Failed to load settings: \(error)")
- return nil
- }
-}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/ContentView.swift
similarity index 63%
rename from src/MeloNX/MeloNX/App/Views/Main/ContentView.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/ContentView.swift
index 34fcb32c1..1eb05187d 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/ContentView.swift
@@ -10,6 +10,7 @@ import GameController
import Darwin
import UIKit
import MetalKit
+import CoreLocation
struct MoltenVKSettings: Codable, Hashable {
let string: String
@@ -31,19 +32,25 @@ struct ContentView: View {
@AppStorage("isVirtualController") var isVCA: Bool = true
// Settings and Configuration
- @State private var config: Ryujinx.Configuration
+ private var config: Ryujinx.Arguments {
+ settingsManager.config
+ }
+
+ @StateObject private var settingsManager = SettingsManager.shared
+
@State var settings: [MoltenVKSettings]
- @AppStorage("useTrollStore") var useTrollStore: Bool = false
// JIT
+ @AppStorage("useTrollStore") var useTrollStore: Bool = false
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
+ @AppStorage("stikJIT") var stikJIT: Bool = false
// Other Configuration
@State var isMK8: Bool = false
@AppStorage("quit") var quit: Bool = false
@State var quits: Bool = false
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
- @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true
+ @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
// Loading Animation
@@ -53,6 +60,9 @@ struct ContentView: View {
private let animationDuration: Double = 1.0
@State private var isAnimating = false
@State var isLoading = true
+
+
+ // MARK: - CORE
@StateObject var ryujinx = Ryujinx.shared
// MARK: - SDL
@@ -60,14 +70,6 @@ struct ContentView: View {
// MARK: - Initialization
init() {
- var defaultConfig = loadSettings()
- if defaultConfig == nil {
- saveSettings(config: .init(gamepath: ""))
- defaultConfig = loadSettings()
- }
-
- _config = State(initialValue: defaultConfig!)
-
let defaultSettings: [MoltenVKSettings] = [
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
@@ -79,8 +81,6 @@ struct ContentView: View {
_settings = State(initialValue: defaultSettings)
- print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
-
initializeSDL()
}
@@ -119,7 +119,7 @@ struct ContentView: View {
private var jitErrorView: some View {
Text("")
- .sheet(isPresented:Binding(
+ .fullScreenCover(isPresented:Binding(
get: { !ryujinx.jitenabled },
set: { newValue in
ryujinx.jitenabled = newValue
@@ -130,14 +130,12 @@ struct ContentView: View {
JITPopover() {
ryujinx.jitenabled = false
}
- .interactiveDismissDisabled()
}
}
private var mainMenuView: some View {
MainTabView(
startemu: $game,
- config: $config,
MVKconfig: $settings,
controllersList: $controllersList,
currentControllers: $currentControllers,
@@ -152,24 +150,18 @@ struct ContentView: View {
refreshControllersList()
}
-
- print(MTLHud.shared.isEnabled)
+ UserDefaults.standard.set(false, forKey: "lockInApp")
initControllerObservers()
Air.play(AnyView(
- VStack {
- Image(systemName: "gamecontroller")
- .font(.system(size: 300))
- .foregroundColor(.gray)
- .padding(.bottom, 10)
-
- Text("Select Game")
- .font(.system(size: 150))
- .bold()
- }
+ ControllerListView(game: $game)
))
+ refreshControllersList()
+
+ ryujinx.addGames()
+
checkJitStatus()
}
.onOpenURL { url in
@@ -288,7 +280,6 @@ struct ContentView: View {
queue: .main
) { notification in
if let controller = notification.object as? GCController {
- print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller)
refreshControllersList()
}
@@ -300,7 +291,8 @@ struct ContentView: View {
queue: .main
) { notification in
if let controller = notification.object as? GCController {
- print("Controller disconnected: \(controller.productCategory)")
+ currentControllers = []
+ controllersList = []
nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil
refreshControllersList()
@@ -317,6 +309,9 @@ struct ContentView: View {
}
private func refreshControllersList() {
+ currentControllers = []
+ controllersList = []
+
controllersList = ryujinx.getConnectedControllers()
if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
@@ -325,8 +320,6 @@ struct ContentView: View {
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
-
- currentControllers = []
if controllersList.count == 1 {
currentControllers.append(controllersList[0])
@@ -339,22 +332,54 @@ struct ContentView: View {
}
}
+ private func registerMotionForMatchingControllers() {
+ // Loop through currentControllers with index
+ for (index, controller) in currentControllers.enumerated() {
+ let slot = UInt8(index)
+
+ // Check native controllers
+ for (_, nativeController) in nativeControllers where nativeController.controllername == String("GC - \(controller.name)") && nativeController.tryGetMotionProvider() == nil {
+ nativeController.tryRegisterMotion(slot: slot)
+ continue
+ }
+
+ // Check virtual controller if active
+ if Ryujinx.shared.virtualController.controllername == controller.name && Ryujinx.shared.virtualController.tryGetMotionProvider() == nil {
+ Ryujinx.shared.virtualController.tryRegisterMotion(slot: slot)
+ continue
+ }
+ }
+ }
+
+ @StateObject private var persettings = PerGameSettingsManager.shared
private func start(displayid: UInt32) {
guard let game else { return }
+ var config = self.config
+
+ persettings.loadSettings()
+
+ if let customgame = persettings.config[game.titleId] {
+ config = customgame
+ }
config.gamepath = game.fileURL.path
config.inputids = Array(Set(currentControllers.map(\.id)))
configureEnvironmentVariables()
- if config.inputids.isEmpty {
- config.inputids.append("0")
+ registerMotionForMatchingControllers()
+
+ config.inputids.isEmpty ? config.inputids.append("0") : ()
+
+ // Local DSU loopback to ryujinx per input id
+ for _ in config.inputids {
+ config.inputDSUServers.append("127.0.0.1:26760")
}
do {
try ryujinx.start(with: config)
} catch {
- print("Error: \(error.localizedDescription)")
+ // print("Error: \(error.localizedDescription)")
}
}
@@ -365,7 +390,7 @@ struct ContentView: View {
}
if syncqsubmits {
- setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "2", 1)
+ setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "1", 1)
}
}
@@ -377,13 +402,25 @@ struct ContentView: View {
private func checkJitStatus() {
ryujinx.ryuIsJITEnabled()
+ if jitStreamerEB {
+ jitStreamerEB = false // byee jitstreamer eb
+ }
+
+
if !ryujinx.jitenabled {
if useTrollStore {
askForJIT()
+ } else if stikJIT {
+ enableJITStik()
} else if jitStreamerEB {
enableJITEB()
} else {
- print("no JIT")
+ if !allocateTest(), checkDebugged() {
+ loop_heartbeat()
+ sleep(5)
+ let cool = String(cString: attach(getpid())!)
+ print(cool)
+ }
}
}
}
@@ -391,6 +428,9 @@ struct ContentView: View {
private func handleDeepLink(_ url: URL) {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" {
+
+ refreshControllersList()
+
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = ryujinx.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
@@ -407,3 +447,135 @@ extension Array {
}
}
}
+
+class LocationManager: NSObject, CLLocationManagerDelegate {
+
+ private var locationManager: CLLocationManager
+
+ static let sharedInstance = LocationManager()
+
+ private override init() {
+ locationManager = CLLocationManager()
+ super.init()
+ locationManager.delegate = self
+ locationManager.desiredAccuracy = kCLLocationAccuracyBest
+ locationManager.pausesLocationUpdatesAutomatically = false
+ }
+
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+ // print("wow")
+ }
+
+ func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
+ print("Location manager failed with: \(error)")
+ }
+
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
+ if manager.authorizationStatus == .denied {
+ print("Location services are disabled in settings.")
+ } else {
+ startUpdatingLocation()
+ }
+ }
+
+ func stop() {
+ if UserDefaults.standard.bool(forKey: "location-enabled") {
+ locationManager.stopUpdatingLocation()
+ }
+ }
+
+ func startUpdatingLocation() {
+ if UserDefaults.standard.bool(forKey: "location-enabled") {
+ locationManager.requestAlwaysAuthorization()
+ locationManager.allowsBackgroundLocationUpdates = true
+ locationManager.startUpdatingLocation()
+ }
+ }
+}
+
+struct ControllerListView: View {
+ @State private var selectedIndex = 0
+ @Binding var game: Game?
+ @ObservedObject private var ryujinx = Ryujinx.shared
+
+ var body: some View {
+ List(ryujinx.games.indices, id: \.self) { index in
+ let game = ryujinx.games[index]
+
+ HStack(spacing: 16) {
+ // Game Icon
+ Group {
+ if let icon = game.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ } else {
+ ZStack {
+ RoundedRectangle(cornerRadius: 10)
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 24))
+ .foregroundColor(.gray)
+ }
+ }
+ }
+ .frame(width: 55, height: 55)
+ .cornerRadius(10)
+
+ // Game Info
+ VStack(alignment: .leading, spacing: 4) {
+ Text(game.titleName)
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(.primary)
+
+ HStack(spacing: 4) {
+ Text(game.developer)
+
+ if !game.version.isEmpty && game.version != "0" {
+ Text("•")
+ Text("v\(game.version)")
+ }
+ }
+ .font(.system(size: 14))
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+ }
+ .background(selectedIndex == index ? Color.blue.opacity(0.3) : .clear)
+ }
+ .onAppear(perform: setupControllerObservers)
+ }
+
+ private func setupControllerObservers() {
+ let dpadHandler: GCControllerDirectionPadValueChangedHandler = { _, _, yValue in
+ if yValue == 1.0 {
+ selectedIndex = max(0, selectedIndex - 1)
+ } else if yValue == -1.0 {
+ selectedIndex = min(ryujinx.games.count - 1, selectedIndex + 1)
+ }
+ }
+
+ for controller in GCController.controllers() {
+ print("Controller connected: \(controller.vendorName ?? "Unknown")")
+ controller.playerIndex = .index1
+
+ controller.microGamepad?.dpad.valueChangedHandler = dpadHandler
+ controller.extendedGamepad?.dpad.valueChangedHandler = dpadHandler
+
+ controller.extendedGamepad?.buttonA.pressedChangedHandler = { _, _, pressed in
+ if pressed {
+ print("A button pressed")
+ game = ryujinx.games[selectedIndex]
+ }
+ }
+ }
+
+ NotificationCenter.default.addObserver(
+ forName: .GCControllerDidConnect,
+ object: nil,
+ queue: .main
+ ) { _ in
+ setupControllerObservers()
+ }
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameCompatibility/GameCompatibilityCache.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameCompatibility/GameCompatibilityCache.swift
new file mode 100644
index 000000000..4b757ff35
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameCompatibility/GameCompatibilityCache.swift
@@ -0,0 +1,44 @@
+//
+// GameRequirementsCache.swift
+// MeloNX
+//
+// Created by Stossy11 on 21/03/2025.
+//
+
+
+import Foundation
+
+class GameCompatibiliryCache {
+ static let shared = GameCompatibiliryCache()
+ private let cacheKey = "gameRequirementsCache"
+ private let timestampKey = "gameRequirementsCacheTimestamp"
+
+ private let cacheDuration: TimeInterval = Double.random(in: 3...5) * 24 * 60 * 60 // Randomly pick 3-5 days
+
+ func getCachedData() -> [GameRequirements]? {
+ guard let cachedData = UserDefaults.standard.data(forKey: cacheKey),
+ let timestamp = UserDefaults.standard.object(forKey: timestampKey) as? Date else {
+ return nil
+ }
+
+ let timeElapsed = Date().timeIntervalSince(timestamp)
+ if timeElapsed > cacheDuration {
+ clearCache()
+ return nil
+ }
+
+ return try? JSONDecoder().decode([GameRequirements].self, from: cachedData)
+ }
+
+ func setCachedData(_ data: [GameRequirements]) {
+ if let encodedData = try? JSONEncoder().encode(data) {
+ UserDefaults.standard.set(encodedData, forKey: cacheKey)
+ UserDefaults.standard.set(Date(), forKey: timestampKey)
+ }
+ }
+
+ func clearCache() {
+ UserDefaults.standard.removeObject(forKey: cacheKey)
+ UserDefaults.standard.removeObject(forKey: timestampKey)
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameInfoSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift
similarity index 85%
rename from src/MeloNX/MeloNX/App/Views/Main/GamesList/GameInfoSheet.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift
index 5c4f9c3c8..c6cbb3880 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameInfoSheet.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameInfoSheet.swift
@@ -10,7 +10,7 @@ import SwiftUI
struct GameInfoSheet: View {
let game: Game
- @Environment(\.dismiss) var dismiss
+ @Environment(\.presentationMode) var presentationMode
var body: some View {
iOSNav {
@@ -44,7 +44,7 @@ struct GameInfoSheet: View {
.multilineTextAlignment(.center)
Text(game.developer)
.font(.caption)
- .foregroundStyle(.secondary)
+ .foregroundColor(.secondary)
}
.padding(.vertical, 3)
}
@@ -56,7 +56,7 @@ struct GameInfoSheet: View {
Text("**Version**")
Spacer()
Text(game.version)
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
HStack {
Text("**Title ID**")
@@ -69,36 +69,36 @@ struct GameInfoSheet: View {
}
Spacer()
Text(game.titleId)
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
HStack {
Text("**Game Size**")
Spacer()
Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes")
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
HStack {
Text("**File Type**")
Spacer()
Text(getFileType(game.fileURL))
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
VStack(alignment: .leading, spacing: 4) {
Text("**Game URL**")
Text(trimGameURL(game.fileURL))
- .foregroundStyle(Color.secondary)
+ .foregroundColor(Color.secondary)
}
} header: {
Text("Information")
}
- .headerProminence(.increased)
+ // .headerProminence(.increased)
}
.navigationTitle(game.titleName)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
- ToolbarItem(placement: .navigationBarTrailing) {
- Button("Done") {
- dismiss()
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Dismiss") {
+ presentationMode.wrappedValue.dismiss()
}
}
}
@@ -113,7 +113,7 @@ struct GameInfoSheet: View {
return size
}
} catch {
- print("Error getting file size: \(error)")
+ // print("Error getting file size: \(error)")
}
return nil
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift
new file mode 100644
index 000000000..47124f48e
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift
@@ -0,0 +1,1267 @@
+//
+// GameListView.swift
+// MeloNX
+//
+// Created by Stossy11 on 3/11/2024.
+//
+
+import SwiftUI
+import UniformTypeIdentifiers
+
+extension UTType {
+ static let nsp = UTType(exportedAs: "com.nintendo.switch-package")
+ static let xci = UTType(exportedAs: "com.nintendo.switch-cartridge")
+}
+
+struct GameLibraryView: View {
+ @Binding var startemu: Game?
+ @State private var searchText = ""
+ @State private var isSearching = false
+ @AppStorage("recentGames") private var recentGamesData: Data = Data()
+ @State private var recentGames: [Game] = []
+ @Environment(\.colorScheme) var colorScheme
+ @State var firmwareInstaller = false
+ @State var firmwareversion = "0"
+ @State var isImporting: Bool = false
+ @State var startgame = false
+ @State var isSelectingGameFile = false
+ @State var isViewingGameInfo: Bool = false
+ @State var gamePerGameSettings: Game?
+ var isShowingPerGameSettings: Binding {
+ Binding {
+ gamePerGameSettings != nil
+ } set: { value in
+ !value ? gamePerGameSettings = nil : ()
+ }
+
+ }
+ @State var isSelectingGameUpdate: Bool = false
+ @State var isSelectingGameDLC: Bool = false
+ @StateObject var ryujinx = Ryujinx.shared
+ @State var gameInfo: Game?
+ @State var gameRequirements: [GameRequirements] = []
+ @State private var showingOptions = false
+
+ var games: Binding<[Game]> {
+ Binding(
+ get: { Ryujinx.shared.games },
+ set: { Ryujinx.shared.games = $0 }
+ )
+ }
+
+ var filteredGames: [Game] {
+ if searchText.isEmpty {
+ return Ryujinx.shared.games.filter { game in
+ !realRecentGames.contains(where: { $0.fileURL == game.fileURL })
+ }
+ }
+ return Ryujinx.shared.games.filter {
+ $0.titleName.localizedCaseInsensitiveContains(searchText) ||
+ $0.developer.localizedCaseInsensitiveContains(searchText)
+ }
+ }
+
+ var realRecentGames: [Game] {
+ let games = Ryujinx.shared.games
+ return recentGames.compactMap { recentGame in
+ games.first(where: { $0.fileURL == recentGame.fileURL })
+ }
+ }
+
+ var body: some View {
+ iOSNav {
+ ZStack {
+ // Background color
+ Color(UIColor.systemBackground)
+ .ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ // Header with stats
+ if !Ryujinx.shared.games.isEmpty {
+ GameLibraryHeader(
+ totalGames: Ryujinx.shared.games.count,
+ recentGames: realRecentGames.count,
+ firmwareVersion: firmwareversion
+ )
+ }
+
+ // Game list
+ if Ryujinx.shared.games.isEmpty {
+ EmptyGameLibraryView(isSelectingGameFile: $isSelectingGameFile)
+ } else {
+ gameListView
+ .animation(.easeInOut(duration: 0.3), value: searchText)
+ }
+ }
+ }
+ .navigationTitle("Game Library")
+ .navigationBarTitleDisplayMode(.large)
+ .onAppear {
+ loadRecentGames()
+ firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
+
+ pullGameCompatibility() { result in
+ switch result {
+ case .success(let success):
+ gameRequirements = success
+ case .failure(_):
+ print("Failed to load game compatibility data")
+ }
+ }
+ }
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+
+ Button {
+ isSelectingGameFile = true
+ isImporting = true
+ } label: {
+ Label("Add Game", systemImage: "plus")
+ .labelStyle(.iconOnly)
+ .font(.system(size: 16, weight: .semibold))
+ }
+ // .buttonStyle(.bordered)
+ .accentColor(.blue)
+ }
+
+ ToolbarItem(placement: .topBarLeading) {
+ Menu {
+ firmwareSection
+
+ Divider()
+
+ Button {
+ isSelectingGameFile = false
+ isImporting = true
+ } label: {
+ Label("Open Game", systemImage: "square.and.arrow.down")
+ }
+
+ Button {
+ let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+ var sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
+ if ProcessInfo.processInfo.isiOSAppOnMac {
+ sharedurl = documentsUrl.absoluteString
+ }
+ if UIApplication.shared.canOpenURL(URL(string: sharedurl)!) {
+ UIApplication.shared.open(URL(string: sharedurl)!, options: [:])
+ }
+ } label: {
+ Label("Show MeloNX Folder", systemImage: "folder")
+ }
+ } label: {
+ Label("Options", systemImage: "ellipsis.circle")
+ .labelStyle(.iconOnly)
+ .foregroundColor(.blue)
+ }
+ }
+ }
+ .overlay(Group {
+ if ryujinx.jitenabled {
+ VStack {
+ HStack {
+ Spacer()
+ Circle()
+ .frame(width: 12, height: 12)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .foregroundColor(Color.green)
+ .padding()
+ }
+ Spacer()
+ }
+ }
+ })
+ .onChange(of: startemu) { game in
+ guard let game else { return }
+ addToRecentGames(game)
+ }
+ // .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers")
+ .onChange(of: searchText) { _ in
+ isSearching = !searchText.isEmpty
+ }
+ .onChange(of: isImporting) { newValue in
+ if newValue {
+ FileImporterManager.shared.importFiles(types: [.nsp, .xci, .item]) { result in
+ isImporting = false
+ handleRunningGame(result: result)
+ }
+ }
+ }
+ .onChange(of: isSelectingGameFile) { newValue in
+ if newValue {
+ FileImporterManager.shared.importFiles(types: [.nsp, .xci, .item]) { result in
+ isImporting = false
+ handleAddingGame(result: result)
+ }
+ }
+ }
+ .onChange(of: firmwareInstaller) { newValue in
+ if newValue {
+ FileImporterManager.shared.importFiles(types: [.folder, .zip]) { result in
+ isImporting = false
+ handleFirmwareImport(result: result)
+ }
+ }
+ }
+ .sheet(isPresented: $isSelectingGameUpdate) {
+ UpdateManagerSheet(game: $gameInfo)
+ }
+ .sheet(isPresented: $isSelectingGameDLC) {
+ DLCManagerSheet(game: $gameInfo)
+ }
+ .sheet(isPresented: isShowingPerGameSettings) {
+ PerGameSettingsView(titleId: gamePerGameSettings!.titleId)
+ }
+ .sheet(isPresented: Binding(
+ get: { isViewingGameInfo && gameInfo != nil },
+ set: { newValue in
+ if !newValue {
+ isViewingGameInfo = false
+ gameInfo = nil
+ }
+ }
+ )) {
+ if let game = gameInfo {
+ GameInfoSheet(game: game)
+ }
+ }
+ }
+ }
+
+ // MARK: - Subviews
+
+ private var gameListView: some View {
+ ScrollView {
+ LazyVStack(spacing: 0) {
+ if !isSearching && !realRecentGames.isEmpty {
+ // Recent Games Section
+ VStack(alignment: .leading, spacing: 0) {
+ Text("Recent Games")
+ .font(.headline)
+ .foregroundColor(.primary)
+ .padding(.horizontal)
+ .padding(.top)
+
+ ScrollView(.horizontal, showsIndicators: false) {
+ LazyHStack(spacing: 16) {
+ ForEach(realRecentGames) { game in
+ GameCardView(
+ game: game,
+ startemu: $startemu,
+ games: games,
+ isViewingGameInfo: $isViewingGameInfo,
+ isSelectingGameUpdate: $isSelectingGameUpdate,
+ isSelectingGameDLC: $isSelectingGameDLC,
+ gameRequirements: $gameRequirements,
+ gameInfo: $gameInfo
+ )
+ .contextMenu {
+ gameContextMenu(for: game)
+ }
+ }
+ }
+ .padding()
+ }
+ }
+
+ // Library Section
+ if !filteredGames.isEmpty {
+ VStack(alignment: .leading) {
+ Text("Library")
+ .font(.headline)
+ .foregroundColor(.primary)
+ .padding(.horizontal)
+ .padding(.top)
+
+ ForEach(filteredGames) { game in
+ GameListRow(
+ game: game,
+ startemu: $startemu,
+ games: games,
+ isViewingGameInfo: $isViewingGameInfo,
+ isSelectingGameUpdate: $isSelectingGameUpdate,
+ isSelectingGameDLC: $isSelectingGameDLC,
+ gameRequirements: $gameRequirements,
+ gameInfo: $gameInfo,
+ perGameSettings: $gamePerGameSettings
+ )
+ .padding(.horizontal)
+ .padding(.vertical, 8)
+ }
+ }
+ }
+ } else {
+ ForEach(filteredGames) { game in
+ GameListRow(
+ game: game,
+ startemu: $startemu,
+ games: games,
+ isViewingGameInfo: $isViewingGameInfo,
+ isSelectingGameUpdate: $isSelectingGameUpdate,
+ isSelectingGameDLC: $isSelectingGameDLC,
+ gameRequirements: $gameRequirements,
+ gameInfo: $gameInfo,
+ perGameSettings: $gamePerGameSettings
+ )
+ .padding(.horizontal)
+ .padding(.vertical, 8)
+ }
+ }
+
+ Spacer(minLength: 50)
+ }
+ }
+ }
+
+ private var firmwareSection: some View {
+ Group {
+ if firmwareversion == "0" {
+ Button {
+ DispatchQueue.main.async {
+ firmwareInstaller.toggle()
+ }
+ } label: {
+ Label("Install Firmware", systemImage: "square.and.arrow.down")
+ }
+
+ } else {
+ Menu("Applets") {
+ Button {
+ let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "0x0100000000001009")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
+ self.startemu = game
+ } label: {
+ Label("Launch Mii Maker", systemImage: "person.crop.circle")
+ }
+
+ Button {
+ let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "0x0100000000001000")!, titleName: "Home Menu (Broken)", titleId: "0", developer: "Nintendo", version: firmwareversion)
+ self.startemu = game
+ } label: {
+ Label("Home Menu (Broken)", systemImage: "house.circle")
+ }
+ .foregroundStyle(.red)
+ }
+ }
+ }
+ }
+
+ // MARK: - Game Management Functions
+
+ private func addToRecentGames(_ game: Game) {
+ recentGames.removeAll { $0.titleId == game.titleId }
+ recentGames.insert(game, at: 0)
+
+ if recentGames.count > 5 {
+ recentGames = Array(recentGames.prefix(5))
+ }
+
+ saveRecentGames()
+ }
+
+ private func removeFromRecentGames(_ game: Game) {
+ recentGames.removeAll { $0.titleId == game.titleId }
+ saveRecentGames()
+ }
+
+ private func saveRecentGames() {
+ do {
+ let encoder = JSONEncoder()
+ let data = try encoder.encode(recentGames)
+ recentGamesData = data
+ } catch {
+ // print("Error saving recent games: \(error)")
+ }
+ }
+
+ private func loadRecentGames() {
+ do {
+ let decoder = JSONDecoder()
+ recentGames = try decoder.decode([Game].self, from: recentGamesData)
+ } catch {
+ // print("Error loading recent games: \(error)")
+ recentGames = []
+ }
+ }
+
+ private func deleteGame(game: Game) {
+ let fileManager = FileManager.default
+ do {
+ try fileManager.removeItem(at: game.fileURL)
+ Ryujinx.shared.games.removeAll { $0.id == game.id }
+ Ryujinx.shared.games = Ryujinx.shared.loadGames()
+ } catch {
+ // print("Error deleting game: \(error)")
+ }
+ }
+
+ // MARK: - Import Handlers
+
+ private func handleAddingGame(result: Result<[URL], Error>) {
+ switch result {
+ case .success(let urls):
+ guard let url = urls.first, url.startAccessingSecurityScopedResource() else {
+ // print("Failed to access security-scoped resource")
+ return
+ }
+ defer { url.stopAccessingSecurityScopedResource() }
+
+ do {
+ let fileManager = FileManager.default
+ let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+ let romsDirectory = documentsDirectory.appendingPathComponent("roms")
+
+ if !fileManager.fileExists(atPath: romsDirectory.path) {
+ try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
+ }
+
+ let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
+ try fileManager.copyItem(at: url, to: destinationURL)
+
+ Ryujinx.shared.games = Ryujinx.shared.loadGames()
+ } catch {
+ // print("Error copying game file: \(error)")
+ }
+ case .failure(let err):
+ print("File import failed: \(err.localizedDescription)")
+ }
+ }
+
+ private func handleRunningGame(result: Result<[URL], Error>) {
+ switch result {
+ case .success(let urls):
+ guard let url = urls.first, url.startAccessingSecurityScopedResource() else {
+ // print("Failed to access security-scoped resource")
+ return
+ }
+
+ do {
+ let handle = try FileHandle(forReadingFrom: url)
+ let fileExtension = (url.pathExtension as NSString).utf8String
+ let extensionPtr = UnsafeMutablePointer(mutating: fileExtension)
+
+ let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
+
+ let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
+
+ DispatchQueue.main.async {
+ startemu = game
+ }
+ } catch {
+ // print(error)
+ }
+
+ case .failure(let err):
+ print("File import failed: \(err.localizedDescription)")
+ }
+ }
+
+ private func handleFirmwareImport(result: Result<[URL], Error>) {
+ switch result {
+ case .success(let url):
+ guard let url = url.first else {
+ return
+ }
+
+ do {
+ let fun = url.startAccessingSecurityScopedResource()
+ let path = url.path
+
+ Ryujinx.shared.installFirmware(firmwarePath: path)
+
+ firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
+ if fun {
+ url.stopAccessingSecurityScopedResource()
+ }
+ }
+ case .failure(let error):
+ print(error)
+ }
+ }
+
+ // MARK: - Context Menus
+
+ private func gameContextMenu(for game: Game) -> some View {
+ Group {
+ Section {
+ Button {
+ startemu = game
+ } label: {
+ Label("Play Now", systemImage: "play.fill")
+ }
+
+ Button {
+ gameInfo = game
+ isViewingGameInfo.toggle()
+ } label: {
+ Label("Game Info", systemImage: "info.circle")
+ }
+
+ Button {
+ gamePerGameSettings = game
+ } label: {
+ Label("\(game.titleName) Settings", systemImage: "gear")
+ }
+ }
+
+ Section {
+ Button {
+ gameInfo = game
+ isSelectingGameUpdate.toggle()
+ } label: {
+ Label("Update Manager", systemImage: "arrow.up.circle")
+ }
+
+ Button {
+ gameInfo = game
+ isSelectingGameDLC.toggle()
+ } label: {
+ Label("DLC Manager", systemImage: "plus.circle")
+ }
+ }
+
+ Section {
+ Button(role: .destructive) {
+ removeFromRecentGames(game)
+ } label: {
+ Label("Remove from Recents", systemImage: "trash")
+ }
+
+ if #available(iOS 15, *) {
+ Button(role: .destructive) {
+ deleteGame(game: game)
+ } label: {
+ Label("Delete Game", systemImage: "trash")
+ }
+ } else {
+ Button(action: {
+ deleteGame(game: game)
+ }) {
+ Label("Delete Game", systemImage: "trash")
+ .foregroundColor(.red)
+ }
+ }
+
+ }
+ }
+ }
+}
+
+extension Game: Codable {
+ private enum CodingKeys: String, CodingKey {
+ case titleName, titleId, developer, version, fileURL, containerFolder, fileType
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ titleName = try container.decode(String.self, forKey: .titleName)
+ titleId = try container.decode(String.self, forKey: .titleId)
+ developer = try container.decode(String.self, forKey: .developer)
+ version = try container.decode(String.self, forKey: .version)
+ fileURL = try container.decode(URL.self, forKey: .fileURL)
+ containerFolder = try container.decode(URL.self, forKey: .containerFolder)
+ fileType = try container.decode(UTType.self, forKey: .fileType)
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(titleName, forKey: .titleName)
+ try container.encode(titleId, forKey: .titleId)
+ try container.encode(developer, forKey: .developer)
+ try container.encode(version, forKey: .version)
+ try container.encode(fileURL, forKey: .fileURL)
+ try container.encode(containerFolder, forKey: .containerFolder)
+ try container.encode(fileType, forKey: .fileType)
+ }
+}
+
+
+// MARK: - Empty Library View
+struct EmptyGameLibraryView: View {
+ @Binding var isSelectingGameFile: Bool
+
+ var body: some View {
+ VStack(spacing: 24) {
+ Spacer()
+
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 70))
+ .foregroundColor(.blue.opacity(0.7))
+ .padding(.bottom)
+
+ Text("No Games Found")
+ .font(.title2.bold())
+ .foregroundColor(.primary)
+
+ Text("Add ROM files to get started with your gaming experience")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal, 40)
+
+ Button {
+ isSelectingGameFile = true
+ } label: {
+ Label("Add Game", systemImage: "plus")
+ .font(.headline)
+ .padding(.horizontal, 24)
+ .padding(.vertical, 12)
+ .background(Color.blue)
+ .foregroundColor(.white)
+ .cornerRadius(10)
+ }
+ .padding(.top)
+
+ Spacer()
+ }
+ .padding()
+ }
+}
+
+// MARK: - Library Header
+struct GameLibraryHeader: View {
+ let totalGames: Int
+ let recentGames: Int
+ let firmwareVersion: String
+
+ var body: some View {
+ HStack(spacing: 16) {
+ // Stats cards
+ StatCard(
+ icon: "gamecontroller.fill",
+ title: "Total Games",
+ value: "\(totalGames)",
+ color: .blue
+ )
+
+ StatCard(
+ icon: "clock.fill",
+ title: "Recent",
+ value: "\(recentGames)",
+ color: .green
+ )
+
+ StatCard(
+ icon: "cpu",
+ title: "Firmware",
+ value: firmwareVersion == "0" ? "None" : firmwareVersion,
+ color: firmwareVersion == "0" ? .red : .orange
+ )
+ }
+ .padding(.horizontal)
+ .padding(.top, 8)
+ .padding(.bottom, 4)
+ }
+}
+
+struct StatCard: View {
+ let icon: String
+ let title: String
+ let value: String
+ let color: Color
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Image(systemName: icon)
+ .foregroundColor(color)
+ Text(title)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Text(value)
+ .font(.system(size: 16, weight: .bold))
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(10)
+ .background(color.opacity(0.1))
+ .cornerRadius(10)
+ }
+}
+
+// MARK: - Game Card View
+struct GameCardView: View {
+ let game: Game
+ @Binding var startemu: Game?
+ @Binding var games: [Game]
+ @Binding var isViewingGameInfo: Bool
+ @Binding var isSelectingGameUpdate: Bool
+ @Binding var isSelectingGameDLC: Bool
+ @Binding var gameRequirements: [GameRequirements]
+ @Binding var gameInfo: Game?
+ @Environment(\.colorScheme) var colorScheme
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ var gameRequirement: GameRequirements? {
+ gameRequirements.first(where: { $0.game_id == game.titleId })
+ }
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Game Icon
+ ZStack {
+ if let icon = game.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 130, height: 130)
+ .cornerRadius(8)
+ } else {
+ RoundedRectangle(cornerRadius: 8)
+ .fill(colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))
+ .frame(width: 130, height: 130)
+
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 40))
+ .foregroundColor(.gray)
+ }
+
+ // Play button overlay
+ Button {
+ startemu = game
+ } label: {
+ Circle()
+ .fill(Color.black.opacity(0.6))
+ .frame(width: 40, height: 40)
+ .overlay(
+ Image(systemName: "play.fill")
+ .font(.system(size: 16))
+ .foregroundColor(.white)
+ )
+ }
+ .offset(x: 0, y: 0)
+ .opacity(0.8)
+ }
+
+ // Game info
+ VStack(alignment: .leading, spacing: 4) {
+ Text(game.titleName)
+ .font(.system(size: 14, weight: .medium))
+ .multilineTextAlignment(.leading)
+ .foregroundColor(.primary)
+ .lineLimit(1)
+
+ Text(game.developer)
+ .font(.system(size: 12))
+ .foregroundColor(.secondary)
+ .lineLimit(1)
+
+ // Compatibility tag
+ if let req = gameRequirement {
+ HStack(spacing: 4) {
+ Text(req.compatibility)
+ .font(.system(size: 10, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(req.color)
+ .cornerRadius(4)
+
+ Text(req.device_memory)
+ .font(.system(size: 10, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(req.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.blue : Color.red)
+ .cornerRadius(4)
+ }
+ } else {
+ HStack(spacing: 4) {
+ Text("0GB")
+ .font(.system(size: 10, weight: .medium))
+ .foregroundColor(.clear)
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(Color.clear)
+ .cornerRadius(4)
+ }
+ }
+ }
+ .frame(width: 130, alignment: .leading)
+ .padding(.top, 8)
+ }
+ .onTapGesture {
+ startemu = game
+ }
+ }
+}
+
+// MARK: - Game List Row
+struct GameListRow: View {
+ let game: Game
+ @Binding var startemu: Game?
+ @Binding var games: [Game]
+ @Binding var isViewingGameInfo: Bool
+ @Binding var isSelectingGameUpdate: Bool
+ @Binding var isSelectingGameDLC: Bool
+ @Binding var gameRequirements: [GameRequirements]
+ @Binding var gameInfo: Game?
+ @StateObject private var settingsManager = PerGameSettingsManager.shared
+ @Binding var perGameSettings: Game?
+ @State var gametoDelete: Game?
+ @State var showGameDeleteConfirmation: Bool = false
+ @Environment(\.colorScheme) var colorScheme
+ @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
+ @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
+
+ @AppStorage("portal") var gamepo = false
+
+ var body: some View {
+ if #available(iOS 15.0, *) {
+ Button(action: {
+ startemu = game
+ }) {
+ HStack(spacing: 16) {
+ // Game Icon
+ if let icon = game.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 55, height: 55)
+ .cornerRadius(10)
+ } else {
+ ZStack {
+ RoundedRectangle(cornerRadius: 10)
+ .fill(colorScheme == .dark ?
+ Color(.systemGray5) : Color(.systemGray6))
+ .frame(width: 55, height: 55)
+
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 24))
+ .foregroundColor(.gray)
+ }
+ }
+
+ // Game Info
+ VStack(alignment: .leading, spacing: 4) {
+ Text(game.titleName)
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(.primary)
+ .multilineTextAlignment(.leading)
+
+ HStack {
+ Text(game.developer)
+ .font(.system(size: 12))
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.leading)
+
+ if !game.version.isEmpty && game.version != "0" {
+ Divider().frame(width: 1, height: 15)
+
+ Text("v\(game.version)")
+ .font(.system(size: 10))
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+
+ if $settingsManager.config.wrappedValue.contains(where: { $0.key == game.titleId }) {
+ Image(systemName: "gearshape.circle")
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .foregroundStyle(.blue)
+ .frame(width: 20, height: 20)
+ }
+
+ Spacer()
+
+ VStack(alignment: .leading) {
+ // Compatibility badges
+ HStack {
+ if let gameReq = gameRequirements.first(where: { $0.game_id == game.titleId }) {
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ HStack(spacing: 4) {
+ Text(gameReq.device_memory)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 4)
+ .background(
+ Capsule()
+ .fill(gameReq.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.blue : Color.red)
+ )
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .fixedSize(horizontal: true, vertical: false)
+ .layoutPriority(1)
+
+ Text(gameReq.compatibility)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 4)
+ .background(
+ Capsule()
+ .fill(gameReq.color)
+ )
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .fixedSize(horizontal: true, vertical: false)
+ .layoutPriority(1)
+ }
+ }
+
+ // Play button
+ Image(systemName: "play.circle.fill")
+ .font(.title3)
+ .foregroundColor(.blue)
+ }
+ }
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 4)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ .contentShape(Rectangle())
+ .contextMenu {
+ Section {
+ Button {
+ startemu = game
+ } label: {
+ Label("Play Now", systemImage: "play.fill")
+ }
+
+ Button {
+ gameInfo = game
+ isViewingGameInfo.toggle()
+
+ if game.titleName.lowercased() == "portal" || game.titleName.lowercased() == "portal 2" {
+ gamepo = true
+ }
+ } label: {
+ Label("Game Info", systemImage: "info.circle")
+ }
+
+ Button {
+ perGameSettings = game
+ } label: {
+ Label("\(game.titleName) Settings", systemImage: "gear")
+ }
+ }
+
+ Section {
+ Button {
+ gameInfo = game
+ isSelectingGameUpdate.toggle()
+ } label: {
+ Label("Update Manager", systemImage: "arrow.up.circle")
+ }
+
+ Button {
+ gameInfo = game
+ isSelectingGameDLC.toggle()
+ } label: {
+ Label("DLC Manager", systemImage: "plus.circle")
+ }
+ }
+
+ Section {
+ Button(role: .destructive) {
+ gametoDelete = game
+ showGameDeleteConfirmation.toggle()
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ }
+ .swipeActions(edge: .trailing) {
+ Button(role: .destructive) {
+ gametoDelete = game
+ showGameDeleteConfirmation.toggle()
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+
+ Button {
+ gameInfo = game
+ isViewingGameInfo.toggle()
+ } label: {
+ Label("Info", systemImage: "info.circle")
+ }
+ .tint(.blue)
+ }
+ .swipeActions(edge: .leading) {
+ Button {
+ startemu = game
+ } label: {
+ Label("Play", systemImage: "play.fill")
+ }
+ .tint(.green)
+ }
+ .confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
+ Button("Delete", role: .destructive) {
+ if let game = gametoDelete {
+ deleteGame(game: game)
+ }
+ }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?")
+ }
+ .listRowInsets(EdgeInsets())
+ .wow(colorScheme)
+ } else {
+ Button(action: {
+ startemu = game
+ }) {
+ HStack(spacing: 16) {
+ // Game Icon
+ if let icon = game.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: 55, height: 55)
+ .cornerRadius(10)
+ } else {
+ ZStack {
+ RoundedRectangle(cornerRadius: 10)
+ .fill(colorScheme == .dark ?
+ Color(.systemGray5) : Color(.systemGray6))
+ .frame(width: 55, height: 55)
+
+ Image(systemName: "gamecontroller.fill")
+ .font(.system(size: 24))
+ .foregroundColor(.gray)
+ }
+ }
+
+ // Game Info
+ VStack(alignment: .leading, spacing: 4) {
+ Text(game.titleName)
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(.primary)
+ .multilineTextAlignment(.leading)
+
+ HStack {
+ Text(game.developer)
+ .font(.system(size: 14))
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.leading)
+
+ if !game.version.isEmpty && game.version != "0" {
+ Text("•")
+ .foregroundColor(.secondary)
+
+ Text("v\(game.version)")
+ .font(.system(size: 14))
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+
+ Spacer()
+
+ VStack(alignment: .leading) {
+ // Compatibility badges
+ HStack {
+ if let gameReq = gameRequirements.first(where: { $0.game_id == game.titleId }) {
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ HStack(spacing: 4) {
+ // Memory requirement badge
+ Text(gameReq.device_memory)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 4)
+ .background(
+ Capsule()
+ .fill(gameReq.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.blue : Color.red)
+ )
+
+ // Compatibility badge
+ Text(gameReq.compatibility)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 4)
+ .background(
+ Capsule()
+ .fill(gameReq.color)
+ )
+ }
+ }
+
+ // Play button
+ Image(systemName: "play.circle.fill")
+ .font(.title3)
+ .foregroundColor(.blue)
+ }
+ }
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 4)
+ .frame(width: .infinity, height: .infinity)
+ }
+ .contentShape(Rectangle())
+ .contextMenu {
+ Section {
+ Button {
+ startemu = game
+ } label: {
+ Label("Play Now", systemImage: "play.fill")
+ }
+
+ Button {
+ gameInfo = game
+ isViewingGameInfo.toggle()
+
+ if game.titleName.lowercased() == "portal" || game.titleName.lowercased() == "portal 2" {
+ gamepo = true
+ }
+ } label: {
+ Label("Game Info", systemImage: "info.circle")
+ }
+ }
+
+ Section {
+ Button {
+ gameInfo = game
+ isSelectingGameUpdate.toggle()
+ } label: {
+ Label("Update Manager", systemImage: "arrow.up.circle")
+ }
+
+ Button {
+ gameInfo = game
+ isSelectingGameDLC.toggle()
+ } label: {
+ Label("DLC Manager", systemImage: "plus.circle")
+ }
+ }
+
+ Section {
+ Button {
+ gametoDelete = game
+ showGameDeleteConfirmation.toggle()
+ } label: {
+ Label("Delete", systemImage: "trash")
+ .foregroundColor(.red)
+ }
+ }
+ }
+ .alert(isPresented: $showGameDeleteConfirmation) {
+ Alert(
+ title: Text("Are you sure you want to delete this game?"),
+ message: Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?"),
+ primaryButton: .destructive(Text("Delete")) {
+ if let game = gametoDelete {
+ deleteGame(game: game)
+ }
+ },
+ secondaryButton: .cancel()
+ )
+ }
+ .listRowInsets(EdgeInsets())
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5))
+ )
+ }
+ }
+
+ private func deleteGame(game: Game) {
+ let fileManager = FileManager.default
+ do {
+ try fileManager.removeItem(at: game.fileURL)
+ games.removeAll { $0.id == game.id }
+ } catch {
+ // print("Error deleting game: \(error)")
+ }
+ }
+}
+
+struct GameRequirements: Codable {
+ var game_id: String
+ var compatibility: String
+ var device_memory: String
+ var memoryInt: Int {
+ var devicemem = device_memory
+ devicemem.removeLast(2)
+ // print(devicemem)
+ return Int(devicemem) ?? 0
+ }
+
+ var color: Color {
+ switch compatibility {
+ case "Perfect":
+ return .green
+ case "Playable":
+ return .yellow
+ case "Menu":
+ return .orange
+ case "Boots":
+ return .red
+ case "Nothing":
+ return .black
+ default:
+ return .clear
+ }
+ }
+}
+
+func pullGameCompatibility(completion: @escaping (Result<[GameRequirements], Error>) -> Void) {
+ if let cachedData = GameCompatibiliryCache.shared.getCachedData() {
+ completion(.success(cachedData))
+ return
+ }
+
+ guard let url = URL(string: "https://melonx.net/api/game_entries") else {
+ completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
+ return
+ }
+
+ let task = URLSession.shared.dataTask(with: url) { data, response, error in
+ if let error = error {
+ completion(.failure(error))
+ return
+ }
+
+ guard let data = data else {
+ completion(.failure(NSError(domain: "No data", code: 0, userInfo: nil)))
+ return
+ }
+
+ do {
+ let decodedData = try JSONDecoder().decode([GameRequirements].self, from: data)
+ GameCompatibiliryCache.shared.setCachedData(decodedData)
+ completion(.success(decodedData))
+ } catch {
+ completion(.failure(error))
+ }
+ }
+
+ task.resume()
+}
+
+extension View {
+ func wow(_ colorScheme: ColorScheme) -> some View {
+ if #available(iOS 26.0, *) {
+ return self
+ .glassEffect(Glass.regular, in:
+ RoundedRectangle(cornerRadius: 12)
+ )
+ } else {
+ return self
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5))
+ )
+ }
+ }
+}
+
+
+extension View {
+ @available(iOS, introduced: 14.0, deprecated: 19.0, message: "")
+ func glassEffect(_ style: Glass, in shape: some Shape) -> some View {
+ return self
+ }
+}
+
+@available(iOS, introduced: 14.0, deprecated: 19.0, message: "")
+struct Glass: Hashable {
+ static var regular = Glass()
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/JIT/JITPopover.swift
similarity index 91%
rename from src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/JIT/JITPopover.swift
index b5e219d9d..b779a0381 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/JIT/JITPopover.swift
@@ -9,7 +9,7 @@ import SwiftUI
struct JITPopover: View {
var onJITEnabled: () -> Void
- @Environment(\.dismiss) var dismiss
+ @Environment(\.presentationMode) var presentationMode
@State var isJIT: Bool = false
var body: some View {
@@ -35,7 +35,7 @@ struct JITPopover: View {
if isJIT {
- dismiss()
+ presentationMode.wrappedValue.dismiss()
onJITEnabled()
Ryujinx.shared.ryuIsJITEnabled()
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/Logging/Logs.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/Logging/Logs.swift
new file mode 100644
index 000000000..e3755aa6c
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/Logging/Logs.swift
@@ -0,0 +1,66 @@
+//
+// LogEntry.swift
+// MeloNX
+//
+// Created by Stossy11 on 09/02/2025.
+//
+
+import SwiftUI
+import Combine
+
+struct LogFileView: View {
+ @StateObject var logsModel = LogViewModel()
+ @State private var showingLogs = false
+
+ public var isfps: Bool
+
+ private let fileManager = FileManager.default
+ private let maxDisplayLines = 4
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(logsModel.logs.suffix(maxDisplayLines), id: \.self) { log in
+ Text(log)
+ .font(.caption)
+ .foregroundColor(.white)
+ .padding(4)
+ .background(Color.black.opacity(0.7))
+ .cornerRadius(4)
+ .transition(.opacity)
+ }
+ }
+ .padding()
+ }
+
+ private func stopLogFileWatching() {
+ showingLogs = false
+ }
+}
+
+
+class LogViewModel: ObservableObject {
+ @Published var logs: [String] = []
+ private var cancellables = Set()
+
+ init() {
+ _ = LogCapture.shared
+
+ NotificationCenter.default.publisher(for: .newLogCaptured)
+ .receive(on: RunLoop.main)
+ .sink { [weak self] _ in
+ self?.updateLogs()
+ }
+ .store(in: &cancellables)
+
+ updateLogs()
+ }
+
+ func updateLogs() {
+ logs = LogCapture.shared.capturedLogs
+ }
+
+ func clearLogs() {
+ LogCapture.shared.capturedLogs = []
+ updateLogs()
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/AppIcon/AppIconSwitcher.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/AppIcon/AppIconSwitcher.swift
new file mode 100644
index 000000000..aeea14df4
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/AppIcon/AppIconSwitcher.swift
@@ -0,0 +1,236 @@
+//
+// AppIconSwitcher.swift
+// MeloNX
+//
+// Created by Stossy11 on 02/06/2025.
+//
+
+import SwiftUI
+
+struct AppIcon: Identifiable, Equatable {
+ var id: String { creator }
+
+ var iconNames: [String: String]
+ var creator: String
+}
+
+struct AppIconSwitcherView: View {
+ @Environment(\.dismiss) private var dismiss
+ @State var appIcons: [AppIcon] = [
+ AppIcon(iconNames: ["Default": UIImage.appIcon(), "Dark Mode": "DarkMode", "Round": "RoundAppIcon"], creator: "CycloKid"),
+ AppIcon(iconNames: ["Pixel Default": "PixelAppIcon", "Pixel Round": "PixelRoundAppIcon"], creator: "Nobody"),
+ AppIcon(iconNames: ["\"UwU\"": "uwuAppIcon"], creator: "𝒰𝓃𝓀𝓃𝑜𝓌𝓃")
+
+ ]
+
+ @State var columns: [GridItem] = [
+ GridItem(.flexible(), spacing: 20),
+ GridItem(.flexible(), spacing: 20),
+ GridItem(.flexible(), spacing: 20)
+ ]
+ @State private var currentIconName: String? = nil
+ @State var refresh = 0
+
+ var body: some View {
+ NavigationView {
+ ZStack {
+ LinearGradient(
+ gradient: Gradient(colors: [
+ Color(.systemBackground).opacity(0.95),
+ Color(.systemGroupedBackground)
+ ]),
+ startPoint: .top,
+ endPoint: .bottom
+ )
+ .ignoresSafeArea()
+
+ ScrollView {
+ LazyVStack(spacing: 32) {
+ ForEach(appIcons.indices, id: \.self) { index in
+ let iconGroup = appIcons[index]
+
+ VStack(alignment: .leading, spacing: 20) {
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(iconGroup.creator)
+ .font(.title2)
+ .fontWeight(.bold)
+ .foregroundStyle(.primary)
+
+ Text("\(iconGroup.iconNames.count) icons")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ }
+ .padding(.horizontal, 24)
+
+ LazyVGrid(columns: columns, spacing: 20) {
+ ForEach(Array(iconGroup.iconNames.keys.sorted()), id: \.self) { key in
+ if let iconName = iconGroup.iconNames[key] {
+ Button {
+ selectIcon(iconName)
+ } label: {
+ ZStack {
+ AppIconView(app: (iconName, key))
+
+ if iconName == currentIconName ?? UIImage.appIcon() {
+ VStack {
+ HStack {
+ Spacer()
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 24, weight: .bold))
+ .foregroundStyle(.white)
+ .background(
+ Circle()
+ .fill(
+ LinearGradient(
+ colors: [.blue, .purple],
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ )
+ .frame(width: 28, height: 28)
+ )
+ }
+ Spacer()
+ }
+ .frame(width: 80, height: 80)
+ .offset(x: 6, y: -6)
+ }
+ }
+ }
+ .buttonStyle(PlainButtonStyle())
+ .scaleEffect(isCurrentIcon(iconName) ? 0.95 : 1.0)
+ .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isCurrentIcon(iconName))
+ }
+ }
+ }
+ .padding(.horizontal, 24)
+ }
+
+ // Stylized divider
+ if index < appIcons.count - 1 {
+ Rectangle()
+ .fill(
+ LinearGradient(
+ colors: [.clear, Color(.separator), .clear],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ )
+ .frame(height: 1)
+ .padding(.horizontal, 40)
+ }
+ }
+ }
+ .padding(.vertical, 32)
+ }
+ }
+ .navigationTitle("Choose App Icon")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("Done") {
+ dismiss()
+ }
+ .font(.system(size: 16, weight: .semibold))
+ .foregroundStyle(.blue)
+ }
+ }
+ }
+ .onAppear(perform: setupColumns)
+ .onAppear(perform: getCurrentIconName)
+ }
+
+ private func setupColumns() {
+ if #available(iOS 18.5, *) {
+ //
+ } else {
+ if checkforOld() {
+ if let value = appIcons[0].iconNames.removeValue(forKey: "Round") {
+ appIcons[0].iconNames["PomeloNX"] = value
+ }
+
+ if let value = appIcons[1].iconNames.removeValue(forKey: "Pixel Round") {
+ appIcons[1].iconNames["Pixel PomeloNX"] = value
+ }
+ }
+ }
+ }
+
+ private func getCurrentIconName() {
+ currentIconName = UIApplication.shared.alternateIconName ?? UIImage.appIcon()
+ }
+
+ private func isCurrentIcon(_ iconName: String) -> Bool {
+ let currentIcon = UIApplication.shared.alternateIconName ?? UIImage.appIcon()
+ return currentIcon == iconName
+ }
+
+ private func selectIcon(_ iconName: String) {
+ // Haptic feedback
+ let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
+ impactFeedback.impactOccurred()
+
+ if iconName == UIImage.appIcon() {
+ UIApplication.shared.setAlternateIconName(nil) { error in
+ if let error = error {
+ print("Error setting icon: \(error)")
+ } else {
+ DispatchQueue.main.async {
+ currentIconName = nil
+ refresh = Int.random(in: 0...100)
+ }
+ }
+ }
+ } else {
+ UIApplication.shared.setAlternateIconName(iconName) { error in
+ if let error = error {
+ print("Error setting icon: \(error)")
+ } else {
+ DispatchQueue.main.async {
+ currentIconName = iconName
+ refresh = Int.random(in: 0...100)
+ }
+ }
+ }
+ }
+ }
+}
+
+struct AppIconView: View {
+ let app: (String, String)
+
+ var body: some View {
+ VStack(spacing: 7) {
+ ZStack {
+ Image(uiImage: UIImage(named: app.0)!)
+ .resizable()
+ .cornerRadius(15)
+ .frame(width: 62, height: 62)
+ .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
+ }
+
+ Text(app.1)
+ .font(.system(size: 12, weight: .medium))
+ .foregroundColor(.white)
+ .multilineTextAlignment(.center)
+ .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
+ .frame(width: 100)
+ .lineLimit(1)
+ }
+ }
+}
+
+extension UIImage {
+ static func appIcon() -> String {
+ if let icons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any],
+ let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
+ let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
+ let lastIcon = iconFiles.last {
+ return lastIcon
+ }
+ return "AppIcon"
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift
new file mode 100644
index 000000000..c53c8a99a
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/Per-Game Settings/PerGameSettingsView.swift
@@ -0,0 +1,707 @@
+//
+// PerGameSettingsView.swift
+// MeloNX
+//
+// Created by Stossy11 on 12/06/2025.
+//
+
+import SwiftUI
+
+protocol PerGameSettingsManaging: ObservableObject {
+ var config: [String: Ryujinx.Arguments] { get set }
+
+ func debouncedSave()
+ func saveSettings()
+ func loadSettings()
+
+ static func loadSettings() -> [String: Ryujinx.Arguments]?
+}
+
+
+
+class PerGameSettingsManager: PerGameSettingsManaging {
+ @Published var config: [String: Ryujinx.Arguments] {
+ didSet {
+ debouncedSave()
+ }
+ }
+
+ private var saveWorkItem: DispatchWorkItem?
+
+ public static var shared = PerGameSettingsManager()
+
+ private init() {
+ self.config = PerGameSettingsManager.loadSettings() ?? [:]
+ }
+
+ func debouncedSave() {
+ saveWorkItem?.cancel()
+
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ self.saveSettings()
+ }
+
+ saveWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
+ }
+
+ func saveSettings() {
+ do {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ let data = try encoder.encode(config)
+
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config-pergame.json")
+
+ try data.write(to: fileURL)
+ print("Settings saved successfully")
+ } catch {
+ print("Failed to save settings: \(error)")
+ }
+ }
+
+ static func loadSettings() -> [String: Ryujinx.Arguments]? {
+ do {
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config-pergame.json")
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ print("Config file does not exist, creating new config")
+ return nil
+ }
+
+ let data = try Data(contentsOf: fileURL)
+
+ let decoder = JSONDecoder()
+ let configs = try decoder.decode([String: Ryujinx.Arguments].self, from: data)
+ return configs
+ } catch {
+ print("Failed to load settings: \(error)")
+ return nil
+ }
+ }
+
+ func loadSettings() {
+ self.config = PerGameSettingsManager.loadSettings() ?? [:]
+ }
+}
+
+
+struct PerGameSettingsView: View {
+
+ @StateObject private var settingsManager: PerGameSettingsManager
+
+ var titleId: String
+
+ init(titleId: String, manager: any PerGameSettingsManaging = PerGameSettingsManager.shared) {
+ self._settingsManager = StateObject(wrappedValue: manager as! PerGameSettingsManager)
+ self.titleId = titleId
+ }
+
+
+ private var config: Binding {
+ return Binding {
+ return settingsManager.config[titleId] ?? Ryujinx.Arguments()
+ } set: { newValue in
+ settingsManager.config[titleId] = newValue
+ settingsManager.debouncedSave()
+ }
+ }
+
+ var memoryManagerModes = [
+ ("HostMapped", "Host (fast)"),
+ ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
+ ("SoftwarePageTable", "Software (slow)"),
+ ]
+
+
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ @State private var showResolutionInfo = false
+ @State private var showAnisotropicInfo = false
+ @State private var showControllerInfo = false
+ @State private var showAppIconSwitcher = false
+ @State private var searchText = ""
+ @StateObject var ryujinx = Ryujinx.shared
+ @Environment(\.dismiss) var dismiss
+ @Environment(\.colorScheme) var colorScheme
+ @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
+ @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
+
+ @State private var selectedCategory: PerSettingsCategory = .graphics
+
+ @StateObject var metalHudEnabler = MTLHud.shared
+
+ var filteredMemoryModes: [(String, String)] {
+ guard !searchText.isEmpty else { return memoryManagerModes }
+ return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
+ }
+
+ var appVersion: String {
+ guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
+ return "Unknown"
+ }
+ return version
+ }
+
+ @FocusState private var isArgumentsKeyboardVisible: Bool
+
+
+ @State private var selectedView = "Data Management"
+ @State private var sidebar = true
+
+ enum PerSettingsCategory: String, CaseIterable, Identifiable {
+ case graphics = "Graphics"
+ case system = "System"
+ case advanced = "Advanced"
+
+ var id: String { self.rawValue }
+
+ var icon: String {
+ switch self {
+ case .graphics: return "paintbrush.fill"
+ case .system: return "gearshape.fill"
+ case .advanced: return "terminal.fill"
+ }
+ }
+ }
+
+ var body: some View {
+ iOSNav {
+ ZStack {
+ Color(UIColor.systemBackground)
+ .ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 12) {
+ ForEach(PerSettingsCategory.allCases, id: \.id) { category in
+ CategoryButton(
+ title: category.rawValue,
+ icon: category.icon,
+ isSelected: selectedCategory == category
+ ) {
+ selectedCategory = category
+ }
+ }
+ }
+ .padding(.horizontal)
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ // Settings content
+ ScrollView {
+ VStack(spacing: 24) {
+ switch selectedCategory {
+ case .graphics:
+ graphicsSettings
+ .padding(.top)
+ case .system:
+ systemSettings
+ .padding(.top)
+ case .advanced:
+ advancedSettings
+ .padding(.top)
+
+ }
+
+ Spacer(minLength: 50)
+ }
+ .padding(.bottom)
+ }
+ .scrollDismissesKeyboardIfAvailable()
+ }
+ }
+ .navigationTitle("Settings")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar {
+ ToolbarItem(placement: .confirmationAction) {
+ Button("Done") {
+ settingsManager.debouncedSave()
+ dismiss()
+ }
+ }
+
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Reset") {
+ dismiss()
+ settingsManager.config[titleId] = nil
+ settingsManager.saveSettings()
+ }
+ }
+ }
+ // .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
+ .onAppear {
+
+ // if let configs = SettingsManager.loadSettings() {
+ // settingsManager.loadSettings()
+ // } else {
+ // settingsManager.saveSettings()
+ //}
+
+ print(titleId)
+
+ if settingsManager.config[titleId] == nil {
+ settingsManager.config[titleId] = Ryujinx.Arguments()
+ settingsManager.debouncedSave()
+ }
+ }
+ }
+ }
+
+ // MARK: - Graphics Settings
+
+ private var graphicsSettings: some View {
+ SettingsSection(title: "Graphics & Performance") {
+ // Resolution scale card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showResolutionInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showResolutionInfo) {
+ Alert(
+ title: Text("Resolution Scale"),
+ message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: config.resscale, in: 0.1...3.0, step: 0.05)
+
+ HStack {
+ Text("0.1x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(config.resscale.wrappedValue, specifier: "%.2f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("3.0x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // Anisotropic filtering card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ labelWithIcon("Max Anisotropic Filtering", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showAnisotropicInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showAnisotropicInfo) {
+ Alert(
+ title: Text("Max Anisotropic Filtering"),
+ message: Text("Adjust the internal Anisotropic filtering. Higher values improve texture quality at angles but may reduce performance. Default at 0 lets game decide."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: config.maxAnisotropy, in: 0...16.0, step: 0.1)
+
+ HStack {
+ Text("Off")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(config.maxAnisotropy.wrappedValue, specifier: "%.1f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("16x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // Toggle options card
+ SettingsCard {
+ VStack(spacing: 4) {
+ PerSettingsToggle(isOn: config.disableShaderCache, icon: "memorychip", label: "Shader Cache")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.disablevsync, icon: "arrow.triangle.2.circlepath", label: "Disable VSync")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.enableTextureRecompression, icon: "rectangle.compress.vertical", label: "Texture Recompression")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.disableDockedMode, icon: "dock.rectangle", label: "Docked Mode")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.macroHLE, icon: "gearshape", label: "Macro HLE")
+ }
+ }
+
+ // Aspect ratio card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
+ .font(.headline)
+
+ if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) {
+ Picker(selection: config.aspectRatio) {
+ ForEach(AspectRatio.allCases, id: \.self) { ratio in
+ Text(ratio.displayName).tag(ratio)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.segmented)
+ } else {
+ Picker(selection: config.aspectRatio) {
+ ForEach(AspectRatio.allCases, id: \.self) { ratio in
+ Text(ratio.displayName).tag(ratio)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+ }
+ }
+
+
+ // MARK: - System Settings
+
+ private var systemSettings: some View {
+ SettingsSection(title: "System Configuration") {
+ // Language and region card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ VStack(alignment: .leading, spacing: 8) {
+ labelWithIcon("System Language", iconName: "character.bubble")
+ .font(.headline)
+
+ Picker(selection: config.language) {
+ ForEach(SystemLanguage.allCases, id: \.self) { language in
+ Text(language.displayName).tag(language)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+
+ Divider()
+
+ VStack(alignment: .leading, spacing: 8) {
+ labelWithIcon("Region", iconName: "globe")
+ .font(.headline)
+
+ Picker(selection: config.regioncode) {
+ ForEach(SystemRegionCode.allCases, id: \.self) { region in
+ Text(region.displayName).tag(region)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+
+ // CPU options card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("CPU Configuration")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Memory Manager Mode")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Picker(selection: config.memoryManagerMode) {
+ ForEach(filteredMemoryModes, id: \.0) { key, displayName in
+ Text(displayName).tag(key)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.segmented)
+ }
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.disablePTC, icon: "cpu", label: "Disable PTC")
+
+ if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
+ Divider()
+
+ if #available(iOS 16.4, *) {
+ PerSettingsToggle(isOn: .constant(false), icon: "bolt", label: "Hypervisor")
+ .disabled(true)
+ } else if checkAppEntitlement("com.apple.private.hypervisor") {
+ PerSettingsToggle(isOn: config.hypervisor, icon: "bolt", label: "Hypervisor")
+ }
+ }
+ }
+ }
+
+ // Controller options card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("Controller Configuration")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
+
+ }
+ }
+ }
+ }
+
+ // MARK: - Advanced Settings
+
+ private var advancedSettings: some View {
+ SettingsSection(title: "Advanced Options") {
+ // Debug options card
+ SettingsCard {
+ VStack(spacing: 4) {
+ PerSettingsToggle(isOn: config.debuglogs, icon: "exclamationmark.bubble", label: "Debug Logs")
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.tracelogs, icon: "waveform.path", label: "Trace Logs")
+ }
+ }
+
+ // Advanced toggles card
+ SettingsCard {
+ VStack(spacing: 4) {
+
+ PerSettingsToggle(isOn: config.dfsIntegrityChecks, icon: "checkmark.shield", label: "Disable FS Integrity Checks")
+ .accentColor(.red)
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.expandRam, icon: "exclamationmark.bubble", label: "Expand Guest RAM")
+ .accentColor(.red)
+ .disabled(totalMemory < 5723)
+
+ Divider()
+
+ PerSettingsToggle(isOn: config.ignoreMissingServices, icon: "waveform.path", label: "Ignore Missing Services")
+ .accentColor(.red)
+ }
+ }
+
+ // Additional args card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Additional Arguments")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ let binding = Binding(
+ get: {
+ config.additionalArgs.wrappedValue.joined(separator: ", ")
+ },
+ set: { newValue in
+ let args = newValue
+ .split(separator: ",")
+ .map { $0.trimmingCharacters(in: .whitespaces) }
+ config.additionalArgs.wrappedValue = args
+ }
+ )
+
+
+ if #available(iOS 15.0, *) {
+ TextField("Separate arguments with commas", text: binding)
+ .font(.system(.body, design: .monospaced))
+ .textFieldStyle(.roundedBorder)
+ .textInputAutocapitalization(.none)
+ .disableAutocorrection(true)
+ .padding(.vertical, 4)
+ .toolbar {
+ ToolbarItem(placement: .keyboard) {
+ Button("Dismiss") {
+ isArgumentsKeyboardVisible = false
+ }
+ }
+ }
+ .focused($isArgumentsKeyboardVisible)
+ } else {
+ TextField("Separate arguments with commas", text: binding)
+ .font(.system(.body, design: .monospaced))
+ .textFieldStyle(.roundedBorder)
+ .disableAutocorrection(true)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+
+ // Page size info card
+ SettingsCard {
+ HStack {
+ labelWithIcon("Page Size", iconName: "textformat.size")
+ Spacer()
+ Text("\(String(Int(getpagesize())))")
+ .font(.system(.body, design: .monospaced))
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // MARK: - Miscellaneous Settings
+
+ private var miscSettings: some View {
+ SettingsSection(title: "Miscellaneous Options") {
+ SettingsCard {
+ VStack(spacing: 4) {
+ PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
+ }
+ }
+ }
+ }
+
+ // MARK: - Helper Functions
+
+
+ func getGPUInfo() -> String? {
+ let device = MTLCreateSystemDefaultDevice()
+ return device?.name
+ }
+
+ @ViewBuilder
+ private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
+ HStack(spacing: 8) {
+ if iconName.hasSuffix(".svg") {
+ if let flipimage, flipimage {
+ SVGView(svgName: iconName, color: .blue)
+ // .symbolRenderingMode(.hierarchical)
+ .frame(width: 20, height: 20)
+ .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
+ } else {
+ SVGView(svgName: iconName, color: .blue)
+ // .symbolRenderingMode(.hierarchical)
+ .frame(width: 20, height: 20)
+ }
+ } else if !iconName.isEmpty {
+ Image(systemName: iconName)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundColor(.blue)
+ }
+ Text(text)
+ }
+ .font(.body)
+ }
+}
+
+
+// MARK: - Supporting Views
+
+// PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
+
+struct PerSettingsCard: View {
+ @Environment(\.colorScheme) var colorScheme
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+ let content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ content
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(colorScheme == .dark ? Color(.systemGray6) : Color.white)
+ .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
+ )
+ .padding(.horizontal)
+ }
+}
+
+struct PerSettingsToggle: View {
+ @Binding var isOn: Bool
+ let icon: String
+ let label: String
+ var disabled: Bool = false
+ @AppStorage("toggleGreen") var toggleGreen: Bool = false
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+
+ var body: some View {
+ Toggle(isOn: $isOn) {
+ HStack(spacing: 8) {
+ if icon.hasSuffix(".svg") {
+ SVGView(svgName: icon, color: .blue)
+ .frame(width: 20, height: 20)
+ } else {
+ Image(systemName: icon)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundColor(.blue)
+ }
+
+ Text(label)
+ .font(.body)
+ }
+ }
+ .toggleStyle(SwitchToggleStyle(tint: .blue))
+ .disabled(disabled)
+ .padding(.vertical, 6)
+ }
+
+ func disabled(_ disabled: Bool) -> PerSettingsToggle {
+ var view = self
+ view.disabled = disabled
+ return view
+ }
+
+ func accentColor(_ color: Color) -> PerSettingsToggle {
+ var view = self
+ return view
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift
new file mode 100644
index 000000000..4205d284f
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift
@@ -0,0 +1,1669 @@
+//
+// SettingsView.swift
+// MeloNX
+//
+// Created by Stossy11 on 25/11/2024.
+//
+
+import SwiftUI
+import SwiftSVG
+import UIKit
+
+
+class SplitViewController: UISplitViewController {
+ private let sidebarViewController: UIViewController
+ private let contentViewController: UIViewController
+
+ init(sidebarViewController: UIViewController, contentViewController: UIViewController) {
+ self.sidebarViewController = sidebarViewController
+ self.contentViewController = contentViewController
+ super.init(style: .doubleColumn)
+
+ self.preferredDisplayMode = .oneBesideSecondary
+ self.preferredSplitBehavior = .tile
+ self.presentsWithGesture = true
+
+ self.setViewController(sidebarViewController, for: .primary)
+ self.setViewController(contentViewController, for: .secondary)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ self.primaryBackgroundStyle = .sidebar
+
+ let displayModeButtonItem = self.displayModeButtonItem
+ contentViewController.navigationItem.leftBarButtonItem = displayModeButtonItem
+ }
+
+ func showSidebar() {
+ self.preferredDisplayMode = .oneBesideSecondary
+ }
+
+ func hideSidebar() {
+ self.preferredDisplayMode = .secondaryOnly
+ }
+
+ func toggleSidebar() {
+ if self.displayMode == .oneBesideSecondary {
+ self.preferredDisplayMode = .secondaryOnly
+ } else {
+ self.preferredDisplayMode = .oneBesideSecondary
+ }
+ }
+}
+
+struct SidebarView: View {
+ var sidebar: () -> AnyView
+ var content: () -> Content
+ @Binding var showSidebar: Bool
+
+ init(sidebar: @escaping () -> AnyView, content: @escaping () -> Content, showSidebar: Binding) {
+ self.sidebar = sidebar
+ self.content = content
+ self._showSidebar = showSidebar
+ }
+
+ var body: some View {
+ SidebarViewRepresentable(
+ sidebar: sidebar(),
+ content: content(),
+ showSidebar: $showSidebar
+ )
+ }
+}
+
+struct SidebarViewRepresentable: UIViewControllerRepresentable {
+ var sidebar: Sidebar
+ var content: Content
+ @Binding var showSidebar: Bool
+
+ func makeUIViewController(context: Context) -> SplitViewController {
+ let sidebarVC = UIHostingController(rootView: sidebar)
+ let contentVC = UINavigationController(rootViewController: UIHostingController(rootView: content))
+
+ let splitVC = SplitViewController(sidebarViewController: sidebarVC, contentViewController: contentVC)
+ splitVC.setOverrideTraitCollection(
+ UITraitCollection(horizontalSizeClass: .regular),
+ forChild: splitVC
+ )
+ return splitVC
+ }
+
+ func updateUIViewController(_ uiViewController: SplitViewController, context: Context) {
+ if let sidebarVC = uiViewController.viewController(for: .primary) as? UIHostingController {
+ sidebarVC.rootView = sidebar
+ }
+ if let navController = uiViewController.viewController(for: .secondary) as? UINavigationController,
+ let contentVC = navController.topViewController as? UIHostingController {
+ contentVC.rootView = content
+ }
+
+ if showSidebar {
+ uiViewController.showSidebar()
+ } else {
+ uiViewController.hideSidebar()
+ }
+ }
+
+ static func dismantleUIViewController(_ uiViewController: SplitViewController, coordinator: Coordinator) {
+ }
+}
+
+class SettingsManager: ObservableObject {
+ @Published var config: Ryujinx.Arguments {
+ didSet {
+ debouncedSave()
+ }
+ }
+
+ private var saveWorkItem: DispatchWorkItem?;
+
+ public static var shared = SettingsManager()
+
+ private init() {
+ self.config = SettingsManager.loadSettings() ?? Ryujinx.Arguments()
+ }
+
+ func debouncedSave() {
+ saveWorkItem?.cancel()
+
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ self.saveSettings()
+ }
+
+ saveWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
+ }
+
+ func saveSettings() {
+ do {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ let data = try encoder.encode(config)
+
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ try data.write(to: fileURL)
+ print("Settings saved successfully")
+ } catch {
+ print("Failed to save settings: \(error)")
+ }
+ }
+
+ static func loadSettings() -> Ryujinx.Arguments? {
+ do {
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ print("Config file does not exist, creating new config")
+ return nil
+ }
+
+ let data = try Data(contentsOf: fileURL)
+
+ let decoder = JSONDecoder()
+ let configs = try decoder.decode(Ryujinx.Arguments.self, from: data)
+ return configs
+ } catch {
+ print("Failed to load settings: \(error)")
+ return nil
+ }
+ }
+
+ func loadSettings() {
+ do {
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ print("Config file does not exist, creating new config")
+ saveSettings()
+ return
+ }
+
+ let data = try Data(contentsOf: fileURL)
+
+ let decoder = JSONDecoder()
+ let configs = try decoder.decode(Ryujinx.Arguments.self, from: data)
+
+ self.config = configs
+ } catch {
+ print("Failed to load settings: \(error)")
+ }
+ }
+}
+
+
+
+struct SettingsViewNew: View {
+ @StateObject private var settingsManager = SettingsManager.shared
+
+ private var config: Binding {
+ $settingsManager.config
+ }
+
+ @Binding var MoltenVKSettings: [MoltenVKSettings]
+
+ @Binding var controllersList: [Controller]
+ @Binding var currentControllers: [Controller]
+
+ @Binding var onscreencontroller: Controller
+ @AppStorage("useTrollStore") var useTrollStore: Bool = false
+
+ @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
+ @AppStorage("stikJIT") var stikJIT: Bool = false
+
+ @AppStorage("ignoreJIT") var ignoreJIT: Bool = false
+
+ var memoryManagerModes = [
+ ("HostMapped", "Host (fast)"),
+ ("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
+ ("SoftwarePageTable", "Software (slow)"),
+ ]
+
+ @AppStorage("RyuDemoControls") var ryuDemo: Bool = false
+
+ @AppStorage("showScreenShotButton") var ssb: Bool = false
+
+ @AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = false
+ @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
+
+ @AppStorage("performacehud") var performacehud: Bool = false
+
+ @AppStorage("swapBandA") var swapBandA: Bool = false
+
+ @AppStorage("oldWindowCode") var windowCode: Bool = false
+
+ @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
+
+ @AppStorage("On-ScreenControllerOpacity") var controllerOpacity: Double = 1.0
+
+ @AppStorage("hasbeenfinished") var finishedStorage: Bool = false
+
+ @AppStorage("showlogsloading") var showlogsloading: Bool = true
+
+ @AppStorage("showlogsgame") var showlogsgame: Bool = false
+
+ @AppStorage("toggleGreen") var toggleGreen: Bool = false
+
+ @AppStorage("stick-button") var stickButton = false
+ @AppStorage("waitForVPN") var waitForVPN = false
+
+ @AppStorage("HideButtons") var hideButtonsJoy = false
+
+ @AppStorage("checkForUpdate") var checkForUpdate: Bool = true
+
+ @AppStorage("disableTouch") var disableTouch = false
+
+ @AppStorage("disableTouch") var blackScreen = false
+
+ @AppStorage("location-enabled") var locationenabled: Bool = false
+
+ @AppStorage("runOnMainThread") var runOnMainThread = false
+
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+
+ @AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState()
+
+ let totalMemory = ProcessInfo.processInfo.physicalMemory
+
+ @AppStorage("lockInApp") var restartApp = false
+
+ @State private var showResolutionInfo = false
+ @State private var showAnisotropicInfo = false
+ @State private var showControllerInfo = false
+ @State private var showAppIconSwitcher = false
+ @State private var searchText = ""
+ @AppStorage("portal") var gamepo = false
+ @StateObject var ryujinx = Ryujinx.shared
+ @Environment(\.colorScheme) var colorScheme
+ @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
+ @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
+
+ @State private var selectedCategory: SettingsCategory = .graphics
+
+ @StateObject var metalHudEnabler = MTLHud.shared
+
+ var filteredMemoryModes: [(String, String)] {
+ guard !searchText.isEmpty else { return memoryManagerModes }
+ return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
+ }
+
+ var appVersion: String {
+ guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
+ return "Unknown"
+ }
+ return version
+ }
+
+ @FocusState private var isArgumentsKeyboardVisible: Bool
+
+
+ @State private var selectedView = "Data Management"
+ @State private var sidebar = true
+
+ enum SettingsCategory: String, CaseIterable, Identifiable {
+ case graphics = "Graphics"
+ case input = "Input"
+ case system = "System"
+ case misc = "Misc"
+ case advanced = "Advanced"
+
+ var id: String { self.rawValue }
+
+ var icon: String {
+ switch self {
+ case .graphics: return "paintbrush.fill"
+ case .input: return "gamecontroller.fill"
+ case .system: return "gearshape.fill"
+ case .misc: return "ellipsis.circle.fill"
+ case .advanced: return "terminal.fill"
+ }
+ }
+ }
+
+ var body: some View {
+ if UIDevice.current.userInterfaceIdiom == .phone {
+ iOSSettings
+ } else if !oldSettingsUI {
+ iPadOSSettings
+ .ignoresSafeArea()
+ .edgesIgnoringSafeArea(.all)
+ } else {
+ iOSSettings
+ }
+ }
+
+ var iPadOSSettings: some View {
+ VStack {
+ SidebarView(
+ sidebar: {
+ AnyView(
+ ScrollView(.vertical) {
+ VStack {
+ VStack(spacing: 16) {
+ HStack {
+ Circle()
+ .fill(ryujinx.jitenabled ? Color.green : Color.red)
+ .frame(width: 12, height: 12)
+
+ Text(ryujinx.jitenabled ? "JIT Enabled" : "JIT Not Acquired")
+ .font(.subheadline.weight(.medium))
+ .foregroundColor(ryujinx.jitenabled ? .green : .red)
+
+ Spacer()
+
+ let memoryText = ProcessInfo.processInfo.isiOSAppOnMac
+ ? String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024))
+ : String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
+
+ Text("\(memoryText) RAM")
+ .font(.subheadline.weight(.medium))
+ .foregroundColor(.secondary)
+
+ }
+
+ InfoCard(
+ title: "Device",
+ value: UIDevice.modelName,
+ icon: deviceIcon,
+ color: .blue
+ )
+
+ InfoCard(
+ title: "System",
+ value: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)",
+ icon: "applelogo",
+ color: .gray
+ )
+
+ InfoCard(
+ title: "Increased Memory Limit",
+ value: checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled",
+ icon: "memorychip.fill",
+ color: .orange
+ )
+ }
+ .padding()
+
+ Divider()
+
+ ForEach(SettingsCategory.allCases, id: \.id) { key in
+ HStack {
+ Rectangle()
+ .frame(width: 2.5, height: 35)
+ .foregroundStyle(selectedCategory == key ? Color.accentColor : Color.clear)
+ Text(key.rawValue) // Fix here
+ Spacer()
+ }
+ .foregroundStyle(selectedCategory == key ? Color.accentColor : Color.primary)
+ .padding(5)
+ .background(
+ Color(uiColor: .secondarySystemBackground).opacity(selectedCategory == key ? 1 : 0)
+ )
+ .background(
+ Rectangle()
+ .stroke(selectedCategory == key ? .teal : .clear, lineWidth: 2.5)
+ )
+ .contentShape(Rectangle())
+ .onTapGesture {
+ withAnimation(.smooth) {
+ selectedCategory = key // Uncommented and fixed
+ }
+ }
+ }
+ }
+ .padding()
+ }
+ )
+ },
+ content: {
+ ScrollView {
+ switch selectedCategory {
+ case .graphics:
+ graphicsSettings
+ case .input:
+ inputSettings
+ case .system:
+ systemSettings
+ case .advanced:
+ advancedSettings
+ case .misc:
+ miscSettings
+ }
+ }
+ },
+ showSidebar: $sidebar
+ )
+ .onAppear {
+ mVKPreFillBuffer = false
+
+
+ if let configs = SettingsManager.loadSettings() {
+ settingsManager.loadSettings()
+ } else {
+ settingsManager.saveSettings()
+ }
+ }
+ }
+ }
+
+
+ var iOSSettings: some View {
+ iOSNav {
+ ZStack {
+ // Background color
+ Color(UIColor.systemBackground)
+ .ignoresSafeArea()
+
+ VStack(spacing: 0) {
+ // Category selector
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 12) {
+ ForEach(SettingsCategory.allCases, id: \.id) { category in
+ CategoryButton(
+ title: category.rawValue,
+ icon: category.icon,
+ isSelected: selectedCategory == category
+ ) {
+ selectedCategory = category
+ }
+ }
+ }
+ .padding(.horizontal)
+ .padding(.vertical, 8)
+ }
+
+ Divider()
+
+ // Settings content
+ ScrollView {
+ VStack(spacing: 24) {
+ deviceInfoCard
+ .padding(.horizontal)
+ .padding(.top)
+
+ switch selectedCategory {
+ case .graphics:
+ graphicsSettings
+ case .input:
+ inputSettings
+ case .system:
+ systemSettings
+ case .advanced:
+ advancedSettings
+ case .misc:
+ miscSettings
+ }
+
+ Spacer(minLength: 50)
+ }
+ .padding(.bottom)
+ }
+ .scrollDismissesKeyboardIfAvailable()
+ }
+ }
+ .navigationTitle("Settings")
+ .navigationBarTitleDisplayMode(.large)
+ // .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
+ .onAppear {
+ mVKPreFillBuffer = false
+
+ if let configs = SettingsManager.loadSettings() {
+ settingsManager.loadSettings()
+ } else {
+ settingsManager.saveSettings()
+ }
+ }
+ }
+ }
+
+ // MARK: - Device Info Card
+
+ private var deviceInfoCard: some View {
+ VStack(spacing: 16) {
+ // JIT Status indicator
+ HStack {
+ Circle()
+ .fill(ryujinx.jitenabled ? Color.green : Color.red)
+ .frame(width: 12, height: 12)
+
+ Text(ryujinx.jitenabled ? "JIT Enabled" : "JIT Not Acquired")
+ .font(.subheadline.weight(.medium))
+ .foregroundColor(ryujinx.jitenabled ? .green : .red)
+
+ Spacer()
+
+ let memoryText = ProcessInfo.processInfo.isiOSAppOnMac
+ ? String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024))
+ : String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
+
+ Text("\(memoryText) RAM")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Text("·")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Text("Version \(appVersion)")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ }
+
+ // Device cards
+ if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) {
+ HStack(spacing: 16) {
+ InfoCard(
+ title: "Device",
+ value: UIDevice.modelName,
+ icon: deviceIcon,
+ color: .blue
+ )
+
+ InfoCard(
+ title: "System",
+ value: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)",
+ icon: "applelogo",
+ color: .gray
+ )
+
+ InfoCard(
+ title: "Increased Memory Limit",
+ value: checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled",
+ icon: "memorychip.fill",
+ color: .orange
+ )
+ }
+ } else {
+ VStack(spacing: 16) {
+ InfoCard(
+ title: "Device",
+ value: UIDevice.modelName,
+ icon: deviceIcon,
+ color: .blue
+ )
+
+ InfoCard(
+ title: "System",
+ value: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)",
+ icon: "applelogo",
+ color: .gray
+ )
+
+ InfoCard(
+ title: "Increased Memory Limit",
+ value: checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled",
+ icon: "memorychip.fill",
+ color: .orange
+ )
+ }
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 16)
+ .fill(colorScheme == .dark ? Color(.systemGray6) : Color.white)
+ .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
+ )
+ .onAppear {
+ ryujinx.ryuIsJITEnabled()
+ }
+ }
+
+ private var deviceIcon: String {
+ let model = UIDevice.modelName
+ if model.contains("iPad") {
+ return "ipad"
+ } else if model.contains("iPhone") {
+ return "iphone"
+ } else {
+ return "desktopcomputer"
+ }
+ }
+
+ // MARK: - Graphics Settings
+
+ private var graphicsSettings: some View {
+ SettingsSection(title: "Graphics & Performance") {
+ // Resolution scale card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showResolutionInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showResolutionInfo) {
+ Alert(
+ title: Text("Resolution Scale"),
+ message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: config.resscale, in: 0.1...3.0, step: 0.05)
+
+ HStack {
+ Text("0.1x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(settingsManager.config.resscale, specifier: "%.2f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("3.0x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // Anisotropic filtering card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ labelWithIcon("Max Anisotropic Filtering", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showAnisotropicInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showAnisotropicInfo) {
+ Alert(
+ title: Text("Max Anisotropic Filtering"),
+ message: Text("Adjust the internal Anisotropic filtering. Higher values improve texture quality at angles but may reduce performance. Default at 0 lets game decide."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: config.maxAnisotropy, in: 0...16.0, step: 0.1)
+
+ HStack {
+ Text("Off")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(settingsManager.config.maxAnisotropy, specifier: "%.1f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("16x")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
+ // Toggle options card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: config.disableShaderCache, icon: "memorychip", label: "Shader Cache")
+
+ Divider()
+
+ SettingsToggle(isOn: config.disablevsync, icon: "arrow.triangle.2.circlepath", label: "Disable VSync")
+
+ Divider()
+
+ SettingsToggle(isOn: config.enableTextureRecompression, icon: "rectangle.compress.vertical", label: "Texture Recompression")
+
+ Divider()
+
+ SettingsToggle(isOn: config.disableDockedMode, icon: "dock.rectangle", label: "Docked Mode")
+
+ Divider()
+
+ SettingsToggle(isOn: config.macroHLE, icon: "gearshape", label: "Macro HLE")
+
+ Divider()
+
+ SettingsToggle(isOn: $performacehud, icon: "speedometer", label: "Performance Overlay")
+ }
+ }
+
+ // Aspect ratio card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
+ .font(.headline)
+
+ if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) {
+ Picker(selection: config.aspectRatio) {
+ ForEach(AspectRatio.allCases, id: \.self) { ratio in
+ Text(ratio.displayName).tag(ratio)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.segmented)
+ } else {
+ Picker(selection: config.aspectRatio) {
+ ForEach(AspectRatio.allCases, id: \.self) { ratio in
+ Text(ratio.displayName).tag(ratio)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Input Settings
+
+ private var inputSettings: some View {
+ SettingsSection(title: "Input Configuration") {
+ // Controller selection card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Controller Selection")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ if currentControllers.isEmpty {
+ emptyControllersView
+ } else {
+ controllerListView
+ }
+
+ if hasAvailableControllers {
+ Divider()
+ addControllerButton
+ }
+ }
+ }
+
+ // On-screen controls card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
+
+ Divider()
+
+ SettingsToggle(isOn: $stickButton, icon: "l.joystick.press.down", label: "Show Stick Buttons")
+
+ Divider()
+
+ SettingsToggle(isOn: $ryuDemo, icon: "hand.draw", label: "On-Screen Controller (Demo)")
+ .disabled(true)
+
+ Divider()
+
+ SettingsToggle(isOn: $swapBandA, icon: "rectangle.2.swap", label: "Swap Face Buttons (Physical Controller)")
+
+ Divider()
+
+ DisclosureGroup("Toggle Buttons") {
+ SettingsToggle(isOn: $toggleButtons.toggle1, icon: "circle.grid.cross.right.filled", label: "Toggle A")
+ SettingsToggle(isOn: $toggleButtons.toggle2, icon: "circle.grid.cross.down.filled", label: "Toggle B")
+ SettingsToggle(isOn: $toggleButtons.toggle3, icon: "circle.grid.cross.up.filled", label: "Toggle X")
+ SettingsToggle(isOn: $toggleButtons.toggle4, icon: "circle.grid.cross.left.filled", label: "Toggle Y")
+ }
+ .padding(.vertical, 6)
+ }
+ }
+
+ // Controller scale card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("On-Screen Controller")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ Group {
+ HStack {
+ labelWithIcon("Scale", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showControllerInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showControllerInfo) {
+ Alert(
+ title: Text("On-Screen Controller Scale"),
+ message: Text("Adjust the On-Screen Controller size."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: $controllerScale, in: 0.1...3.0, step: 0.05)
+
+ HStack {
+ Text("Smaller")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(controllerScale, specifier: "%.2f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("Larger")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+
+ Divider()
+
+ Group {
+ HStack {
+ labelWithIcon("Opacity", iconName: "magnifyingglass")
+ .font(.headline)
+ Spacer()
+ Button {
+ showControllerInfo.toggle()
+ } label: {
+ Image(systemName: "info.circle")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ .alert(isPresented: $showControllerInfo) {
+ Alert(
+ title: Text("On-Screen Controller Opacity"),
+ message: Text("Adjust the On-Screen Controller transparency."),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+ }
+
+ VStack(spacing: 8) {
+ Slider(value: $controllerOpacity, in: 0.1...1.0, step: 0.05)
+
+ HStack {
+ Text("More Transparent")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ Spacer()
+
+ Text("\(controllerOpacity, specifier: "%.2f")x")
+ .font(.headline)
+ .foregroundColor(.blue)
+
+ Spacer()
+
+ Text("Less Transparent")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Controller Selection Components
+
+ private var hasAvailableControllers: Bool {
+ !controllersList.filter { !currentControllers.contains($0) }.isEmpty
+ }
+
+ private var emptyControllersView: some View {
+ HStack {
+ Text("No controllers selected (Keyboard will be used)")
+ .foregroundColor(.secondary)
+ .italic()
+ Spacer()
+ }
+ .padding(.vertical, 8)
+ }
+
+ private var controllerListView: some View {
+ VStack(spacing: 0) {
+ Divider()
+
+ ForEach(currentControllers.indices, id: \.self) { index in
+ let controller = currentControllers[index]
+
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "gamecontroller.fill")
+ .foregroundColor(.blue)
+
+ Text("Player \(index + 1): \(controller.name)")
+ .lineLimit(1)
+
+ Spacer()
+
+ Button {
+ toggleController(controller)
+ } label: {
+ Image(systemName: "xmark.circle.fill")
+ .foregroundColor(.secondary)
+ }
+ .buttonStyle(.plain)
+ }
+ .padding(.vertical, 8)
+
+ if index < currentControllers.count - 1 {
+ Divider()
+ }
+ }
+ }
+ .onMove { from, to in
+ currentControllers.move(fromOffsets: from, toOffset: to)
+ }
+ .environment(\.editMode, .constant(.active))
+ }
+ }
+
+ private var addControllerButton: some View {
+ Menu {
+ ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in
+ Button {
+ currentControllers.append(controller)
+ } label: {
+ Text(controller.name)
+ }
+ }
+ } label: {
+ Label("Add Controller", systemImage: "plus.circle.fill")
+ .foregroundColor(.blue)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, 6)
+ }
+ }
+
+ // MARK: - System Settings
+
+ private var systemSettings: some View {
+ SettingsSection(title: "System Configuration") {
+ // Language and region card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ VStack(alignment: .leading, spacing: 8) {
+ labelWithIcon("System Language", iconName: "character.bubble")
+ .font(.headline)
+
+ Picker(selection: config.language) {
+ ForEach(SystemLanguage.allCases, id: \.self) { language in
+ Text(language.displayName).tag(language)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+
+ Divider()
+
+ VStack(alignment: .leading, spacing: 8) {
+ labelWithIcon("Region", iconName: "globe")
+ .font(.headline)
+
+ Picker(selection: config.regioncode) {
+ ForEach(SystemRegionCode.allCases, id: \.self) { region in
+ Text(region.displayName).tag(region)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.menu)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+
+ // CPU options card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("CPU Configuration")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Memory Manager Mode")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Picker(selection: config.memoryManagerMode) {
+ ForEach(filteredMemoryModes, id: \.0) { key, displayName in
+ Text(displayName).tag(key)
+ }
+ } label: {
+ EmptyView()
+ }
+ .pickerStyle(.segmented)
+ }
+
+ Divider()
+
+ SettingsToggle(isOn: config.disablePTC, icon: "cpu", label: "Disable PTC")
+
+ if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
+ Divider()
+
+ if #available(iOS 16.4, *) {
+ SettingsToggle(isOn: .constant(false), icon: "bolt", label: "Hypervisor")
+ .disabled(true)
+ } else if checkAppEntitlement("com.apple.private.hypervisor") {
+ SettingsToggle(isOn: config.hypervisor, icon: "bolt", label: "Hypervisor")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Advanced Settings
+
+ private var advancedSettings: some View {
+ SettingsSection(title: "Advanced Options") {
+ // Debug options card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: $showlogsloading, icon: "text.alignleft", label: "Show Logs While Loading")
+
+ Divider()
+
+ SettingsToggle(isOn: $showlogsgame, icon: "text.magnifyingglass", label: "Show Logs In-Game")
+
+ Divider()
+
+ SettingsToggle(isOn: config.debuglogs, icon: "exclamationmark.bubble", label: "Debug Logs")
+
+ Divider()
+
+ SettingsToggle(isOn: config.tracelogs, icon: "waveform.path", label: "Trace Logs")
+ }
+ }
+
+ // Advanced toggles card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: $runOnMainThread, icon: "square.stack.3d.up", label: "Run Core on Main Thread")
+
+ Divider()
+
+ SettingsToggle(isOn: config.dfsIntegrityChecks, icon: "checkmark.shield", label: "Disable FS Integrity Checks")
+
+ Divider()
+
+
+ if MTLHud.shared.canMetalHud {
+ SettingsToggle(isOn: $metalHudEnabler.metalHudEnabled, icon: "speedometer", label: "Metal Performance HUD")
+
+ Divider()
+ }
+
+ SettingsToggle(isOn: $ignoreJIT, icon: "cpu", label: "Ignore JIT Popup")
+
+ Divider()
+
+ Button {
+ finishedStorage = false
+ } label: {
+ HStack {
+ Image(systemName: "arrow.triangle.2.circlepath.circle.fill")
+ .foregroundColor(.blue)
+ Text("Show Setup Screen")
+ .foregroundColor(.blue)
+ Spacer()
+ }
+ .padding(8)
+ }
+
+ }
+ }
+
+ // Memory hacks card
+ SettingsCard {
+ VStack(spacing: 4) {
+ SettingsToggle(isOn: config.expandRam, icon: "exclamationmark.bubble", label: "Expand Guest RAM")
+ .accentColor(.red)
+ .disabled(totalMemory < 5723)
+
+ Divider()
+
+ SettingsToggle(isOn: config.ignoreMissingServices, icon: "waveform.path", label: "Ignore Missing Services")
+ .accentColor(.red)
+ }
+ }
+
+ // Additional args card
+ SettingsCard {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Additional Arguments")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ let binding = Binding(
+ get: {
+ config.additionalArgs.wrappedValue.joined(separator: ", ")
+ },
+ set: { newValue in
+ settingsManager.config.additionalArgs = newValue
+ .split(separator: ",")
+ .map { $0.trimmingCharacters(in: .whitespaces) }
+ }
+ )
+
+
+ if #available(iOS 15.0, *) {
+ TextField("Separate arguments with commas", text: binding)
+ .font(.system(.body, design: .monospaced))
+ .textFieldStyle(.roundedBorder)
+ .textInputAutocapitalization(.none)
+ .disableAutocorrection(true)
+ .padding(.vertical, 4)
+ .toolbar {
+ ToolbarItem(placement: .keyboard) {
+ Button("Dismiss") {
+ isArgumentsKeyboardVisible = false
+ }
+ }
+ }
+ .focused($isArgumentsKeyboardVisible)
+ } else {
+ TextField("Separate arguments with commas", text: binding)
+ .font(.system(.body, design: .monospaced))
+ .textFieldStyle(.roundedBorder)
+ .disableAutocorrection(true)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+
+ // Page size info card
+ SettingsCard {
+ HStack {
+ labelWithIcon("Page Size", iconName: "textformat.size")
+ Spacer()
+ Text("\(String(Int(getpagesize())))")
+ .font(.system(.body, design: .monospaced))
+ .foregroundColor(.secondary)
+ }
+ }
+
+ if gamepo {
+ SettingsCard {
+ Text("The cake is a lie")
+ .font(.system(.body, design: .monospaced))
+ .foregroundColor(.secondary)
+ .frame(maxWidth: .infinity, alignment: .center)
+ }
+ }
+ }
+ }
+
+ // MARK: - Miscellaneous Settings
+
+ private var miscSettings: some View {
+ SettingsSection(title: "Miscellaneous Options") {
+ SettingsCard {
+ VStack(spacing: 4) {
+ if UIDevice.current.userInterfaceIdiom == .pad {
+ SettingsToggle(isOn: $toggleGreen, icon: "arrow.clockwise", label: "Toggle Color Green when \"ON\"")
+
+ Divider()
+ }
+
+
+ // Disable Touch card
+ SettingsToggle(isOn: $disableTouch, icon: "rectangle.and.hand.point.up.left.filled", label: "Disable Touch")
+
+ Divider()
+
+ if colorScheme == .light {
+ SettingsToggle(isOn: $blackScreen, icon: "iphone.slash", label: "Black Screen when using AirPlay")
+
+ Divider()
+ }
+
+ Button {
+ showAppIconSwitcher = true
+ } label: {
+ HStack {
+ Image(systemName: "app.dashed")
+ .foregroundColor(.blue)
+ Text("App Icon Switcher")
+ .foregroundColor(.primary)
+ Spacer()
+ }
+ .padding(.vertical, 8)
+ }
+ .sheet(isPresented: $showAppIconSwitcher) {
+ AppIconSwitcherView()
+ }
+
+ Divider()
+
+ // Exit button card
+ SettingsToggle(isOn: $ssb, icon: "arrow.left.circle", label: "Menu Button (in-game)")
+
+ Divider()
+
+ // Restarts app when it crashes card
+ SettingsToggle(isOn: $restartApp, icon: "arrow.clockwise", label: "Lock in App")
+
+ Divider()
+
+
+ // Location to keep app in Background
+ SettingsToggle(isOn: $locationenabled, icon: "location.viewfinder", label: "Keep app in background")
+
+ Divider()
+
+ if UIDevice.current.userInterfaceIdiom == .pad {
+ // Old Settings UI
+ SettingsToggle(isOn: $oldSettingsUI, icon: "ipad.landscape", label: "Non Switch-like Settings")
+
+ Divider()
+ }
+
+
+ // JIT options
+ if #available(iOS 17.0.1, *) {
+ let checked = stikJITorStikDebug()
+ let stikJIT = checked == 1 ? "StikDebug" : checked == 2 ? "StikJIT" : "StikDebug"
+
+ SettingsToggle(isOn: $stikJIT, icon: "bolt.heart", label: stikJIT)
+ .contextMenu {
+ Button {
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let mainWindow = windowScene.windows.last {
+ let alertController = UIAlertController(title: "About \(stikJIT)", message: "\(stikJIT) is a really amazing iOS Application to Enable JIT on the go on-device, made by the best, most kind, helpful and nice developers of all time jkcoxson and Blu <3", preferredStyle: .alert)
+
+ let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
+ UIApplication.shared.open(URL(string: "https://github.com/StephenDev0/StikJIT")!)
+ }
+ alertController.addAction(learnMoreButton)
+
+ let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil)
+ alertController.addAction(doneButton)
+
+ mainWindow.rootViewController?.present(alertController, animated: true)
+ }
+ } label: {
+ Text("About")
+ }
+ }
+ } else {
+ SettingsToggle(isOn: $useTrollStore, icon: "troll.svg", label: "TrollStore JIT")
+ }
+
+ Divider()
+
+ // MoltenVK Options
+ SettingsToggle(isOn: $syncqsubmits, icon: "line.diagonal", label: "MVK: Synchronous Queue Submits")
+ .contextMenu {
+ Button {
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let mainWindow = windowScene.windows.last {
+ let alertController = UIAlertController(title: "About MVK: Synchronous Queue Submits", message: "Enable this option if Mario Kart 8 is crashing at Grand Prix mode.", preferredStyle: .alert)
+
+ let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil)
+ alertController.addAction(doneButton)
+
+ mainWindow.rootViewController?.present(alertController, animated: true)
+ }
+ } label: {
+ Text("About")
+ }
+ }
+
+ Divider()
+
+ SettingsToggle(isOn: $checkForUpdate, icon: "square.and.arrow.down", label: "Check for Updates")
+
+ if ryujinx.firmwareversion != "0" {
+ Divider()
+ Button {
+ Ryujinx.shared.removeFirmware()
+ } label: {
+ HStack {
+ Text("Remove Firmware")
+ .foregroundColor(.blue)
+ Spacer()
+ }
+ .padding(.vertical, 8)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // MARK: - Helper Functions
+
+ private func toggleController(_ controller: Controller) {
+ if currentControllers.contains(where: { $0.id == controller.id }) {
+ currentControllers.removeAll(where: { $0.id == controller.id })
+ } else {
+ currentControllers.append(controller)
+ }
+ }
+
+
+ func getGPUInfo() -> String? {
+ let device = MTLCreateSystemDefaultDevice()
+ return device?.name
+ }
+
+ @ViewBuilder
+ private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
+ HStack(spacing: 8) {
+ if iconName.hasSuffix(".svg") {
+ if let flipimage, flipimage {
+ SVGView(svgName: iconName, color: .blue)
+ // .symbolRenderingMode(.hierarchical)
+ .frame(width: 20, height: 20)
+ .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
+ } else {
+ SVGView(svgName: iconName, color: .blue)
+ // .symbolRenderingMode(.hierarchical)
+ .frame(width: 20, height: 20)
+ }
+ } else if !iconName.isEmpty {
+ Image(systemName: iconName)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundColor(.blue)
+ }
+ Text(text)
+ }
+ .font(.body)
+ }
+}
+
+struct SVGView: UIViewRepresentable {
+ var svgName: String
+ var color: Color = Color.black
+
+ func makeUIView(context: Context) -> UIView {
+ var svgName = svgName
+ let hammock = UIView()
+
+ if svgName.hasSuffix(".svg") {
+ svgName.removeLast(4)
+ }
+
+ _ = UIView(svgNamed: svgName) { svgLayer in
+ svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color
+ svgLayer.resizeToFit(hammock.frame)
+ hammock.layer.addSublayer(svgLayer)
+ }
+
+ return hammock
+ }
+
+ func updateUIView(_ uiView: UIView, context: Context) {
+ // Update the SVG view's fill color when the color changes
+ if let svgLayer = uiView.layer.sublayers?.first as? CAShapeLayer {
+ svgLayer.fillColor = UIColor(color).cgColor
+ }
+ }
+}
+
+func saveSettings(config: Ryujinx.Arguments) {
+ do {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ let data = try encoder.encode(config)
+
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ try data.write(to: fileURL)
+ // print("Settings saved to: \(fileURL.path)")
+ } catch {
+ // print("Failed to save settings: \(error)")
+ }
+}
+
+func loadSettings() -> Ryujinx.Arguments? {
+ do {
+ let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ // print("Config file does not exist at: \(fileURL.path)")
+ return nil
+ }
+
+ let data = try Data(contentsOf: fileURL)
+
+ let decoder = JSONDecoder()
+ let configs = try decoder.decode(Ryujinx.Arguments.self, from: data)
+ return configs
+ } catch {
+ // print("Failed to load settings: \(error)")
+ return nil
+ }
+}
+
+
+// MARK: - Supporting Views
+
+struct CategoryButton: View {
+ let title: String
+ let icon: String
+ let isSelected: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ VStack(spacing: 6) {
+ Image(systemName: icon)
+ .font(.system(size: 16, weight: isSelected ? .semibold : .regular))
+ Text(title)
+ .font(.system(size: 12, weight: isSelected ? .semibold : .regular))
+ }
+ .foregroundColor(isSelected ? .blue : .secondary)
+ .frame(width: 70, height: 56)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(isSelected ? Color.blue.opacity(0.15) : Color.clear)
+ )
+ .animation(.bouncy(duration: 0.3), value: isSelected)
+ }
+ }
+}
+
+struct SettingsSection: View {
+ let title: String
+ let content: Content
+
+ init(title: String, @ViewBuilder content: () -> Content) {
+ self.title = title
+ self.content = content()
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text(title)
+ .font(.title2.weight(.bold))
+ .padding(.horizontal)
+
+ content
+ }
+ }
+}
+
+struct SettingsCard: View {
+ @Environment(\.colorScheme) var colorScheme
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+ let content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ if UIDevice.current.userInterfaceIdiom == .phone || oldSettingsUI {
+ content
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(colorScheme == .dark ? Color(.systemGray6) : Color.white)
+ .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
+ )
+ .padding(.horizontal)
+ } else {
+ VStack {
+ Divider()
+ content
+ Divider()
+ }
+ .padding()
+ }
+ }
+}
+
+struct SettingsToggle: View {
+ @Binding var isOn: Bool
+ let icon: String
+ let label: String
+ var disabled: Bool = false
+ @AppStorage("toggleGreen") var toggleGreen: Bool = false
+ @AppStorage("oldSettingsUI") var oldSettingsUI = false
+
+ var body: some View {
+ if UIDevice.current.userInterfaceIdiom == .phone || oldSettingsUI {
+ Toggle(isOn: $isOn) {
+ HStack(spacing: 8) {
+ if icon.hasSuffix(".svg") {
+ SVGView(svgName: icon, color: .blue)
+ .frame(width: 20, height: 20)
+ } else {
+ Image(systemName: icon)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundColor(.blue)
+ }
+
+ Text(label)
+ .font(.body)
+ }
+ }
+ .toggleStyle(SwitchToggleStyle(tint: .blue))
+ .disabled(disabled)
+ .padding(.vertical, 6)
+ } else {
+ Group {
+ HStack(spacing: 8) {
+ HStack {
+ if icon.hasSuffix(".svg") {
+ SVGView(svgName: icon, color: .blue)
+ .frame(width: 20, height: 20)
+ } else {
+ Image(systemName: icon)
+ // .symbolRenderingMode(.hierarchical)
+ .foregroundStyle(.blue)
+ }
+
+ Text(label)
+ .font(.body)
+ }
+
+ Spacer()
+
+
+ Text(isOn ? "ON" : "Off")
+ .foregroundStyle(isOn ? (toggleGreen ? .green : .blue) : .blue)
+ }
+ .padding()
+ .onTapGesture {
+ isOn.toggle()
+ }
+ }
+ }
+ }
+
+ func disabled(_ disabled: Bool) -> SettingsToggle {
+ var view = self
+ view.disabled = disabled
+ return view
+ }
+
+ func accentColor(_ color: Color) -> SettingsToggle {
+ var view = self
+ return view
+ }
+}
+
+struct InfoCard: View {
+ let title: String
+ let value: String
+ let icon: String
+ let color: Color
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Image(systemName: icon)
+ .foregroundColor(color)
+ Text(title)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Text(value)
+ .font(.system(size: 14, weight: .medium))
+ .lineLimit(1)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(10)
+ .background(color.opacity(0.1))
+ .cornerRadius(8)
+ }
+}
+
+// this code is used to enable the keyboard to be dismissed when scrolling if available on iOS 16+
+extension View {
+ @ViewBuilder
+ func scrollDismissesKeyboardIfAvailable() -> some View {
+ if #available(iOS 16.0, *) {
+ self.scrollDismissesKeyboard(.interactively)
+ } else {
+ self
+ }
+ }
+}
+
diff --git a/src/MeloNX/MeloNX/App/Views/Main/TabView/TabView.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/TabView/TabView.swift
similarity index 66%
rename from src/MeloNX/MeloNX/App/Views/Main/TabView/TabView.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/TabView/TabView.swift
index ccf7796bc..ac6d33ee7 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/TabView/TabView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/TabView/TabView.swift
@@ -11,7 +11,6 @@ import UniformTypeIdentifiers
struct MainTabView: View {
@Binding var startemu: Game?
- @Binding var config: Ryujinx.Configuration
@Binding var MVKconfig: [MoltenVKSettings]
@Binding var controllersList: [Controller]
@Binding var currentControllers: [Controller]
@@ -25,7 +24,8 @@ struct MainTabView: View {
Label("Games", systemImage: "gamecontroller.fill")
}
- SettingsView(config: $config, MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
+ // SettingsView(config: $config, MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
+ SettingsViewNew(MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
.tabItem {
Label("Settings", systemImage: "gear")
}
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/App/MeloNXUpdateSheet.swift
similarity index 58%
rename from src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/Updates/App/MeloNXUpdateSheet.swift
index a9f98de55..d2297555e 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/App/MeloNXUpdateSheet.swift
@@ -33,18 +33,32 @@ struct MeloNXUpdateSheet: View {
Spacer()
- Button(action: {
- if let url = URL(string: updateInfo.download_link) {
- UIApplication.shared.open(url)
+ if #available(iOS 15.0, *) {
+ Button(action: {
+ if let url = URL(string: updateInfo.download_link) {
+ UIApplication.shared.open(url)
+ }
+ }) {
+ Text("Download Now")
+ .font(.title3)
+ .bold()
+ .frame(width: 300, height: 40)
}
- }) {
- Text("Download Now")
- .font(.title3)
- .bold()
- .frame(width: 300, height: 40)
+ .buttonStyle(.borderedProminent)
+ .frame(alignment: .bottom)
+ } else {
+ Button(action: {
+ if let url = URL(string: updateInfo.download_link) {
+ UIApplication.shared.open(url)
+ }
+ }) {
+ Text("Download Now")
+ .font(.title3)
+ .bold()
+ .frame(width: 300, height: 40)
+ }
+ .frame(alignment: .bottom)
}
- .buttonStyle(.borderedProminent)
- .frame(alignment: .bottom)
}
.padding(.horizontal)
.navigationTitle("Version \(updateInfo.version_number) Available!")
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameDLCManagerSheet.swift
similarity index 79%
rename from src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameDLCManagerSheet.swift
index 653e7c9a6..b479e4c2f 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameDLCManagerSheet.swift
@@ -46,7 +46,7 @@ struct DLCManagerSheet: View {
@Binding var game: Game!
@State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = []
- @Environment(\.dismiss) private var dismiss
+ @Environment(\.presentationMode) var presentationMode
// MARK: - Body
var body: some View {
@@ -66,7 +66,7 @@ struct DLCManagerSheet: View {
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
- dismiss()
+ presentationMode.wrappedValue.dismiss()
}
}
@@ -127,27 +127,56 @@ struct DLCManagerSheet: View {
private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
- Button {
- toggleDLC(dlc)
- } label: {
- HStack {
- Text(dlc.filename)
- .foregroundStyle(.primary)
- Spacer()
- Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
- .foregroundStyle(dlc.isEnabled ? .primary : .secondary)
- .imageScale(.large)
- }
- .contentShape(Rectangle())
- }
- .buttonStyle(.plain)
- .swipeActions(edge: .trailing) {
- Button(role: .destructive) {
- if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
- removeDLC(at: IndexSet(integer: index))
+ Group {
+ if #available(iOS 15.0, *) {
+ Button {
+ toggleDLC(dlc)
+ } label: {
+ HStack {
+ Text(dlc.filename)
+ .foregroundColor(.primary)
+ Spacer()
+ Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
+ .foregroundColor(dlc.isEnabled ? .primary : .secondary)
+ .imageScale(.large)
+ }
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .swipeActions(edge: .trailing) {
+ Button(role: .destructive) {
+ if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
+ removeDLC(at: IndexSet(integer: index))
+ }
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ } else {
+ Button {
+ toggleDLC(dlc)
+ } label: {
+ HStack {
+ Text(dlc.filename)
+ .foregroundColor(.primary)
+ Spacer()
+ Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
+ .foregroundColor(dlc.isEnabled ? .primary : .secondary)
+ .imageScale(.large)
+ }
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ .contextMenu {
+ Button {
+ if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
+ removeDLC(at: IndexSet(integer: index))
+ }
+ } label: {
+ Label("Delete", systemImage: "trash")
+ .foregroundColor(.red)
+ }
}
- } label: {
- Label("Delete", systemImage: "trash")
}
}
}
@@ -261,7 +290,7 @@ private extension DLCManagerSheet {
return result
} catch {
- print("Error loading DLCs: \(error)")
+ // print("Error loading DLCs: \(error)")
return []
}
}
@@ -300,7 +329,7 @@ extension Array where Element: AnyObject {
// MARK: - URL Extension
extension URL {
- @available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
+ @available(iOS, introduced: 14.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
static var documentsDirectory: URL {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return documentDirectory
diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameUpdateManagerSheet.swift
similarity index 86%
rename from src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift
rename to src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameUpdateManagerSheet.swift
index f4a236264..6ee97b6f7 100644
--- a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift
+++ b/src/MeloNX/MeloNX/App/Views/Main/UI/Updates/Games/GameUpdateManagerSheet.swift
@@ -14,7 +14,7 @@ struct UpdateManagerSheet: View {
@Binding var game: Game?
@State private var isSelectingGameUpdate = false
@State private var jsonURL: URL? = nil
- @Environment(\.dismiss) private var dismiss
+ @Environment(\.presentationMode) var presentationMode
// MARK: - Models
class UpdateItem: Identifiable, ObservableObject {
@@ -51,7 +51,7 @@ struct UpdateManagerSheet: View {
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
- dismiss()
+ presentationMode.wrappedValue.dismiss()
}
}
@@ -106,15 +106,26 @@ struct UpdateManagerSheet: View {
}
private func updateRow(_ update: UpdateItem) -> some View {
+ Group {
+ if #available(iOS 15, *) {
+ updateRowNew(update)
+ } else {
+ updateRowOld(update)
+ }
+ }
+ }
+
+ @available(iOS 15, *)
+ private func updateRowNew(_ update: UpdateItem) -> some View {
Button {
toggleSelection(update)
} label: {
HStack {
Text(update.filename)
- .foregroundStyle(.primary)
+ .foregroundColor(.primary)
Spacer()
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
- .foregroundStyle(update.isSelected ? .primary : .secondary)
+ .foregroundColor(update.isSelected ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
@@ -131,6 +142,31 @@ struct UpdateManagerSheet: View {
}
}
+ private func updateRowOld(_ update: UpdateItem) -> some View {
+ Button {
+ toggleSelection(update)
+ } label: {
+ HStack {
+ Text(update.filename)
+ .foregroundColor(.primary)
+ Spacer()
+ Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
+ .foregroundColor(update.isSelected ? .primary : .secondary)
+ .imageScale(.large)
+ }
+ .contentShape(Rectangle())
+ }
+ .contextMenu {
+ Button {
+ if let index = updates.firstIndex(where: { $0.path == update.path }) {
+ removeUpdate(at: IndexSet(integer: index))
+ }
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ }
+
// MARK: - Functions
private func loadData() {
guard let game = game else { return }
@@ -244,14 +280,13 @@ struct UpdateManagerSheet: View {
print("toggle selection \(update.path)")
updates = updates.map { item in
- var mutableItem = item
- mutableItem.isSelected = item.path == update.path && !update.isSelected
- print(mutableItem.isSelected)
- print(update.isSelected)
- return mutableItem
+ item.isSelected = item.path == update.path && !update.isSelected
+ // print(mutableItem.isSelected)
+ // print(update.isSelected)
+ return item
}
- print(updates)
+ // print(updates)
saveJSON()
}
diff --git a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift
index 0bc1b5090..aeb7e5146 100644
--- a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift
+++ b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift
@@ -8,8 +8,15 @@
import SwiftUI
import UIKit
import CryptoKit
+import UniformTypeIdentifiers
+import AVFoundation
+extension UIDocumentPickerViewController {
+ @objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
+ return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
+ }
+}
@main
struct MeloNXApp: App {
@@ -21,38 +28,73 @@ struct MeloNXApp: App {
@State var showOutOfDateSheet = false
@State var updateInfo: LatestVersionResponse? = nil
+ @StateObject var metalHudEnabler = MTLHud.shared
+
@State var finished = false
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
+ @AppStorage("location-enabled") var locationenabled: Bool = false
+ @AppStorage("checkForUpdate") var checkForUpdate: Bool = true
+
+ @AppStorage("runOnMainThread") var runOnMainThread = false
+
+ @AppStorage("autoJIT") var autoJIT = false
+
+ @State var fourgbiPad = false
+ @AppStorage("4GB iPad") var ignores = false
+ // String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
var body: some Scene {
WindowGroup {
- if finishedStorage {
- ContentView()
- .onAppear {
- checkLatestVersion()
- }
- .sheet(isPresented: Binding(
- get: { showOutOfDateSheet && updateInfo != nil },
- set: { newValue in
- if !newValue {
- showOutOfDateSheet = false
- updateInfo = nil
+ Group {
+ if finishedStorage {
+ ContentView()
+ .withFileImporter()
+ .onAppear {
+ if checkForUpdate {
+ checkLatestVersion()
+ }
+
+ print(metalHudEnabler.canMetalHud)
+
+ UserDefaults.standard.set(false, forKey: "lockInApp")
+ }
+ .sheet(isPresented: Binding(
+ get: { showOutOfDateSheet && updateInfo != nil },
+ set: { newValue in
+ if !newValue {
+ showOutOfDateSheet = false
+ updateInfo = nil
+ }
+ }
+ )) {
+ if let updateInfo = updateInfo {
+ MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
}
}
- )) {
- if let updateInfo = updateInfo {
- MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
- }
- }
- } else {
- SetupView(finished: $finished)
- .onChange(of: finished) { newValue in
- withAnimation {
- withAnimation {
+ } else {
+ SetupView(finished: $finished)
+ .onChange(of: finished) { newValue in
+ withAnimation(.easeOut) {
finishedStorage = newValue
}
}
+ }
+ }
+ .onAppear() {
+ if UIDevice.current.userInterfaceIdiom == .pad && !ignores {
+ print((Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000))
+ if round(Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000) <= 4 {
+ fourgbiPad = true
}
+ }
+ }
+ .alert("Unsupported Device", isPresented: $fourgbiPad) {
+ Button("Continue") {
+ ignores = true
+ fourgbiPad = false
+ }
+ } message: {
+ Text("Your Device is an iPad with \(String(format: "%.0f GB", Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000)) of memory, MeloNX has issues with those devices")
}
}
}
@@ -64,22 +106,22 @@ struct MeloNXApp: App {
#if DEBUG
let urlString = "http://192.168.178.116:8000/api/latest_release"
#else
- let urlString = "https://melonx.org/api/latest_release"
+ let urlString = "https://melonx.net/api/latest_release"
#endif
guard let url = URL(string: urlString) else {
- print("Invalid URL")
+ // print("Invalid URL")
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
- print("Error checking for new version: \(error)")
+ // print("Error checking for new version: \(error)")
return
}
guard let data = data else {
- print("No data received")
+ // print("No data received")
return
}
@@ -94,10 +136,15 @@ struct MeloNXApp: App {
}
}
} catch {
- print("Failed to decode response: \(error)")
+ // print("Failed to decode response: \(error)")
}
}
task.resume()
}
}
+
+func changeAppUI(_ string: String) -> String? {
+ guard let data = Data(base64Encoded: string) else { return nil }
+ return String(data: data, encoding: .utf8)
+}
diff --git a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
index 9a55bc66c..50caa99aa 100644
--- a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
+++ b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
@@ -54,12 +54,17 @@ struct SetupView: View {
) { result in
handleFirmwareImport(result: result)
}
- .alert(alertMessage, isPresented: $showAlert) {
- Button("OK", role: .cancel) {}
+ .alert(isPresented: $showAlert) {
+ Alert(title: Text(alertMessage), dismissButton: .default(Text("OK")))
}
- .alert("Skip Setup?", isPresented: $showSkipAlert) {
- Button("Skip", role: .destructive) { finished = true }
- Button("Cancel", role: .cancel) {}
+ .alert(isPresented: $showSkipAlert) {
+ Alert(
+ title: Text("Skip Setup?"),
+ primaryButton: .destructive(Text("Skip")) {
+ finished = true
+ },
+ secondaryButton: .cancel()
+ )
}
.onAppear {
initialize()
@@ -390,7 +395,7 @@ struct SetupView: View {
let iconFileName = iconFiles.last else {
- print("Could not find icons in bundle")
+ // print("Could not find icons in bundle")
return ""
}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png
index 421f7d484..ce7a13a38 100644
Binary files a/src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/Contents.json
new file mode 100644
index 000000000..5607ee502
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "darker.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/darker.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/darker.png
new file mode 100644
index 000000000..e4096e734
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.appiconset/darker.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/Contents.json
new file mode 100644
index 000000000..fb81560ef
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "darker.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/darker.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/darker.png
new file mode 100644
index 000000000..e4096e734
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/darker.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..739e96301
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "MeloNX 1024.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/MeloNX 1024.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/MeloNX 1024.png
new file mode 100644
index 000000000..14245da09
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.appiconset/MeloNX 1024.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/Contents.json
new file mode 100644
index 000000000..729a76a41
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "MeloNX 1024.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/MeloNX 1024.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/MeloNX 1024.png
new file mode 100644
index 000000000..14245da09
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/MeloNX 1024.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..f46306ca5
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "PixelPomeloNX 1024.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/PixelPomeloNX 1024.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/PixelPomeloNX 1024.png
new file mode 100644
index 000000000..9aa22bdc6
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.appiconset/PixelPomeloNX 1024.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/Contents.json
new file mode 100644
index 000000000..77faa36d9
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "PixelPomeloNX 1024.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/PixelPomeloNX 1024.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/PixelPomeloNX 1024.png
new file mode 100644
index 000000000..9aa22bdc6
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/PixelPomeloNX 1024.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..c843eafa5
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "copycat.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/copycat.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/copycat.png
new file mode 100644
index 000000000..034a52652
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.appiconset/copycat.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/Contents.json
new file mode 100644
index 000000000..a16ce9fc7
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "copycat.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/copycat.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/copycat.png
new file mode 100644
index 000000000..034a52652
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/copycat.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..37268d676
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "melowonx.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/melowonx.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/melowonx.png
new file mode 100644
index 000000000..ad0867e2f
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.appiconset/melowonx.png differ
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/Contents.json b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/Contents.json
new file mode 100644
index 000000000..52e9bd731
--- /dev/null
+++ b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "melowonx.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/melowonx.png b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/melowonx.png
new file mode 100644
index 000000000..ad0867e2f
Binary files /dev/null and b/src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/melowonx.png differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist
index 0a5037678..176855fa8 100644
Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper
index cfb7b2348..b411095c9 100755
Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources
index 8ff39bf10..a342a7acb 100644
--- a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources
@@ -10,7 +10,7 @@
Info.plist
- UOH9NuuEcz5NQiQlrM2LNFaG2pI=
+ GYWZONTCP5su4yOAk0d5jCd2K88=
Modules/module.modulemap
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT-Swift.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT-Swift.h
new file mode 100644
index 000000000..e87058bf0
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT-Swift.h
@@ -0,0 +1,330 @@
+#if 0
+#elif defined(__arm64__) && __arm64__
+// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
+#ifndef STOSJIT_SWIFT_H
+#define STOSJIT_SWIFT_H
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wgcc-compat"
+
+#if !defined(__has_include)
+# define __has_include(x) 0
+#endif
+#if !defined(__has_attribute)
+# define __has_attribute(x) 0
+#endif
+#if !defined(__has_feature)
+# define __has_feature(x) 0
+#endif
+#if !defined(__has_warning)
+# define __has_warning(x) 0
+#endif
+
+#if __has_include()
+# include
+#endif
+
+#pragma clang diagnostic ignored "-Wauto-import"
+#if defined(__OBJC__)
+#include
+#endif
+#if defined(__cplusplus)
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#else
+#include
+#include
+#include
+#include
+#endif
+#if defined(__cplusplus)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
+#if defined(__arm64e__) && __has_include()
+# include
+#else
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
+# ifndef __ptrauth_swift_value_witness_function_pointer
+# define __ptrauth_swift_value_witness_function_pointer(x)
+# endif
+# ifndef __ptrauth_swift_class_method_pointer
+# define __ptrauth_swift_class_method_pointer(x)
+# endif
+#pragma clang diagnostic pop
+#endif
+#pragma clang diagnostic pop
+#endif
+
+#if !defined(SWIFT_TYPEDEFS)
+# define SWIFT_TYPEDEFS 1
+# if __has_include()
+# include
+# elif !defined(__cplusplus)
+typedef uint_least16_t char16_t;
+typedef uint_least32_t char32_t;
+# endif
+typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
+typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
+typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
+typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
+typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
+typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
+typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
+typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
+typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
+typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
+typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
+typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
+#endif
+
+#if !defined(SWIFT_PASTE)
+# define SWIFT_PASTE_HELPER(x, y) x##y
+# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
+#endif
+#if !defined(SWIFT_METATYPE)
+# define SWIFT_METATYPE(X) Class
+#endif
+#if !defined(SWIFT_CLASS_PROPERTY)
+# if __has_feature(objc_class_property)
+# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
+# else
+# define SWIFT_CLASS_PROPERTY(...)
+# endif
+#endif
+#if !defined(SWIFT_RUNTIME_NAME)
+# if __has_attribute(objc_runtime_name)
+# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
+# else
+# define SWIFT_RUNTIME_NAME(X)
+# endif
+#endif
+#if !defined(SWIFT_COMPILE_NAME)
+# if __has_attribute(swift_name)
+# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
+# else
+# define SWIFT_COMPILE_NAME(X)
+# endif
+#endif
+#if !defined(SWIFT_METHOD_FAMILY)
+# if __has_attribute(objc_method_family)
+# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
+# else
+# define SWIFT_METHOD_FAMILY(X)
+# endif
+#endif
+#if !defined(SWIFT_NOESCAPE)
+# if __has_attribute(noescape)
+# define SWIFT_NOESCAPE __attribute__((noescape))
+# else
+# define SWIFT_NOESCAPE
+# endif
+#endif
+#if !defined(SWIFT_RELEASES_ARGUMENT)
+# if __has_attribute(ns_consumed)
+# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
+# else
+# define SWIFT_RELEASES_ARGUMENT
+# endif
+#endif
+#if !defined(SWIFT_WARN_UNUSED_RESULT)
+# if __has_attribute(warn_unused_result)
+# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+# else
+# define SWIFT_WARN_UNUSED_RESULT
+# endif
+#endif
+#if !defined(SWIFT_NORETURN)
+# if __has_attribute(noreturn)
+# define SWIFT_NORETURN __attribute__((noreturn))
+# else
+# define SWIFT_NORETURN
+# endif
+#endif
+#if !defined(SWIFT_CLASS_EXTRA)
+# define SWIFT_CLASS_EXTRA
+#endif
+#if !defined(SWIFT_PROTOCOL_EXTRA)
+# define SWIFT_PROTOCOL_EXTRA
+#endif
+#if !defined(SWIFT_ENUM_EXTRA)
+# define SWIFT_ENUM_EXTRA
+#endif
+#if !defined(SWIFT_CLASS)
+# if __has_attribute(objc_subclassing_restricted)
+# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
+# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# else
+# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# endif
+#endif
+#if !defined(SWIFT_RESILIENT_CLASS)
+# if __has_attribute(objc_class_stub)
+# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
+# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# else
+# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
+# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# endif
+#endif
+#if !defined(SWIFT_PROTOCOL)
+# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+#endif
+#if !defined(SWIFT_EXTENSION)
+# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
+#endif
+#if !defined(OBJC_DESIGNATED_INITIALIZER)
+# if __has_attribute(objc_designated_initializer)
+# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
+# else
+# define OBJC_DESIGNATED_INITIALIZER
+# endif
+#endif
+#if !defined(SWIFT_ENUM_ATTR)
+# if __has_attribute(enum_extensibility)
+# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
+# else
+# define SWIFT_ENUM_ATTR(_extensibility)
+# endif
+#endif
+#if !defined(SWIFT_ENUM)
+# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# if __has_feature(generalized_swift_name)
+# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# else
+# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
+# endif
+#endif
+#if !defined(SWIFT_UNAVAILABLE)
+# define SWIFT_UNAVAILABLE __attribute__((unavailable))
+#endif
+#if !defined(SWIFT_UNAVAILABLE_MSG)
+# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
+#endif
+#if !defined(SWIFT_AVAILABILITY)
+# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
+#endif
+#if !defined(SWIFT_WEAK_IMPORT)
+# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
+#endif
+#if !defined(SWIFT_DEPRECATED)
+# define SWIFT_DEPRECATED __attribute__((deprecated))
+#endif
+#if !defined(SWIFT_DEPRECATED_MSG)
+# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
+#endif
+#if !defined(SWIFT_DEPRECATED_OBJC)
+# if __has_feature(attribute_diagnose_if_objc)
+# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
+# else
+# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
+# endif
+#endif
+#if defined(__OBJC__)
+#if !defined(IBSegueAction)
+# define IBSegueAction
+#endif
+#endif
+#if !defined(SWIFT_EXTERN)
+# if defined(__cplusplus)
+# define SWIFT_EXTERN extern "C"
+# else
+# define SWIFT_EXTERN extern
+# endif
+#endif
+#if !defined(SWIFT_CALL)
+# define SWIFT_CALL __attribute__((swiftcall))
+#endif
+#if !defined(SWIFT_INDIRECT_RESULT)
+# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
+#endif
+#if !defined(SWIFT_CONTEXT)
+# define SWIFT_CONTEXT __attribute__((swift_context))
+#endif
+#if !defined(SWIFT_ERROR_RESULT)
+# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
+#endif
+#if defined(__cplusplus)
+# define SWIFT_NOEXCEPT noexcept
+#else
+# define SWIFT_NOEXCEPT
+#endif
+#if !defined(SWIFT_C_INLINE_THUNK)
+# if __has_attribute(always_inline)
+# if __has_attribute(nodebug)
+# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
+# else
+# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
+# endif
+# else
+# define SWIFT_C_INLINE_THUNK inline
+# endif
+#endif
+#if defined(_WIN32)
+#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
+# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
+#endif
+#else
+#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
+# define SWIFT_IMPORT_STDLIB_SYMBOL
+#endif
+#endif
+#if defined(__OBJC__)
+#if __has_feature(objc_modules)
+#if __has_warning("-Watimport-in-framework-header")
+#pragma clang diagnostic ignored "-Watimport-in-framework-header"
+#endif
+#endif
+
+#endif
+#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
+#pragma clang diagnostic ignored "-Wduplicate-method-arg"
+#if __has_warning("-Wpragma-clang-attribute")
+# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
+#endif
+#pragma clang diagnostic ignored "-Wunknown-pragmas"
+#pragma clang diagnostic ignored "-Wnullability"
+#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
+#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
+
+#if __has_attribute(external_source_symbol)
+# pragma push_macro("any")
+# undef any
+# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="StosJIT",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
+# pragma pop_macro("any")
+#endif
+
+#if defined(__OBJC__)
+
+SWIFT_EXTERN char * _Nullable attach(int32_t pid) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;
+
+
+SWIFT_EXTERN char * _Nullable debugattachanddetachApp(char * _Nonnull bundleId) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;
+
+
+SWIFT_EXTERN void detach(void) SWIFT_NOEXCEPT;
+
+
+SWIFT_EXTERN void loop_heartbeat(void) SWIFT_NOEXCEPT;
+
+
+SWIFT_EXTERN BOOL writeZeroToMemory(uint64_t addr, int32_t length) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;
+
+#endif
+#if __has_attribute(external_source_symbol)
+# pragma clang attribute pop
+#endif
+#if defined(__cplusplus)
+#endif
+#pragma clang diagnostic pop
+#endif
+
+#else
+#error unsupported Swift architecture
+#endif
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT.h
new file mode 100644
index 000000000..572ca9033
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/StosJIT.h
@@ -0,0 +1,19 @@
+//
+// StosJIT.h
+// StosJIT
+//
+// Created by Stossy11 on 10/05/2025.
+//
+
+#import
+#import
+
+//! Project version number for StosJIT.
+FOUNDATION_EXPORT double StosJITVersionNumber;
+
+//! Project version string for StosJIT.
+FOUNDATION_EXPORT const unsigned char StosJITVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
+
+
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/idevice.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/idevice.h
new file mode 100644
index 000000000..836b1e94d
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/idevice.h
@@ -0,0 +1,2916 @@
+// Jackson Coxson
+// Bindings to idevice - https://github.com/jkcoxson/idevice
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define LOCKDOWN_PORT 62078
+
+typedef enum AfcFopenMode {
+ AfcRdOnly = 1,
+ AfcRw = 2,
+ AfcWrOnly = 3,
+ AfcWr = 4,
+ AfcAppend = 5,
+ AfcRdAppend = 6,
+} AfcFopenMode;
+
+/**
+ * Link type for creating hard or symbolic links
+ */
+typedef enum AfcLinkType {
+ Hard = 1,
+ Symbolic = 2,
+} AfcLinkType;
+
+typedef enum IdeviceErrorCode {
+ IdeviceSuccess = 0,
+ Socket = -1,
+ Tls = -2,
+ TlsBuilderFailed = -3,
+ Plist = -4,
+ Utf8 = -5,
+ UnexpectedResponse = -6,
+ GetProhibited = -7,
+ SessionInactive = -8,
+ InvalidHostID = -9,
+ NoEstablishedConnection = -10,
+ HeartbeatSleepyTime = -11,
+ HeartbeatTimeout = -12,
+ NotFound = -13,
+ CdtunnelPacketTooShort = -14,
+ CdtunnelPacketInvalidMagic = -15,
+ PacketSizeMismatch = -16,
+ Json = -17,
+ DeviceNotFound = -18,
+ DeviceLocked = -19,
+ UsbConnectionRefused = -20,
+ UsbBadCommand = -21,
+ UsbBadDevice = -22,
+ UsbBadVersion = -23,
+ BadBuildManifest = -24,
+ ImageNotMounted = -25,
+ Reqwest = -26,
+ InternalError = -27,
+ Xpc = -28,
+ NsKeyedArchiveError = -29,
+ UnknownAuxValueType = -30,
+ UnknownChannel = -31,
+ AddrParseError = -32,
+ DisableMemoryLimitFailed = -33,
+ NotEnoughBytes = -34,
+ Utf8Error = -35,
+ InvalidArgument = -36,
+ UnknownErrorType = -37,
+ PemParseFailed = -38,
+ MisagentFailure = -39,
+ InstallationProxyOperationFailed = -40,
+ Afc = -41,
+ UnknownAfcOpcode = -42,
+ InvalidAfcMagic = -43,
+ AfcMissingAttribute = -44,
+ AdapterIOFailed = -996,
+ ServiceNotFound = -997,
+ BufferTooSmall = -998,
+ InvalidString = -999,
+ InvalidArg = -1000,
+} IdeviceErrorCode;
+
+typedef enum IdeviceLogLevel {
+ Disabled = 0,
+ ErrorLevel = 1,
+ Warn = 2,
+ Info = 3,
+ Debug = 4,
+ Trace = 5,
+} IdeviceLogLevel;
+
+typedef enum IdeviceLoggerError {
+ Success = 0,
+ FileError = -1,
+ AlreadyInitialized = -2,
+ InvalidPathString = -3,
+} IdeviceLoggerError;
+
+typedef struct AdapterHandle AdapterHandle;
+
+typedef struct AfcClientHandle AfcClientHandle;
+
+/**
+ * Handle for an open file on the device
+ */
+typedef struct AfcFileHandle AfcFileHandle;
+
+typedef struct AmfiClientHandle AmfiClientHandle;
+
+typedef struct CoreDeviceProxyHandle CoreDeviceProxyHandle;
+
+/**
+ * Opaque handle to a DebugProxyClient
+ */
+typedef struct DebugProxyAdapterHandle DebugProxyAdapterHandle;
+
+typedef struct HeartbeatClientHandle HeartbeatClientHandle;
+
+/**
+ * Opaque C-compatible handle to an Idevice connection
+ */
+typedef struct IdeviceHandle IdeviceHandle;
+
+/**
+ * Opaque C-compatible handle to a PairingFile
+ */
+typedef struct IdevicePairingFile IdevicePairingFile;
+
+typedef struct IdeviceSocketHandle IdeviceSocketHandle;
+
+typedef struct ImageMounterHandle ImageMounterHandle;
+
+typedef struct InstallationProxyClientHandle InstallationProxyClientHandle;
+
+/**
+ * Opaque handle to a ProcessControlClient
+ */
+typedef struct LocationSimulationAdapterHandle LocationSimulationAdapterHandle;
+
+typedef struct LockdowndClientHandle LockdowndClientHandle;
+
+typedef struct MisagentClientHandle MisagentClientHandle;
+
+/**
+ * Opaque handle to a ProcessControlClient
+ */
+typedef struct ProcessControlAdapterHandle ProcessControlAdapterHandle;
+
+/**
+ * Opaque handle to a RemoteServerClient
+ */
+typedef struct RemoteServerAdapterHandle RemoteServerAdapterHandle;
+
+typedef struct SpringBoardServicesClientHandle SpringBoardServicesClientHandle;
+
+typedef struct TcpProviderHandle TcpProviderHandle;
+
+typedef struct UsbmuxdAddrHandle UsbmuxdAddrHandle;
+
+typedef struct UsbmuxdConnectionHandle UsbmuxdConnectionHandle;
+
+typedef struct UsbmuxdProviderHandle UsbmuxdProviderHandle;
+
+/**
+ * Opaque handle to an XPCDevice
+ */
+typedef struct XPCDeviceAdapterHandle XPCDeviceAdapterHandle;
+
+typedef struct sockaddr sockaddr;
+
+/**
+ * File information structure for C bindings
+ */
+typedef struct AfcFileInfo {
+ size_t size;
+ size_t blocks;
+ int64_t creation;
+ int64_t modified;
+ char *st_nlink;
+ char *st_ifmt;
+ char *st_link_target;
+} AfcFileInfo;
+
+/**
+ * Device information structure for C bindings
+ */
+typedef struct AfcDeviceInfo {
+ char *model;
+ size_t total_bytes;
+ size_t free_bytes;
+ size_t block_size;
+} AfcDeviceInfo;
+
+/**
+ * Represents a debugserver command
+ */
+typedef struct DebugserverCommandHandle {
+ char *name;
+ char **argv;
+ uintptr_t argv_count;
+} DebugserverCommandHandle;
+
+/**
+ * Opaque handle to an XPCService
+ */
+typedef struct XPCServiceHandle {
+ char *entitlement;
+ uint16_t port;
+ bool uses_remote_xpc;
+ char **features;
+ uintptr_t features_count;
+ int64_t service_version;
+} XPCServiceHandle;
+
+/**
+ * Creates a new Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - Socket for communication with the device
+ * * [`label`] - Label for the connection
+ * * [`idevice`] - On success, will be set to point to a newly allocated Idevice handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `label` must be a valid null-terminated C string
+ * `idevice` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_new(struct IdeviceSocketHandle *socket,
+ const char *label,
+ struct IdeviceHandle **idevice);
+
+/**
+ * Creates a new Idevice connection
+ *
+ * # Arguments
+ * * [`addr`] - The socket address to connect to
+ * * [`addr_len`] - Length of the socket
+ * * [`label`] - Label for the connection
+ * * [`idevice`] - On success, will be set to point to a newly allocated Idevice handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid sockaddr
+ * `label` must be a valid null-terminated C string
+ * `idevice` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_new_tcp_socket(const struct sockaddr *addr,
+ socklen_t addr_len,
+ const char *label,
+ struct IdeviceHandle **idevice);
+
+/**
+ * Gets the device type
+ *
+ * # Arguments
+ * * [`idevice`] - The Idevice handle
+ * * [`device_type`] - On success, will be set to point to a newly allocated string containing the device type
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `idevice` must be a valid, non-null pointer to an Idevice handle
+ * `device_type` must be a valid, non-null pointer to a location where the string pointer will be stored
+ */
+enum IdeviceErrorCode idevice_get_type(struct IdeviceHandle *idevice,
+ char **device_type);
+
+/**
+ * Performs RSD checkin
+ *
+ * # Arguments
+ * * [`idevice`] - The Idevice handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `idevice` must be a valid, non-null pointer to an Idevice handle
+ */
+enum IdeviceErrorCode idevice_rsd_checkin(struct IdeviceHandle *idevice);
+
+/**
+ * Starts a TLS session
+ *
+ * # Arguments
+ * * [`idevice`] - The Idevice handle
+ * * [`pairing_file`] - The pairing file to use for TLS
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `idevice` must be a valid, non-null pointer to an Idevice handle
+ * `pairing_file` must be a valid, non-null pointer to a pairing file handle
+ */
+enum IdeviceErrorCode idevice_start_session(struct IdeviceHandle *idevice,
+ const struct IdevicePairingFile *pairing_file);
+
+/**
+ * Frees an Idevice handle
+ *
+ * # Arguments
+ * * [`idevice`] - The Idevice handle to free
+ *
+ * # Safety
+ * `idevice` must be a valid pointer to an Idevice handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_free(struct IdeviceHandle *idevice);
+
+/**
+ * Frees a string allocated by this library
+ *
+ * # Arguments
+ * * [`string`] - The string to free
+ *
+ * # Safety
+ * `string` must be a valid pointer to a string that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_string_free(char *string);
+
+/**
+ * Connects the adapter to a specific port
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ * * [`port`] - The port to connect to
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode adapter_connect(struct AdapterHandle *handle, uint16_t port);
+
+/**
+ * Enables PCAP logging for the adapter
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ * * [`path`] - The path to save the PCAP file (null-terminated string)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `path` must be a valid null-terminated string
+ */
+enum IdeviceErrorCode adapter_pcap(struct AdapterHandle *handle, const char *path);
+
+/**
+ * Closes the adapter connection
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode adapter_close(struct AdapterHandle *handle);
+
+/**
+ * Sends data through the adapter
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ * * [`data`] - The data to send
+ * * [`length`] - The length of the data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `data` must be a valid pointer to at least `length` bytes
+ */
+enum IdeviceErrorCode adapter_send(struct AdapterHandle *handle,
+ const uint8_t *data,
+ uintptr_t length);
+
+/**
+ * Receives data from the adapter
+ *
+ * # Arguments
+ * * [`handle`] - The adapter handle
+ * * [`data`] - Pointer to a buffer where the received data will be stored
+ * * [`length`] - Pointer to store the actual length of received data
+ * * [`max_length`] - Maximum number of bytes that can be stored in `data`
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `data` must be a valid pointer to at least `max_length` bytes
+ * `length` must be a valid pointer to a usize
+ */
+enum IdeviceErrorCode adapter_recv(struct AdapterHandle *handle,
+ uint8_t *data,
+ uintptr_t *length,
+ uintptr_t max_length);
+
+/**
+ * Connects to the AFC service using a TCP provider
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated AfcClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode afc_client_connect_tcp(struct TcpProviderHandle *provider,
+ struct AfcClientHandle **client);
+
+/**
+ * Connects to the AFC service using a Usbmuxd provider
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated AfcClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode afc_client_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct AfcClientHandle **client);
+
+/**
+ * Creates a new AfcClient from an existing Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated AfcClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode afc_client_new(struct IdeviceHandle *socket, struct AfcClientHandle **client);
+
+/**
+ * Frees an AfcClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void afc_client_free(struct AfcClientHandle *handle);
+
+/**
+ * Lists the contents of a directory on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the directory to list (UTF-8 null-terminated)
+ * * [`entries`] - Will be set to point to an array of directory entries
+ * * [`count`] - Will be set to the number of entries
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_list_directory(struct AfcClientHandle *client,
+ const char *path,
+ char ***entries,
+ size_t *count);
+
+/**
+ * Creates a new directory on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path of the directory to create (UTF-8 null-terminated)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_make_directory(struct AfcClientHandle *client, const char *path);
+
+/**
+ * Retrieves information about a file or directory
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the file or directory (UTF-8 null-terminated)
+ * * [`info`] - Will be populated with file information
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` and `path` must be valid pointers
+ * `info` must be a valid pointer to an AfcFileInfo struct
+ */
+enum IdeviceErrorCode afc_get_file_info(struct AfcClientHandle *client,
+ const char *path,
+ struct AfcFileInfo *info);
+
+/**
+ * Frees memory allocated by afc_get_file_info
+ *
+ * # Arguments
+ * * [`info`] - Pointer to AfcFileInfo struct to free
+ *
+ * # Safety
+ * `info` must be a valid pointer to an AfcFileInfo struct previously returned by afc_get_file_info
+ */
+void afc_file_info_free(struct AfcFileInfo *info);
+
+/**
+ * Retrieves information about the device's filesystem
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`info`] - Will be populated with device information
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` and `info` must be valid pointers
+ */
+enum IdeviceErrorCode afc_get_device_info(struct AfcClientHandle *client,
+ struct AfcDeviceInfo *info);
+
+/**
+ * Frees memory allocated by afc_get_device_info
+ *
+ * # Arguments
+ * * [`info`] - Pointer to AfcDeviceInfo struct to free
+ *
+ * # Safety
+ * `info` must be a valid pointer to an AfcDeviceInfo struct previously returned by afc_get_device_info
+ */
+void afc_device_info_free(struct AfcDeviceInfo *info);
+
+/**
+ * Removes a file or directory
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the file or directory to remove (UTF-8 null-terminated)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_remove_path(struct AfcClientHandle *client, const char *path);
+
+/**
+ * Recursively removes a directory and all its contents
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the directory to remove (UTF-8 null-terminated)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_remove_path_and_contents(struct AfcClientHandle *client,
+ const char *path);
+
+/**
+ * Opens a file on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`path`] - Path to the file to open (UTF-8 null-terminated)
+ * * [`mode`] - File open mode
+ * * [`handle`] - Will be set to a new file handle on success
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode afc_file_open(struct AfcClientHandle *client,
+ const char *path,
+ enum AfcFopenMode mode,
+ struct AfcFileHandle **handle);
+
+/**
+ * Closes a file handle
+ *
+ * # Arguments
+ * * [`handle`] - File handle to close
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode afc_file_close(struct AfcFileHandle *handle);
+
+/**
+ * Reads data from an open file
+ *
+ * # Arguments
+ * * [`handle`] - File handle to read from
+ * * [`data`] - Will be set to point to the read data
+ * * [`length`] - Will be set to the length of the read data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ */
+enum IdeviceErrorCode afc_file_read(struct AfcFileHandle *handle, uint8_t **data, size_t *length);
+
+/**
+ * Writes data to an open file
+ *
+ * # Arguments
+ * * [`handle`] - File handle to write to
+ * * [`data`] - Data to write
+ * * [`length`] - Length of data to write
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `data` must point to at least `length` bytes
+ */
+enum IdeviceErrorCode afc_file_write(struct AfcFileHandle *handle,
+ const uint8_t *data,
+ size_t length);
+
+/**
+ * Creates a hard or symbolic link
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`target`] - Target path of the link (UTF-8 null-terminated)
+ * * [`source`] - Path where the link should be created (UTF-8 null-terminated)
+ * * [`link_type`] - Type of link to create
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `target` and `source` must be valid null-terminated C strings
+ */
+enum IdeviceErrorCode afc_make_link(struct AfcClientHandle *client,
+ const char *target,
+ const char *source,
+ enum AfcLinkType link_type);
+
+/**
+ * Renames a file or directory
+ *
+ * # Arguments
+ * * [`client`] - A valid AfcClient handle
+ * * [`source`] - Current path of the file/directory (UTF-8 null-terminated)
+ * * [`target`] - New path for the file/directory (UTF-8 null-terminated)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `source` and `target` must be valid null-terminated C strings
+ */
+enum IdeviceErrorCode afc_rename_path(struct AfcClientHandle *client,
+ const char *source,
+ const char *target);
+
+/**
+ * Automatically creates and connects to AMFI service, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode amfi_connect_tcp(struct TcpProviderHandle *provider,
+ struct AmfiClientHandle **client);
+
+/**
+ * Automatically creates and connects to AMFI service, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode amfi_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct AmfiClientHandle **client);
+
+/**
+ * Automatically creates and connects to AMFI service, returning a client handle
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode amfi_new(struct IdeviceHandle *socket, struct AmfiClientHandle **client);
+
+/**
+ * Shows the option in the settings UI
+ *
+ * # Arguments
+ * * `client` - A valid AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode amfi_reveal_developer_mode_option_in_ui(struct AmfiClientHandle *client);
+
+/**
+ * Enables developer mode on the device
+ *
+ * # Arguments
+ * * `client` - A valid AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode amfi_enable_developer_mode(struct AmfiClientHandle *client);
+
+/**
+ * Accepts developer mode on the device
+ *
+ * # Arguments
+ * * `client` - A valid AmfiClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode amfi_accept_developer_mode(struct AmfiClientHandle *client);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void amfi_client_free(struct AmfiClientHandle *handle);
+
+/**
+ * Automatically creates and connects to Core Device Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated CoreDeviceProxy handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode core_device_proxy_connect_tcp(struct TcpProviderHandle *provider,
+ struct CoreDeviceProxyHandle **client);
+
+/**
+ * Automatically creates and connects to Core Device Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated CoreDeviceProxy handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode core_device_proxy_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct CoreDeviceProxyHandle **client);
+
+/**
+ * Automatically creates and connects to Core Device Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated CoreDeviceProxy handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode core_device_proxy_new(struct IdeviceHandle *socket,
+ struct CoreDeviceProxyHandle **client);
+
+/**
+ * Sends data through the CoreDeviceProxy tunnel
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`data`] - The data to send
+ * * [`length`] - The length of the data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `data` must be a valid pointer to at least `length` bytes
+ */
+enum IdeviceErrorCode core_device_proxy_send(struct CoreDeviceProxyHandle *handle,
+ const uint8_t *data,
+ uintptr_t length);
+
+/**
+ * Receives data from the CoreDeviceProxy tunnel
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`data`] - Pointer to a buffer where the received data will be stored
+ * * [`length`] - Pointer to store the actual length of received data
+ * * [`max_length`] - Maximum number of bytes that can be stored in `data`
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `data` must be a valid pointer to at least `max_length` bytes
+ * `length` must be a valid pointer to a usize
+ */
+enum IdeviceErrorCode core_device_proxy_recv(struct CoreDeviceProxyHandle *handle,
+ uint8_t *data,
+ uintptr_t *length,
+ uintptr_t max_length);
+
+/**
+ * Gets the client parameters from the handshake
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`mtu`] - Pointer to store the MTU value
+ * * [`address`] - Pointer to store the IP address string (must be at least 16 bytes)
+ * * [`netmask`] - Pointer to store the netmask string (must be at least 16 bytes)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `mtu` must be a valid pointer to a u16
+ * `address` and `netmask` must be valid pointers to buffers of at least 16 bytes
+ */
+enum IdeviceErrorCode core_device_proxy_get_client_parameters(struct CoreDeviceProxyHandle *handle,
+ uint16_t *mtu,
+ char **address,
+ char **netmask);
+
+/**
+ * Gets the server address from the handshake
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`address`] - Pointer to store the server address string (must be at least 16 bytes)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `address` must be a valid pointer to a buffer of at least 16 bytes
+ */
+enum IdeviceErrorCode core_device_proxy_get_server_address(struct CoreDeviceProxyHandle *handle,
+ char **address);
+
+/**
+ * Gets the server RSD port from the handshake
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`port`] - Pointer to store the port number
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `port` must be a valid pointer to a u16
+ */
+enum IdeviceErrorCode core_device_proxy_get_server_rsd_port(struct CoreDeviceProxyHandle *handle,
+ uint16_t *port);
+
+/**
+ * Creates a software TCP tunnel adapter
+ *
+ * # Arguments
+ * * [`handle`] - The CoreDeviceProxy handle
+ * * [`adapter`] - Pointer to store the newly created adapter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library, and never used again
+ * `adapter` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode core_device_proxy_create_tcp_adapter(struct CoreDeviceProxyHandle *handle,
+ struct AdapterHandle **adapter);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void core_device_proxy_free(struct CoreDeviceProxyHandle *handle);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void adapter_free(struct AdapterHandle *handle);
+
+/**
+ * Creates a new DebugserverCommand
+ *
+ * # Safety
+ * Caller must free with debugserver_command_free
+ */
+struct DebugserverCommandHandle *debugserver_command_new(const char *name,
+ const char *const *argv,
+ uintptr_t argv_count);
+
+/**
+ * Frees a DebugserverCommand
+ *
+ * # Safety
+ * `command` must be a valid pointer or NULL
+ */
+void debugserver_command_free(struct DebugserverCommandHandle *command);
+
+/**
+ * Creates a new DebugProxyClient
+ *
+ * # Arguments
+ * * [`socket`] - The socket to use for communication
+ * * [`handle`] - Pointer to store the newly created DebugProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `handle` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode debug_proxy_adapter_new(struct AdapterHandle *socket,
+ struct DebugProxyAdapterHandle **handle);
+
+/**
+ * Frees a DebugProxyClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void debug_proxy_free(struct DebugProxyAdapterHandle *handle);
+
+/**
+ * Sends a command to the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`command`] - The command to send
+ * * [`response`] - Pointer to store the response (caller must free)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` and `command` must be valid pointers
+ * `response` must be a valid pointer to a location where the string will be stored
+ */
+enum IdeviceErrorCode debug_proxy_send_command(struct DebugProxyAdapterHandle *handle,
+ struct DebugserverCommandHandle *command,
+ char **response);
+
+/**
+ * Reads a response from the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`response`] - Pointer to store the response (caller must free)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ * `response` must be a valid pointer to a location where the string will be stored
+ */
+enum IdeviceErrorCode debug_proxy_read_response(struct DebugProxyAdapterHandle *handle,
+ char **response);
+
+/**
+ * Sends raw data to the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`data`] - The data to send
+ * * [`len`] - Length of the data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ * `data` must be a valid pointer to `len` bytes
+ */
+enum IdeviceErrorCode debug_proxy_send_raw(struct DebugProxyAdapterHandle *handle,
+ const uint8_t *data,
+ uintptr_t len);
+
+/**
+ * Reads data from the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`len`] - Maximum number of bytes to read
+ * * [`response`] - Pointer to store the response (caller must free)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ * `response` must be a valid pointer to a location where the string will be stored
+ */
+enum IdeviceErrorCode debug_proxy_read(struct DebugProxyAdapterHandle *handle,
+ uintptr_t len,
+ char **response);
+
+/**
+ * Sets the argv for the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`argv`] - NULL-terminated array of arguments
+ * * [`argv_count`] - Number of arguments
+ * * [`response`] - Pointer to store the response (caller must free)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ * `argv` must be a valid pointer to `argv_count` C strings or NULL
+ * `response` must be a valid pointer to a location where the string will be stored
+ */
+enum IdeviceErrorCode debug_proxy_set_argv(struct DebugProxyAdapterHandle *handle,
+ const char *const *argv,
+ uintptr_t argv_count,
+ char **response);
+
+/**
+ * Sends an ACK to the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ */
+enum IdeviceErrorCode debug_proxy_send_ack(struct DebugProxyAdapterHandle *handle);
+
+/**
+ * Sends a NACK to the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ */
+enum IdeviceErrorCode debug_proxy_send_nack(struct DebugProxyAdapterHandle *handle);
+
+/**
+ * Sets the ACK mode for the debug proxy
+ *
+ * # Arguments
+ * * [`handle`] - The DebugProxyClient handle
+ * * [`enabled`] - Whether ACK mode should be enabled
+ *
+ * # Safety
+ * `handle` must be a valid pointer
+ */
+void debug_proxy_set_ack_mode(struct DebugProxyAdapterHandle *handle, int enabled);
+
+/**
+ * Returns the underlying socket from a DebugProxyClient
+ *
+ * # Arguments
+ * * [`handle`] - The handle to get the socket from
+ * * [`adapter`] - The newly allocated ConnectionHandle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL, and never used again
+ */
+enum IdeviceErrorCode debug_proxy_adapter_into_inner(struct DebugProxyAdapterHandle *handle,
+ struct AdapterHandle **adapter);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode heartbeat_connect_tcp(struct TcpProviderHandle *provider,
+ struct HeartbeatClientHandle **client);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode heartbeat_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct HeartbeatClientHandle **client);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode heartbeat_new(struct IdeviceHandle *socket,
+ struct HeartbeatClientHandle **client);
+
+/**
+ * Sends a polo to the device
+ *
+ * # Arguments
+ * * `client` - A valid HeartbeatClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode heartbeat_send_polo(struct HeartbeatClientHandle *client);
+
+/**
+ * Sends a polo to the device
+ *
+ * # Arguments
+ * * `client` - A valid HeartbeatClient handle
+ * * `interval` - The time to wait for a marco
+ * * `new_interval` - A pointer to set the requested marco
+ *
+ * # Returns
+ * An error code indicating success or failure.
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode heartbeat_get_marco(struct HeartbeatClientHandle *client,
+ uint64_t interval,
+ uint64_t *new_interval);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void heartbeat_client_free(struct HeartbeatClientHandle *handle);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode installation_proxy_connect_tcp(struct TcpProviderHandle *provider,
+ struct InstallationProxyClientHandle **client);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode installation_proxy_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct InstallationProxyClientHandle **client);
+
+/**
+ * Automatically creates and connects to Installation Proxy, returning a client handle
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated InstallationProxyClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode installation_proxy_new(struct IdeviceHandle *socket,
+ struct InstallationProxyClientHandle **client);
+
+/**
+ * Gets installed apps on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`application_type`] - The application type to filter by (optional, NULL for "Any")
+ * * [`bundle_identifiers`] - The identifiers to filter by (optional, NULL for all apps)
+ * * [`out_result`] - On success, will be set to point to a newly allocated array of PlistRef
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `out_result` must be a valid, non-null pointer to a location where the result will be stored
+ */
+enum IdeviceErrorCode installation_proxy_get_apps(struct InstallationProxyClientHandle *client,
+ const char *application_type,
+ const char *const *bundle_identifiers,
+ size_t bundle_identifiers_len,
+ void **out_result,
+ size_t *out_result_len);
+
+/**
+ * Frees a handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void installation_proxy_client_free(struct InstallationProxyClientHandle *handle);
+
+/**
+ * Installs an application package on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`package_path`] - Path to the .ipa package in the AFC jail
+ * * [`options`] - Optional installation options as a plist dictionary (can be NULL)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `package_path` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_install(struct InstallationProxyClientHandle *client,
+ const char *package_path,
+ void *options);
+
+/**
+ * Installs an application package on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`package_path`] - Path to the .ipa package in the AFC jail
+ * * [`options`] - Optional installation options as a plist dictionary (can be NULL)
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `package_path` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_install_with_callback(struct InstallationProxyClientHandle *client,
+ const char *package_path,
+ void *options,
+ void (*callback)(uint64_t progress,
+ void *context),
+ void *context);
+
+/**
+ * Upgrades an existing application on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`package_path`] - Path to the .ipa package in the AFC jail
+ * * [`options`] - Optional upgrade options as a plist dictionary (can be NULL)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `package_path` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_upgrade(struct InstallationProxyClientHandle *client,
+ const char *package_path,
+ void *options);
+
+/**
+ * Upgrades an existing application on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`package_path`] - Path to the .ipa package in the AFC jail
+ * * [`options`] - Optional upgrade options as a plist dictionary (can be NULL)
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `package_path` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_upgrade_with_callback(struct InstallationProxyClientHandle *client,
+ const char *package_path,
+ void *options,
+ void (*callback)(uint64_t progress,
+ void *context),
+ void *context);
+
+/**
+ * Uninstalls an application from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`bundle_id`] - Bundle identifier of the application to uninstall
+ * * [`options`] - Optional uninstall options as a plist dictionary (can be NULL)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `bundle_id` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_uninstall(struct InstallationProxyClientHandle *client,
+ const char *bundle_id,
+ void *options);
+
+/**
+ * Uninstalls an application from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`bundle_id`] - Bundle identifier of the application to uninstall
+ * * [`options`] - Optional uninstall options as a plist dictionary (can be NULL)
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `bundle_id` must be a valid C string
+ * `options` must be a valid plist dictionary or NULL
+ */
+enum IdeviceErrorCode installation_proxy_uninstall_with_callback(struct InstallationProxyClientHandle *client,
+ const char *bundle_id,
+ void *options,
+ void (*callback)(uint64_t progress,
+ void *context),
+ void *context);
+
+/**
+ * Checks if the device capabilities match the required capabilities
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`capabilities`] - Array of plist values representing required capabilities
+ * * [`capabilities_len`] - Length of the capabilities array
+ * * [`options`] - Optional check options as a plist dictionary (can be NULL)
+ * * [`out_result`] - Will be set to true if all capabilities are supported, false otherwise
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `capabilities` must be a valid array of plist values or NULL
+ * `options` must be a valid plist dictionary or NULL
+ * `out_result` must be a valid pointer to a bool
+ */
+enum IdeviceErrorCode installation_proxy_check_capabilities_match(struct InstallationProxyClientHandle *client,
+ void *const *capabilities,
+ size_t capabilities_len,
+ void *options,
+ bool *out_result);
+
+/**
+ * Browses installed applications on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid InstallationProxyClient handle
+ * * [`options`] - Optional browse options as a plist dictionary (can be NULL)
+ * * [`out_result`] - On success, will be set to point to a newly allocated array of PlistRef
+ * * [`out_result_len`] - Will be set to the length of the result array
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `options` must be a valid plist dictionary or NULL
+ * `out_result` must be a valid, non-null pointer to a location where the result will be stored
+ * `out_result_len` must be a valid, non-null pointer to a location where the length will be stored
+ */
+enum IdeviceErrorCode installation_proxy_browse(struct InstallationProxyClientHandle *client,
+ void *options,
+ void **out_result,
+ size_t *out_result_len);
+
+/**
+ * Creates a new ProcessControlClient from a RemoteServerClient
+ *
+ * # Arguments
+ * * [`server`] - The RemoteServerClient to use
+ * * [`handle`] - Pointer to store the newly created ProcessControlClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `server` must be a valid pointer to a handle allocated by this library
+ * `handle` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode location_simulation_new(struct RemoteServerAdapterHandle *server,
+ struct LocationSimulationAdapterHandle **handle);
+
+/**
+ * Frees a ProcessControlClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void location_simulation_free(struct LocationSimulationAdapterHandle *handle);
+
+/**
+ * Clears the location set
+ *
+ * # Arguments
+ * * [`handle`] - The LocationSimulation handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid or NULL where appropriate
+ */
+enum IdeviceErrorCode location_simulation_clear(struct LocationSimulationAdapterHandle *handle);
+
+/**
+ * Sets the location
+ *
+ * # Arguments
+ * * [`handle`] - The LocationSimulation handle
+ * * [`latitude`] - The latitude to set
+ * * [`longitude`] - The longitude to set
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid or NULL where appropriate
+ */
+enum IdeviceErrorCode location_simulation_set(struct LocationSimulationAdapterHandle *handle,
+ double latitude,
+ double longitude);
+
+/**
+ * Connects to lockdownd service using TCP provider
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated LockdowndClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode lockdownd_connect_tcp(struct TcpProviderHandle *provider,
+ struct LockdowndClientHandle **client);
+
+/**
+ * Connects to lockdownd service using Usbmuxd provider
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated LockdowndClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode lockdownd_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct LockdowndClientHandle **client);
+
+/**
+ * Creates a new LockdowndClient from an existing Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated LockdowndClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode lockdownd_new(struct IdeviceHandle *socket,
+ struct LockdowndClientHandle **client);
+
+/**
+ * Starts a session with lockdownd
+ *
+ * # Arguments
+ * * `client` - A valid LockdowndClient handle
+ * * `pairing_file` - An IdevicePairingFile alocated by this library
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `pairing_file` must be a valid plist_t containing a pairing file
+ */
+enum IdeviceErrorCode lockdownd_start_session(struct LockdowndClientHandle *client,
+ struct IdevicePairingFile *pairing_file);
+
+/**
+ * Starts a service through lockdownd
+ *
+ * # Arguments
+ * * `client` - A valid LockdowndClient handle
+ * * `identifier` - The service identifier to start (null-terminated string)
+ * * `port` - Pointer to store the returned port number
+ * * `ssl` - Pointer to store whether SSL should be enabled
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `identifier` must be a valid null-terminated string
+ * `port` and `ssl` must be valid pointers
+ */
+enum IdeviceErrorCode lockdownd_start_service(struct LockdowndClientHandle *client,
+ const char *identifier,
+ uint16_t *port,
+ bool *ssl);
+
+/**
+ * Gets a value from lockdownd
+ *
+ * # Arguments
+ * * `client` - A valid LockdowndClient handle
+ * * `key` - The value to get (null-terminated string)
+ * * `domain` - The value to get (null-terminated string)
+ * * `out_plist` - Pointer to store the returned plist value
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `value` must be a valid null-terminated string
+ * `out_plist` must be a valid pointer to store the plist
+ */
+enum IdeviceErrorCode lockdownd_get_value(struct LockdowndClientHandle *client,
+ const char *key,
+ const char *domain,
+ void **out_plist);
+
+/**
+ * Gets all values from lockdownd
+ *
+ * # Arguments
+ * * `client` - A valid LockdowndClient handle
+ * * `out_plist` - Pointer to store the returned plist dictionary
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `out_plist` must be a valid pointer to store the plist
+ */
+enum IdeviceErrorCode lockdownd_get_all_values(struct LockdowndClientHandle *client,
+ void **out_plist);
+
+/**
+ * Frees a LockdowndClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void lockdownd_client_free(struct LockdowndClientHandle *handle);
+
+/**
+ * Initializes the logger
+ *
+ * # Arguments
+ * * [`console_level`] - The level to log to the file
+ * * [`file_level`] - The level to log to the file
+ * * [`file_path`] - If not null, the file to write logs to
+ *
+ * ## Log Level
+ * 0. Disabled
+ * 1. Error
+ * 2. Warn
+ * 3. Info
+ * 4. Debug
+ * 5. Trace
+ *
+ * # Returns
+ * 0 for success, -1 if the file couldn't be created, -2 if a logger has been initialized, -3 for invalid path string
+ *
+ * # Safety
+ * Pass a valid CString for file_path. Pass valid log levels according to the enum
+ */
+enum IdeviceLoggerError idevice_init_logger(enum IdeviceLogLevel console_level,
+ enum IdeviceLogLevel file_level,
+ char *file_path);
+
+/**
+ * Automatically creates and connects to Misagent, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated MisagentClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode misagent_connect_tcp(struct TcpProviderHandle *provider,
+ struct MisagentClientHandle **client);
+
+/**
+ * Automatically creates and connects to Misagent, returning a client handle
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated MisagentClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode misagent_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct MisagentClientHandle **client);
+
+/**
+ * Installs a provisioning profile on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid MisagentClient handle
+ * * [`profile_data`] - The provisioning profile data to install
+ * * [`profile_len`] - Length of the profile data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `profile_data` must be a valid pointer to profile data of length `profile_len`
+ */
+enum IdeviceErrorCode misagent_install(struct MisagentClientHandle *client,
+ const uint8_t *profile_data,
+ size_t profile_len);
+
+/**
+ * Removes a provisioning profile from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid MisagentClient handle
+ * * [`profile_id`] - The UUID of the profile to remove (C string)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `profile_id` must be a valid C string
+ */
+enum IdeviceErrorCode misagent_remove(struct MisagentClientHandle *client, const char *profile_id);
+
+/**
+ * Retrieves all provisioning profiles from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid MisagentClient handle
+ * * [`out_profiles`] - On success, will be set to point to an array of profile data
+ * * [`out_profiles_len`] - On success, will be set to the number of profiles
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `out_profiles` must be a valid pointer to store the resulting array
+ * `out_profiles_len` must be a valid pointer to store the array length
+ */
+enum IdeviceErrorCode misagent_copy_all(struct MisagentClientHandle *client,
+ uint8_t ***out_profiles,
+ size_t **out_profiles_len,
+ size_t *out_count);
+
+/**
+ * Frees profiles array returned by misagent_copy_all
+ *
+ * # Arguments
+ * * [`profiles`] - Array of profile data pointers
+ * * [`lens`] - Array of profile lengths
+ * * [`count`] - Number of profiles in the array
+ *
+ * # Safety
+ * Must only be called with values returned from misagent_copy_all
+ */
+void misagent_free_profiles(uint8_t **profiles, size_t *lens, size_t count);
+
+/**
+ * Frees a misagent client handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void misagent_client_free(struct MisagentClientHandle *handle);
+
+/**
+ * Connects to the Image Mounter service using a TCP provider
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode image_mounter_connect_tcp(struct TcpProviderHandle *provider,
+ struct ImageMounterHandle **client);
+
+/**
+ * Connects to the Image Mounter service using a Usbmuxd provider
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode image_mounter_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct ImageMounterHandle **client);
+
+/**
+ * Creates a new ImageMounter client from an existing Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode image_mounter_new(struct IdeviceHandle *socket,
+ struct ImageMounterHandle **client);
+
+/**
+ * Frees an ImageMounter handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void image_mounter_free(struct ImageMounterHandle *handle);
+
+/**
+ * Gets a list of mounted devices
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`devices`] - Will be set to point to a slice of device plists on success
+ * * [`devices_len`] - Will be set to the number of devices copied
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `devices` must be a valid, non-null pointer to a location where the plist will be stored
+ */
+enum IdeviceErrorCode image_mounter_copy_devices(struct ImageMounterHandle *client,
+ void **devices,
+ size_t *devices_len);
+
+/**
+ * Looks up an image and returns its signature
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image to look up
+ * * [`signature`] - Will be set to point to the signature data on success
+ * * [`signature_len`] - Will be set to the length of the signature data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `image_type` must be a valid null-terminated C string
+ * `signature` and `signature_len` must be valid pointers
+ */
+enum IdeviceErrorCode image_mounter_lookup_image(struct ImageMounterHandle *client,
+ const char *image_type,
+ uint8_t **signature,
+ size_t *signature_len);
+
+/**
+ * Uploads an image to the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image being uploaded
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`signature`] - Pointer to the signature data
+ * * [`signature_len`] - Length of the signature data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `image_type` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode image_mounter_upload_image(struct ImageMounterHandle *client,
+ const char *image_type,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *signature,
+ size_t signature_len);
+
+/**
+ * Mounts an image on the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image being mounted
+ * * [`signature`] - Pointer to the signature data
+ * * [`signature_len`] - Length of the signature data
+ * * [`trust_cache`] - Pointer to trust cache data (optional)
+ * * [`trust_cache_len`] - Length of trust cache data (0 if none)
+ * * [`info_plist`] - Pointer to info plist (optional)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ * `image_type` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode image_mounter_mount_image(struct ImageMounterHandle *client,
+ const char *image_type,
+ const uint8_t *signature,
+ size_t signature_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const void *info_plist);
+
+/**
+ * Unmounts an image from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`mount_path`] - The path where the image is mounted
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `mount_path` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode image_mounter_unmount_image(struct ImageMounterHandle *client,
+ const char *mount_path);
+
+/**
+ * Queries the developer mode status
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`status`] - Will be set to the developer mode status (1 = enabled, 0 = disabled)
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `status` must be a valid pointer
+ */
+enum IdeviceErrorCode image_mounter_query_developer_mode_status(struct ImageMounterHandle *client,
+ int *status);
+
+/**
+ * Mounts a developer image
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`signature`] - Pointer to the signature data
+ * * [`signature_len`] - Length of the signature data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ */
+enum IdeviceErrorCode image_mounter_mount_developer(struct ImageMounterHandle *client,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *signature,
+ size_t signature_len);
+
+/**
+ * Queries the personalization manifest from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image to query
+ * * [`signature`] - Pointer to the signature data
+ * * [`signature_len`] - Length of the signature data
+ * * [`manifest`] - Will be set to point to the manifest data on success
+ * * [`manifest_len`] - Will be set to the length of the manifest data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid and non-null
+ * `image_type` must be a valid null-terminated C string
+ */
+enum IdeviceErrorCode image_mounter_query_personalization_manifest(struct ImageMounterHandle *client,
+ const char *image_type,
+ const uint8_t *signature,
+ size_t signature_len,
+ uint8_t **manifest,
+ size_t *manifest_len);
+
+/**
+ * Queries the nonce from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`personalized_image_type`] - The type of image to query (optional)
+ * * [`nonce`] - Will be set to point to the nonce data on success
+ * * [`nonce_len`] - Will be set to the length of the nonce data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client`, `nonce`, and `nonce_len` must be valid pointers
+ * `personalized_image_type` can be NULL
+ */
+enum IdeviceErrorCode image_mounter_query_nonce(struct ImageMounterHandle *client,
+ const char *personalized_image_type,
+ uint8_t **nonce,
+ size_t *nonce_len);
+
+/**
+ * Queries personalization identifiers from the device
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`image_type`] - The type of image to query (optional)
+ * * [`identifiers`] - Will be set to point to the identifiers plist on success
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` and `identifiers` must be valid pointers
+ * `image_type` can be NULL
+ */
+enum IdeviceErrorCode image_mounter_query_personalization_identifiers(struct ImageMounterHandle *client,
+ const char *image_type,
+ void **identifiers);
+
+/**
+ * Rolls the personalization nonce
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode image_mounter_roll_personalization_nonce(struct ImageMounterHandle *client);
+
+/**
+ * Rolls the cryptex nonce
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode image_mounter_roll_cryptex_nonce(struct ImageMounterHandle *client);
+
+/**
+ * Mounts a personalized developer image
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`provider`] - A valid provider handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`trust_cache`] - Pointer to the trust cache data
+ * * [`trust_cache_len`] - Length of the trust cache data
+ * * [`build_manifest`] - Pointer to the build manifest data
+ * * [`build_manifest_len`] - Length of the build manifest data
+ * * [`info_plist`] - Pointer to info plist (optional)
+ * * [`unique_chip_id`] - The device's unique chip ID
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ */
+enum IdeviceErrorCode image_mounter_mount_personalized_usbmuxd(struct ImageMounterHandle *client,
+ struct UsbmuxdProviderHandle *provider,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const uint8_t *build_manifest,
+ size_t build_manifest_len,
+ const void *info_plist,
+ uint64_t unique_chip_id);
+
+/**
+ * Mounts a personalized developer image
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`provider`] - A valid provider handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`trust_cache`] - Pointer to the trust cache data
+ * * [`trust_cache_len`] - Length of the trust cache data
+ * * [`build_manifest`] - Pointer to the build manifest data
+ * * [`build_manifest_len`] - Length of the build manifest data
+ * * [`info_plist`] - Pointer to info plist (optional)
+ * * [`unique_chip_id`] - The device's unique chip ID
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ */
+enum IdeviceErrorCode image_mounter_mount_personalized_tcp(struct ImageMounterHandle *client,
+ struct TcpProviderHandle *provider,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const uint8_t *build_manifest,
+ size_t build_manifest_len,
+ const void *info_plist,
+ uint64_t unique_chip_id);
+
+/**
+ * Mounts a personalized developer image with progress callback
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`provider`] - A valid provider handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`trust_cache`] - Pointer to the trust cache data
+ * * [`trust_cache_len`] - Length of the trust cache data
+ * * [`build_manifest`] - Pointer to the build manifest data
+ * * [`build_manifest_len`] - Length of the build manifest data
+ * * [`info_plist`] - Pointer to info plist (optional)
+ * * [`unique_chip_id`] - The device's unique chip ID
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ */
+enum IdeviceErrorCode image_mounter_mount_personalized_usbmuxd_with_callback(struct ImageMounterHandle *client,
+ struct UsbmuxdProviderHandle *provider,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const uint8_t *build_manifest,
+ size_t build_manifest_len,
+ const void *info_plist,
+ uint64_t unique_chip_id,
+ void (*callback)(size_t progress,
+ size_t total,
+ void *context),
+ void *context);
+
+/**
+ * Mounts a personalized developer image with progress callback
+ *
+ * # Arguments
+ * * [`client`] - A valid ImageMounter handle
+ * * [`provider`] - A valid provider handle
+ * * [`image`] - Pointer to the image data
+ * * [`image_len`] - Length of the image data
+ * * [`trust_cache`] - Pointer to the trust cache data
+ * * [`trust_cache_len`] - Length of the trust cache data
+ * * [`build_manifest`] - Pointer to the build manifest data
+ * * [`build_manifest_len`] - Length of the build manifest data
+ * * [`info_plist`] - Pointer to info plist (optional)
+ * * [`unique_chip_id`] - The device's unique chip ID
+ * * [`callback`] - Progress callback function
+ * * [`context`] - User context to pass to callback
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid (except optional ones which can be null)
+ */
+enum IdeviceErrorCode image_mounter_mount_personalized_tcp_with_callback(struct ImageMounterHandle *client,
+ struct TcpProviderHandle *provider,
+ const uint8_t *image,
+ size_t image_len,
+ const uint8_t *trust_cache,
+ size_t trust_cache_len,
+ const uint8_t *build_manifest,
+ size_t build_manifest_len,
+ const void *info_plist,
+ uint64_t unique_chip_id,
+ void (*callback)(size_t progress,
+ size_t total,
+ void *context),
+ void *context);
+
+/**
+ * Reads a pairing file from the specified path
+ *
+ * # Arguments
+ * * [`path`] - Path to the pairing file
+ * * [`pairing_file`] - On success, will be set to point to a newly allocated pairing file instance
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `path` must be a valid null-terminated C string
+ * `pairing_file` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_pairing_file_read(const char *path,
+ struct IdevicePairingFile **pairing_file);
+
+/**
+ * Parses a pairing file from a byte buffer
+ *
+ * # Arguments
+ * * [`data`] - Pointer to the buffer containing pairing file data
+ * * [`size`] - Size of the buffer in bytes
+ * * [`pairing_file`] - On success, will be set to point to a newly allocated pairing file instance
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `data` must be a valid pointer to a buffer of at least `size` bytes
+ * `pairing_file` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_pairing_file_from_bytes(const uint8_t *data,
+ uintptr_t size,
+ struct IdevicePairingFile **pairing_file);
+
+/**
+ * Serializes a pairing file to XML format
+ *
+ * # Arguments
+ * * [`pairing_file`] - The pairing file to serialize
+ * * [`data`] - On success, will be set to point to a newly allocated buffer containing the serialized data
+ * * [`size`] - On success, will be set to the size of the allocated buffer
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `pairing_file` must be a valid, non-null pointer to a pairing file instance
+ * `data` must be a valid, non-null pointer to a location where the buffer pointer will be stored
+ * `size` must be a valid, non-null pointer to a location where the buffer size will be stored
+ */
+enum IdeviceErrorCode idevice_pairing_file_serialize(const struct IdevicePairingFile *pairing_file,
+ uint8_t **data,
+ uintptr_t *size);
+
+/**
+ * Frees a pairing file instance
+ *
+ * # Arguments
+ * * [`pairing_file`] - The pairing file to free
+ *
+ * # Safety
+ * `pairing_file` must be a valid pointer to a pairing file instance that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_pairing_file_free(struct IdevicePairingFile *pairing_file);
+
+/**
+ * Creates a new ProcessControlClient from a RemoteServerClient
+ *
+ * # Arguments
+ * * [`server`] - The RemoteServerClient to use
+ * * [`handle`] - Pointer to store the newly created ProcessControlClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `server` must be a valid pointer to a handle allocated by this library
+ * `handle` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode process_control_new(struct RemoteServerAdapterHandle *server,
+ struct ProcessControlAdapterHandle **handle);
+
+/**
+ * Frees a ProcessControlClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void process_control_free(struct ProcessControlAdapterHandle *handle);
+
+/**
+ * Launches an application on the device
+ *
+ * # Arguments
+ * * [`handle`] - The ProcessControlClient handle
+ * * [`bundle_id`] - The bundle identifier of the app to launch
+ * * [`env_vars`] - NULL-terminated array of environment variables (format "KEY=VALUE")
+ * * [`arguments`] - NULL-terminated array of arguments
+ * * [`start_suspended`] - Whether to start the app suspended
+ * * [`kill_existing`] - Whether to kill existing instances of the app
+ * * [`pid`] - Pointer to store the process ID of the launched app
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * All pointers must be valid or NULL where appropriate
+ */
+enum IdeviceErrorCode process_control_launch_app(struct ProcessControlAdapterHandle *handle,
+ const char *bundle_id,
+ const char *const *env_vars,
+ uintptr_t env_vars_count,
+ const char *const *arguments,
+ uintptr_t arguments_count,
+ bool start_suspended,
+ bool kill_existing,
+ uint64_t *pid);
+
+/**
+ * Kills a running process
+ *
+ * # Arguments
+ * * [`handle`] - The ProcessControlClient handle
+ * * [`pid`] - The process ID to kill
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode process_control_kill_app(struct ProcessControlAdapterHandle *handle,
+ uint64_t pid);
+
+/**
+ * Disables memory limits for a process
+ *
+ * # Arguments
+ * * [`handle`] - The ProcessControlClient handle
+ * * [`pid`] - The process ID to modify
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ */
+enum IdeviceErrorCode process_control_disable_memory_limit(struct ProcessControlAdapterHandle *handle,
+ uint64_t pid);
+
+/**
+ * Creates a TCP provider for idevice
+ *
+ * # Arguments
+ * * [`ip`] - The sockaddr IP to connect to
+ * * [`pairing_file`] - The pairing file handle to use
+ * * [`label`] - The label to use with the connection
+ * * [`provider`] - A pointer to a newly allocated provider
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `ip` must be a valid sockaddr
+ * `pairing_file` must never be used again
+ * `label` must be a valid Cstr
+ * `provider` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_tcp_provider_new(const struct sockaddr *ip,
+ struct IdevicePairingFile *pairing_file,
+ const char *label,
+ struct TcpProviderHandle **provider);
+
+/**
+ * Frees a TcpProvider handle
+ *
+ * # Arguments
+ * * [`provider`] - The provider handle to free
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a TcpProvider handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void tcp_provider_free(struct TcpProviderHandle *provider);
+
+/**
+ * Creates a usbmuxd provider for idevice
+ *
+ * # Arguments
+ * * [`addr`] - The UsbmuxdAddr handle to connect to
+ * * [`tag`] - The tag returned in usbmuxd responses
+ * * [`udid`] - The UDID of the device to connect to
+ * * [`device_id`] - The muxer ID of the device to connect to
+ * * [`pairing_file`] - The pairing file handle to use
+ * * [`label`] - The label to use with the connection
+ * * [`provider`] - A pointer to a newly allocated provider
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid pointer to UsbmuxdAddrHandle created by this library, and never used again
+ * `udid` must be a valid CStr
+ * `pairing_file` must never be used again
+ * `label` must be a valid Cstr
+ * `provider` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode usbmuxd_provider_new(struct UsbmuxdAddrHandle *addr,
+ uint32_t tag,
+ const char *udid,
+ uint32_t device_id,
+ const char *label,
+ struct UsbmuxdProviderHandle **provider);
+
+/**
+ * Frees a UsbmuxdProvider handle
+ *
+ * # Arguments
+ * * [`provider`] - The provider handle to free
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a UsbmuxdProvider handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void usbmuxd_provider_free(struct UsbmuxdProviderHandle *provider);
+
+/**
+ * Creates a new RemoteServerClient from a ReadWrite connection
+ *
+ * # Arguments
+ * * [`connection`] - The connection to use for communication
+ * * [`handle`] - Pointer to store the newly created RemoteServerClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `connection` must be a valid pointer to a handle allocated by this library
+ * `handle` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode remote_server_adapter_new(struct AdapterHandle *adapter,
+ struct RemoteServerAdapterHandle **handle);
+
+/**
+ * Frees a RemoteServerClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void remote_server_free(struct RemoteServerAdapterHandle *handle);
+
+/**
+ * Returns the underlying connection from a RemoteServerClient
+ *
+ * # Arguments
+ * * [`handle`] - The handle to get the connection from
+ * * [`connection`] - The newly allocated ConnectionHandle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL, and never used again
+ */
+enum IdeviceErrorCode remote_server_adapter_into_inner(struct RemoteServerAdapterHandle *handle,
+ struct AdapterHandle **connection);
+
+/**
+ * Creates a new XPCDevice from an adapter
+ *
+ * # Arguments
+ * * [`adapter`] - The adapter to use for communication
+ * * [`device`] - Pointer to store the newly created XPCDevice handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `adapter` must be a valid pointer to a handle allocated by this library
+ * `device` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode xpc_device_new(struct AdapterHandle *adapter,
+ struct XPCDeviceAdapterHandle **device);
+
+/**
+ * Frees an XPCDevice handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void xpc_device_free(struct XPCDeviceAdapterHandle *handle);
+
+/**
+ * Gets a service by name from the XPCDevice
+ *
+ * # Arguments
+ * * [`handle`] - The XPCDevice handle
+ * * [`service_name`] - The name of the service to get
+ * * [`service`] - Pointer to store the newly created XPCService handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `service_name` must be a valid null-terminated C string
+ * `service` must be a valid pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode xpc_device_get_service(struct XPCDeviceAdapterHandle *handle,
+ const char *service_name,
+ struct XPCServiceHandle **service);
+
+/**
+ * Returns the adapter in the RemoteXPC Device
+ *
+ * # Arguments
+ * * [`handle`] - The handle to get the adapter from
+ * * [`adapter`] - The newly allocated AdapterHandle
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL, and never used again
+ */
+enum IdeviceErrorCode xpc_device_adapter_into_inner(struct XPCDeviceAdapterHandle *handle,
+ struct AdapterHandle **adapter);
+
+/**
+ * Frees an XPCService handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library or NULL
+ */
+void xpc_service_free(struct XPCServiceHandle *handle);
+
+/**
+ * Gets the list of available service names
+ *
+ * # Arguments
+ * * [`handle`] - The XPCDevice handle
+ * * [`names`] - Pointer to store the array of service names
+ * * [`count`] - Pointer to store the number of services
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `handle` must be a valid pointer to a handle allocated by this library
+ * `names` must be a valid pointer to a location where the array will be stored
+ * `count` must be a valid pointer to a location where the count will be stored
+ */
+enum IdeviceErrorCode xpc_device_get_service_names(struct XPCDeviceAdapterHandle *handle,
+ char ***names,
+ uintptr_t *count);
+
+/**
+ * Frees a list of service names
+ *
+ * # Arguments
+ * * [`names`] - The array of service names to free
+ * * [`count`] - The number of services in the array
+ *
+ * # Safety
+ * `names` must be a valid pointer to an array of `count` C strings
+ */
+void xpc_device_free_service_names(char **names, uintptr_t count);
+
+/**
+ * Connects to the Springboard service using a TCP provider
+ *
+ * # Arguments
+ * * [`provider`] - A TcpProvider
+ * * [`client`] - On success, will be set to point to a newly allocated SpringBoardServicesClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode springboard_services_connect_tcp(struct TcpProviderHandle *provider,
+ struct SpringBoardServicesClientHandle **client);
+
+/**
+ * Connects to the Springboard service using a usbmuxd provider
+ *
+ * # Arguments
+ * * [`provider`] - A UsbmuxdProvider
+ * * [`client`] - On success, will be set to point to a newly allocated SpringBoardServicesClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `provider` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode springboard_services_connect_usbmuxd(struct UsbmuxdProviderHandle *provider,
+ struct SpringBoardServicesClientHandle **client);
+
+/**
+ * Creates a new SpringBoardServices client from an existing Idevice connection
+ *
+ * # Arguments
+ * * [`socket`] - An IdeviceSocket handle
+ * * [`client`] - On success, will be set to point to a newly allocated SpringBoardServicesClient handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `socket` must be a valid pointer to a handle allocated by this library
+ * `client` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode springboard_services_new(struct IdeviceHandle *socket,
+ struct SpringBoardServicesClientHandle **client);
+
+/**
+ * Gets the icon of the specified app by bundle identifier
+ *
+ * # Arguments
+ * * `client` - A valid SpringBoardServicesClient handle
+ * * `bundle_identifier` - The identifiers of the app to get icon
+ * * `out_result` - On success, will be set to point to a newly allocated png data
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `client` must be a valid pointer to a handle allocated by this library
+ * `out_result` must be a valid, non-null pointer to a location where the result will be stored
+ */
+enum IdeviceErrorCode springboard_services_get_icon(struct SpringBoardServicesClientHandle *client,
+ const char *bundle_identifier,
+ void **out_result,
+ size_t *out_result_len);
+
+/**
+ * Frees an SpringBoardServicesClient handle
+ *
+ * # Arguments
+ * * [`handle`] - The handle to free
+ *
+ * # Safety
+ * `handle` must be a valid pointer to the handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void springboard_services_free(struct SpringBoardServicesClientHandle *handle);
+
+/**
+ * Connects to a usbmuxd instance over TCP
+ *
+ * # Arguments
+ * * [`addr`] - The socket address to connect to
+ * * [`addr_len`] - Length of the socket
+ * * [`tag`] - A tag that will be returned by usbmuxd responses
+ * * [`usbmuxd_connection`] - On success, will be set to point to a newly allocated UsbmuxdConnection handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid sockaddr
+ * `usbmuxd_connection` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_usbmuxd_new_tcp_connection(const struct sockaddr *addr,
+ socklen_t addr_len,
+ uint32_t tag,
+ struct UsbmuxdConnectionHandle **usbmuxd_connection);
+
+/**
+ * Connects to a usbmuxd instance over unix socket
+ *
+ * # Arguments
+ * * [`addr`] - The socket path to connect to
+ * * [`tag`] - A tag that will be returned by usbmuxd responses
+ * * [`usbmuxd_connection`] - On success, will be set to point to a newly allocated UsbmuxdConnection handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid CStr
+ * `usbmuxd_connection` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_usbmuxd_new_unix_socket_connection(const char *addr,
+ uint32_t tag,
+ struct UsbmuxdConnectionHandle **usbmuxd_connection);
+
+/**
+ * Frees a UsbmuxdConnection handle
+ *
+ * # Arguments
+ * * [`usbmuxd_connection`] - The UsbmuxdConnection handle to free
+ *
+ * # Safety
+ * `usbmuxd_connection` must be a valid pointer to a UsbmuxdConnection handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_usbmuxd_connection_free(struct UsbmuxdConnectionHandle *usbmuxd_connection);
+
+/**
+ * Creates a usbmuxd TCP address struct
+ *
+ * # Arguments
+ * * [`addr`] - The socket address to connect to
+ * * [`addr_len`] - Length of the socket
+ * * [`usbmuxd_addr`] - On success, will be set to point to a newly allocated UsbmuxdAddr handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid sockaddr
+ * `usbmuxd_Addr` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_usbmuxd_tcp_addr_new(const struct sockaddr *addr,
+ socklen_t addr_len,
+ struct UsbmuxdAddrHandle **usbmuxd_addr);
+
+/**
+ * Creates a new UsbmuxdAddr struct with a unix socket
+ *
+ * # Arguments
+ * * [`addr`] - The socket path to connect to
+ * * [`usbmuxd_addr`] - On success, will be set to point to a newly allocated UsbmuxdAddr handle
+ *
+ * # Returns
+ * An error code indicating success or failure
+ *
+ * # Safety
+ * `addr` must be a valid CStr
+ * `usbmuxd_addr` must be a valid, non-null pointer to a location where the handle will be stored
+ */
+enum IdeviceErrorCode idevice_usbmuxd_unix_addr_new(const char *addr,
+ struct UsbmuxdAddrHandle **usbmuxd_addr);
+
+/**
+ * Frees a UsbmuxdAddr handle
+ *
+ * # Arguments
+ * * [`usbmuxd_addr`] - The UsbmuxdAddr handle to free
+ *
+ * # Safety
+ * `usbmuxd_addr` must be a valid pointer to a UsbmuxdAddr handle that was allocated by this library,
+ * or NULL (in which case this function does nothing)
+ */
+void idevice_usbmuxd_addr_free(struct UsbmuxdAddrHandle *usbmuxd_addr);
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/plist.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/plist.h
new file mode 100644
index 000000000..0e211278e
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Headers/plist.h
@@ -0,0 +1,1096 @@
+/**
+ * @file plist/plist.h
+ * @brief Main include of libplist
+ * \internal
+ *
+ * Copyright (c) 2012-2023 Nikias Bassen, All Rights Reserved.
+ * Copyright (c) 2008-2009 Jonathan Beck, All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef LIBPLIST_H
+#define LIBPLIST_H
+
+#if _MSC_VER && _MSC_VER < 1700
+ typedef __int8 int8_t;
+ typedef __int16 int16_t;
+ typedef __int32 int32_t;
+ typedef __int64 int64_t;
+
+ typedef unsigned __int8 uint8_t;
+ typedef unsigned __int16 uint16_t;
+ typedef unsigned __int32 uint32_t;
+ typedef unsigned __int64 uint64_t;
+
+#else
+#include
+#endif
+
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+
+#ifdef __llvm__
+ #if defined(__has_extension)
+ #if (__has_extension(attribute_deprecated_with_message))
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __attribute__((deprecated(x)))
+ #endif
+ #else
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __attribute__((deprecated))
+ #endif
+ #endif
+ #else
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __attribute__((deprecated))
+ #endif
+ #endif
+#elif (__GNUC__ > 4 || (__GNUC__ == 4 && (__GNUC_MINOR__ >= 5)))
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __attribute__((deprecated(x)))
+ #endif
+#elif defined(_MSC_VER)
+ #ifndef PLIST_WARN_DEPRECATED
+ #define PLIST_WARN_DEPRECATED(x) __declspec(deprecated(x))
+ #endif
+#else
+ #define PLIST_WARN_DEPRECATED(x)
+ #pragma message("WARNING: You need to implement DEPRECATED for this compiler")
+#endif
+
+ /**
+ * \mainpage libplist : A library to handle Apple Property Lists
+ * \defgroup PublicAPI Public libplist API
+ */
+ /*@{*/
+
+
+ /**
+ * The basic plist abstract data type.
+ */
+ typedef void *plist_t;
+
+ /**
+ * The plist dictionary iterator.
+ */
+ typedef void* plist_dict_iter;
+
+ /**
+ * The plist array iterator.
+ */
+ typedef void* plist_array_iter;
+
+ /**
+ * The enumeration of plist node types.
+ */
+ typedef enum
+ {
+ PLIST_BOOLEAN, /**< Boolean, scalar type */
+ PLIST_INT, /**< Integer, scalar type */
+ PLIST_REAL, /**< Real, scalar type */
+ PLIST_STRING, /**< ASCII string, scalar type */
+ PLIST_ARRAY, /**< Ordered array, structured type */
+ PLIST_DICT, /**< Unordered dictionary (key/value pair), structured type */
+ PLIST_DATE, /**< Date, scalar type */
+ PLIST_DATA, /**< Binary data, scalar type */
+ PLIST_KEY, /**< Key in dictionaries (ASCII String), scalar type */
+ PLIST_UID, /**< Special type used for 'keyed encoding' */
+ PLIST_NULL, /**< NULL type */
+ PLIST_NONE /**< No type */
+ } plist_type;
+
+ /* for backwards compatibility */
+ #define PLIST_UINT PLIST_INT
+
+ /**
+ * libplist error values
+ */
+ typedef enum
+ {
+ PLIST_ERR_SUCCESS = 0, /**< operation successful */
+ PLIST_ERR_INVALID_ARG = -1, /**< one or more of the parameters are invalid */
+ PLIST_ERR_FORMAT = -2, /**< the plist contains nodes not compatible with the output format */
+ PLIST_ERR_PARSE = -3, /**< parsing of the input format failed */
+ PLIST_ERR_NO_MEM = -4, /**< not enough memory to handle the operation */
+ PLIST_ERR_UNKNOWN = -255 /**< an unspecified error occurred */
+ } plist_err_t;
+
+ /********************************************
+ * *
+ * Creation & Destruction *
+ * *
+ ********************************************/
+
+ /**
+ * Create a new root plist_t type #PLIST_DICT
+ *
+ * @return the created plist
+ * @sa #plist_type
+ */
+ plist_t plist_new_dict(void);
+
+ /**
+ * Create a new root plist_t type #PLIST_ARRAY
+ *
+ * @return the created plist
+ * @sa #plist_type
+ */
+ plist_t plist_new_array(void);
+
+ /**
+ * Create a new plist_t type #PLIST_STRING
+ *
+ * @param val the sting value, encoded in UTF8.
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_string(const char *val);
+
+ /**
+ * Create a new plist_t type #PLIST_BOOLEAN
+ *
+ * @param val the boolean value, 0 is false, other values are true.
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_bool(uint8_t val);
+
+ /**
+ * Create a new plist_t type #PLIST_INT with an unsigned integer value
+ *
+ * @param val the unsigned integer value
+ * @return the created item
+ * @sa #plist_type
+ * @note The value is always stored as uint64_t internally.
+ * Use #plist_get_uint_val or #plist_get_int_val to get the unsigned or signed value.
+ */
+ plist_t plist_new_uint(uint64_t val);
+
+ /**
+ * Create a new plist_t type #PLIST_INT with a signed integer value
+ *
+ * @param val the signed integer value
+ * @return the created item
+ * @sa #plist_type
+ * @note The value is always stored as uint64_t internally.
+ * Use #plist_get_uint_val or #plist_get_int_val to get the unsigned or signed value.
+ */
+ plist_t plist_new_int(int64_t val);
+
+ /**
+ * Create a new plist_t type #PLIST_REAL
+ *
+ * @param val the real value
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_real(double val);
+
+ /**
+ * Create a new plist_t type #PLIST_DATA
+ *
+ * @param val the binary buffer
+ * @param length the length of the buffer
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_data(const char *val, uint64_t length);
+
+ /**
+ * Create a new plist_t type #PLIST_DATE
+ *
+ * @param sec the number of seconds since 01/01/2001
+ * @param usec the number of microseconds
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_date(int32_t sec, int32_t usec);
+
+ /**
+ * Create a new plist_t type #PLIST_UID
+ *
+ * @param val the unsigned integer value
+ * @return the created item
+ * @sa #plist_type
+ */
+ plist_t plist_new_uid(uint64_t val);
+
+ /**
+ * Create a new plist_t type #PLIST_NULL
+ * @return the created item
+ * @sa #plist_type
+ * @note This type is not valid for all formats, e.g. the XML format
+ * does not support it.
+ */
+ plist_t plist_new_null(void);
+
+ /**
+ * Destruct a plist_t node and all its children recursively
+ *
+ * @param plist the plist to free
+ */
+ void plist_free(plist_t plist);
+
+ /**
+ * Return a copy of passed node and it's children
+ *
+ * @param node the plist to copy
+ * @return copied plist
+ */
+ plist_t plist_copy(plist_t node);
+
+
+ /********************************************
+ * *
+ * Array functions *
+ * *
+ ********************************************/
+
+ /**
+ * Get size of a #PLIST_ARRAY node.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @return size of the #PLIST_ARRAY node
+ */
+ uint32_t plist_array_get_size(plist_t node);
+
+ /**
+ * Get the nth item in a #PLIST_ARRAY node.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param n the index of the item to get. Range is [0, array_size[
+ * @return the nth item or NULL if node is not of type #PLIST_ARRAY
+ */
+ plist_t plist_array_get_item(plist_t node, uint32_t n);
+
+ /**
+ * Get the index of an item. item must be a member of a #PLIST_ARRAY node.
+ *
+ * @param node the node
+ * @return the node index or UINT_MAX if node index can't be determined
+ */
+ uint32_t plist_array_get_item_index(plist_t node);
+
+ /**
+ * Set the nth item in a #PLIST_ARRAY node.
+ * The previous item at index n will be freed using #plist_free
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param item the new item at index n. The array is responsible for freeing item when it is no longer needed.
+ * @param n the index of the item to get. Range is [0, array_size[. Assert if n is not in range.
+ */
+ void plist_array_set_item(plist_t node, plist_t item, uint32_t n);
+
+ /**
+ * Append a new item at the end of a #PLIST_ARRAY node.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param item the new item. The array is responsible for freeing item when it is no longer needed.
+ */
+ void plist_array_append_item(plist_t node, plist_t item);
+
+ /**
+ * Insert a new item at position n in a #PLIST_ARRAY node.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param item the new item to insert. The array is responsible for freeing item when it is no longer needed.
+ * @param n The position at which the node will be stored. Range is [0, array_size[. Assert if n is not in range.
+ */
+ void plist_array_insert_item(plist_t node, plist_t item, uint32_t n);
+
+ /**
+ * Remove an existing position in a #PLIST_ARRAY node.
+ * Removed position will be freed using #plist_free.
+ *
+ * @param node the node of type #PLIST_ARRAY
+ * @param n The position to remove. Range is [0, array_size[. Assert if n is not in range.
+ */
+ void plist_array_remove_item(plist_t node, uint32_t n);
+
+ /**
+ * Remove a node that is a child node of a #PLIST_ARRAY node.
+ * node will be freed using #plist_free.
+ *
+ * @param node The node to be removed from its #PLIST_ARRAY parent.
+ */
+ void plist_array_item_remove(plist_t node);
+
+ /**
+ * Create an iterator of a #PLIST_ARRAY node.
+ * The allocated iterator should be freed with the standard free function.
+ *
+ * @param node The node of type #PLIST_ARRAY
+ * @param iter Location to store the iterator for the array.
+ */
+ void plist_array_new_iter(plist_t node, plist_array_iter *iter);
+
+ /**
+ * Increment iterator of a #PLIST_ARRAY node.
+ *
+ * @param node The node of type #PLIST_ARRAY.
+ * @param iter Iterator of the array
+ * @param item Location to store the item. The caller must *not* free the
+ * returned item. Will be set to NULL when no more items are left
+ * to iterate.
+ */
+ void plist_array_next_item(plist_t node, plist_array_iter iter, plist_t *item);
+
+
+ /********************************************
+ * *
+ * Dictionary functions *
+ * *
+ ********************************************/
+
+ /**
+ * Get size of a #PLIST_DICT node.
+ *
+ * @param node the node of type #PLIST_DICT
+ * @return size of the #PLIST_DICT node
+ */
+ uint32_t plist_dict_get_size(plist_t node);
+
+ /**
+ * Create an iterator of a #PLIST_DICT node.
+ * The allocated iterator should be freed with the standard free function.
+ *
+ * @param node The node of type #PLIST_DICT.
+ * @param iter Location to store the iterator for the dictionary.
+ */
+ void plist_dict_new_iter(plist_t node, plist_dict_iter *iter);
+
+ /**
+ * Increment iterator of a #PLIST_DICT node.
+ *
+ * @param node The node of type #PLIST_DICT
+ * @param iter Iterator of the dictionary
+ * @param key Location to store the key, or NULL. The caller is responsible
+ * for freeing the the returned string.
+ * @param val Location to store the value, or NULL. The caller must *not*
+ * free the returned value. Will be set to NULL when no more
+ * key/value pairs are left to iterate.
+ */
+ void plist_dict_next_item(plist_t node, plist_dict_iter iter, char **key, plist_t *val);
+
+ /**
+ * Get key associated key to an item. Item must be member of a dictionary.
+ *
+ * @param node the item
+ * @param key a location to store the key. The caller is responsible for freeing the returned string.
+ */
+ void plist_dict_get_item_key(plist_t node, char **key);
+
+ /**
+ * Get the nth item in a #PLIST_DICT node.
+ *
+ * @param node the node of type #PLIST_DICT
+ * @param key the identifier of the item to get.
+ * @return the item or NULL if node is not of type #PLIST_DICT. The caller should not free
+ * the returned node.
+ */
+ plist_t plist_dict_get_item(plist_t node, const char* key);
+
+ /**
+ * Get key node associated to an item. Item must be member of a dictionary.
+ *
+ * @param node the item
+ * @return the key node of the given item, or NULL.
+ */
+ plist_t plist_dict_item_get_key(plist_t node);
+
+ /**
+ * Set item identified by key in a #PLIST_DICT node.
+ * The previous item identified by key will be freed using #plist_free.
+ * If there is no item for the given key a new item will be inserted.
+ *
+ * @param node the node of type #PLIST_DICT
+ * @param item the new item associated to key
+ * @param key the identifier of the item to set.
+ */
+ void plist_dict_set_item(plist_t node, const char* key, plist_t item);
+
+ /**
+ * Insert a new item into a #PLIST_DICT node.
+ *
+ * @deprecated Deprecated. Use plist_dict_set_item instead.
+ *
+ * @param node the node of type #PLIST_DICT
+ * @param item the new item to insert
+ * @param key The identifier of the item to insert.
+ */
+ PLIST_WARN_DEPRECATED("use plist_dict_set_item instead")
+ void plist_dict_insert_item(plist_t node, const char* key, plist_t item);
+
+ /**
+ * Remove an existing position in a #PLIST_DICT node.
+ * Removed position will be freed using #plist_free
+ *
+ * @param node the node of type #PLIST_DICT
+ * @param key The identifier of the item to remove. Assert if identifier is not present.
+ */
+ void plist_dict_remove_item(plist_t node, const char* key);
+
+ /**
+ * Merge a dictionary into another. This will add all key/value pairs
+ * from the source dictionary to the target dictionary, overwriting
+ * any existing key/value pairs that are already present in target.
+ *
+ * @param target pointer to an existing node of type #PLIST_DICT
+ * @param source node of type #PLIST_DICT that should be merged into target
+ */
+ void plist_dict_merge(plist_t *target, plist_t source);
+
+
+ /********************************************
+ * *
+ * Getters *
+ * *
+ ********************************************/
+
+ /**
+ * Get the parent of a node
+ *
+ * @param node the parent (NULL if node is root)
+ */
+ plist_t plist_get_parent(plist_t node);
+
+ /**
+ * Get the #plist_type of a node.
+ *
+ * @param node the node
+ * @return the type of the node
+ */
+ plist_type plist_get_node_type(plist_t node);
+
+ /**
+ * Get the value of a #PLIST_KEY node.
+ * This function does nothing if node is not of type #PLIST_KEY
+ *
+ * @param node the node
+ * @param val a pointer to a C-string. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ void plist_get_key_val(plist_t node, char **val);
+
+ /**
+ * Get the value of a #PLIST_STRING node.
+ * This function does nothing if node is not of type #PLIST_STRING
+ *
+ * @param node the node
+ * @param val a pointer to a C-string. This function allocates the memory,
+ * caller is responsible for freeing it. Data is UTF-8 encoded.
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ void plist_get_string_val(plist_t node, char **val);
+
+ /**
+ * Get a pointer to the buffer of a #PLIST_STRING node.
+ *
+ * @note DO NOT MODIFY the buffer. Mind that the buffer is only available
+ * until the plist node gets freed. Make a copy if needed.
+ *
+ * @param node The node
+ * @param length If non-NULL, will be set to the length of the string
+ *
+ * @return Pointer to the NULL-terminated buffer.
+ */
+ const char* plist_get_string_ptr(plist_t node, uint64_t* length);
+
+ /**
+ * Get the value of a #PLIST_BOOLEAN node.
+ * This function does nothing if node is not of type #PLIST_BOOLEAN
+ *
+ * @param node the node
+ * @param val a pointer to a uint8_t variable.
+ */
+ void plist_get_bool_val(plist_t node, uint8_t * val);
+
+ /**
+ * Get the unsigned integer value of a #PLIST_INT node.
+ * This function does nothing if node is not of type #PLIST_INT
+ *
+ * @param node the node
+ * @param val a pointer to a uint64_t variable.
+ */
+ void plist_get_uint_val(plist_t node, uint64_t * val);
+
+ /**
+ * Get the signed integer value of a #PLIST_INT node.
+ * This function does nothing if node is not of type #PLIST_INT
+ *
+ * @param node the node
+ * @param val a pointer to a int64_t variable.
+ */
+ void plist_get_int_val(plist_t node, int64_t * val);
+
+ /**
+ * Get the value of a #PLIST_REAL node.
+ * This function does nothing if node is not of type #PLIST_REAL
+ *
+ * @param node the node
+ * @param val a pointer to a double variable.
+ */
+ void plist_get_real_val(plist_t node, double *val);
+
+ /**
+ * Get the value of a #PLIST_DATA node.
+ * This function does nothing if node is not of type #PLIST_DATA
+ *
+ * @param node the node
+ * @param val a pointer to an unallocated char buffer. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @param length the length of the buffer
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ void plist_get_data_val(plist_t node, char **val, uint64_t * length);
+
+ /**
+ * Get a pointer to the data buffer of a #PLIST_DATA node.
+ *
+ * @note DO NOT MODIFY the buffer. Mind that the buffer is only available
+ * until the plist node gets freed. Make a copy if needed.
+ *
+ * @param node The node
+ * @param length Pointer to a uint64_t that will be set to the length of the buffer
+ *
+ * @return Pointer to the buffer
+ */
+ const char* plist_get_data_ptr(plist_t node, uint64_t* length);
+
+ /**
+ * Get the value of a #PLIST_DATE node.
+ * This function does nothing if node is not of type #PLIST_DATE
+ *
+ * @param node the node
+ * @param sec a pointer to an int32_t variable. Represents the number of seconds since 01/01/2001.
+ * @param usec a pointer to an int32_t variable. Represents the number of microseconds
+ */
+ void plist_get_date_val(plist_t node, int32_t * sec, int32_t * usec);
+
+ /**
+ * Get the value of a #PLIST_UID node.
+ * This function does nothing if node is not of type #PLIST_UID
+ *
+ * @param node the node
+ * @param val a pointer to a uint64_t variable.
+ */
+ void plist_get_uid_val(plist_t node, uint64_t * val);
+
+
+ /********************************************
+ * *
+ * Setters *
+ * *
+ ********************************************/
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_KEY
+ *
+ * @param node the node
+ * @param val the key value
+ */
+ void plist_set_key_val(plist_t node, const char *val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_STRING
+ *
+ * @param node the node
+ * @param val the string value. The string is copied when set and will be
+ * freed by the node.
+ */
+ void plist_set_string_val(plist_t node, const char *val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_BOOLEAN
+ *
+ * @param node the node
+ * @param val the boolean value
+ */
+ void plist_set_bool_val(plist_t node, uint8_t val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_INT
+ *
+ * @param node the node
+ * @param val the unsigned integer value
+ */
+ void plist_set_uint_val(plist_t node, uint64_t val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_INT
+ *
+ * @param node the node
+ * @param val the signed integer value
+ */
+ void plist_set_int_val(plist_t node, int64_t val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_REAL
+ *
+ * @param node the node
+ * @param val the real value
+ */
+ void plist_set_real_val(plist_t node, double val);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_DATA
+ *
+ * @param node the node
+ * @param val the binary buffer. The buffer is copied when set and will
+ * be freed by the node.
+ * @param length the length of the buffer
+ */
+ void plist_set_data_val(plist_t node, const char *val, uint64_t length);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_DATE
+ *
+ * @param node the node
+ * @param sec the number of seconds since 01/01/2001
+ * @param usec the number of microseconds
+ */
+ void plist_set_date_val(plist_t node, int32_t sec, int32_t usec);
+
+ /**
+ * Set the value of a node.
+ * Forces type of node to #PLIST_UID
+ *
+ * @param node the node
+ * @param val the unsigned integer value
+ */
+ void plist_set_uid_val(plist_t node, uint64_t val);
+
+
+ /********************************************
+ * *
+ * Import & Export *
+ * *
+ ********************************************/
+
+ /**
+ * Export the #plist_t structure to XML format.
+ *
+ * @param plist the root node to export
+ * @param plist_xml a pointer to a C-string. This function allocates the memory,
+ * caller is responsible for freeing it. Data is UTF-8 encoded.
+ * @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ plist_err_t plist_to_xml(plist_t plist, char **plist_xml, uint32_t * length);
+
+ /**
+ * Export the #plist_t structure to binary format.
+ *
+ * @param plist the root node to export
+ * @param plist_bin a pointer to a char* buffer. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ plist_err_t plist_to_bin(plist_t plist, char **plist_bin, uint32_t * length);
+
+ /**
+ * Export the #plist_t structure to JSON format.
+ *
+ * @param plist the root node to export
+ * @param plist_json a pointer to a char* buffer. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer.
+ * @param prettify pretty print the output if != 0
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ plist_err_t plist_to_json(plist_t plist, char **plist_json, uint32_t* length, int prettify);
+
+ /**
+ * Export the #plist_t structure to OpenStep format.
+ *
+ * @param plist the root node to export
+ * @param plist_openstep a pointer to a char* buffer. This function allocates the memory,
+ * caller is responsible for freeing it.
+ * @param length a pointer to an uint32_t variable. Represents the length of the allocated buffer.
+ * @param prettify pretty print the output if != 0
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ * @note Use plist_mem_free() to free the allocated memory.
+ */
+ plist_err_t plist_to_openstep(plist_t plist, char **plist_openstep, uint32_t* length, int prettify);
+
+
+ /**
+ * Import the #plist_t structure from XML format.
+ *
+ * @param plist_xml a pointer to the xml buffer.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_xml(const char *plist_xml, uint32_t length, plist_t * plist);
+
+ /**
+ * Import the #plist_t structure from binary format.
+ *
+ * @param plist_bin a pointer to the xml buffer.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_bin(const char *plist_bin, uint32_t length, plist_t * plist);
+
+ /**
+ * Import the #plist_t structure from JSON format.
+ *
+ * @param json a pointer to the JSON buffer.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_json(const char *json, uint32_t length, plist_t * plist);
+
+ /**
+ * Import the #plist_t structure from OpenStep plist format.
+ *
+ * @param openstep a pointer to the OpenStep plist buffer.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_openstep(const char *openstep, uint32_t length, plist_t * plist);
+
+ /**
+ * Import the #plist_t structure from memory data.
+ * This method will look at the first bytes of plist_data
+ * to determine if plist_data contains a binary, JSON, or XML plist
+ * and tries to parse the data in the appropriate format.
+ * @note This is just a convenience function and the format detection is
+ * very basic. It checks with plist_is_binary() if the data supposedly
+ * contains binary plist data, if not it checks if the first byte is
+ * either '{' or '[' and assumes JSON format, otherwise it will try
+ * to parse the data as XML.
+ *
+ * @param plist_data a pointer to the memory buffer containing plist data.
+ * @param length length of the buffer to read.
+ * @param plist a pointer to the imported plist.
+ * @return PLIST_ERR_SUCCESS on success or a #plist_err_t on failure
+ */
+ plist_err_t plist_from_memory(const char *plist_data, uint32_t length, plist_t * plist);
+
+ /**
+ * Test if in-memory plist data is in binary format.
+ * This function will look at the first bytes of plist_data to determine
+ * if it supposedly contains a binary plist.
+ * @note The function is not validating the whole memory buffer to check
+ * if the content is truly a plist, it is only using some heuristic on
+ * the first few bytes of plist_data.
+ *
+ * @param plist_data a pointer to the memory buffer containing plist data.
+ * @param length length of the buffer to read.
+ * @return 1 if the buffer is a binary plist, 0 otherwise.
+ */
+ int plist_is_binary(const char *plist_data, uint32_t length);
+
+ /********************************************
+ * *
+ * Utils *
+ * *
+ ********************************************/
+
+ /**
+ * Get a node from its path. Each path element depends on the associated father node type.
+ * For Dictionaries, var args are casted to const char*, for arrays, var args are caster to uint32_t
+ * Search is breath first order.
+ *
+ * @param plist the node to access result from.
+ * @param length length of the path to access
+ * @return the value to access.
+ */
+ plist_t plist_access_path(plist_t plist, uint32_t length, ...);
+
+ /**
+ * Variadic version of #plist_access_path.
+ *
+ * @param plist the node to access result from.
+ * @param length length of the path to access
+ * @param v list of array's index and dic'st key
+ * @return the value to access.
+ */
+ plist_t plist_access_pathv(plist_t plist, uint32_t length, va_list v);
+
+ /**
+ * Compare two node values
+ *
+ * @param node_l left node to compare
+ * @param node_r rigth node to compare
+ * @return TRUE is type and value match, FALSE otherwise.
+ */
+ char plist_compare_node_value(plist_t node_l, plist_t node_r);
+
+ #define _PLIST_IS_TYPE(__plist, __plist_type) (__plist && (plist_get_node_type(__plist) == PLIST_##__plist_type))
+
+ /* Helper macros for the different plist types */
+ #define PLIST_IS_BOOLEAN(__plist) _PLIST_IS_TYPE(__plist, BOOLEAN)
+ #define PLIST_IS_INT(__plist) _PLIST_IS_TYPE(__plist, INT)
+ #define PLIST_IS_REAL(__plist) _PLIST_IS_TYPE(__plist, REAL)
+ #define PLIST_IS_STRING(__plist) _PLIST_IS_TYPE(__plist, STRING)
+ #define PLIST_IS_ARRAY(__plist) _PLIST_IS_TYPE(__plist, ARRAY)
+ #define PLIST_IS_DICT(__plist) _PLIST_IS_TYPE(__plist, DICT)
+ #define PLIST_IS_DATE(__plist) _PLIST_IS_TYPE(__plist, DATE)
+ #define PLIST_IS_DATA(__plist) _PLIST_IS_TYPE(__plist, DATA)
+ #define PLIST_IS_KEY(__plist) _PLIST_IS_TYPE(__plist, KEY)
+ #define PLIST_IS_UID(__plist) _PLIST_IS_TYPE(__plist, UID)
+ /* for backwards compatibility */
+ #define PLIST_IS_UINT PLIST_IS_INT
+
+ /**
+ * Helper function to check the value of a PLIST_BOOL node.
+ *
+ * @param boolnode node of type PLIST_BOOL
+ * @return 1 if the boolean node has a value of TRUE or 0 if FALSE.
+ */
+ int plist_bool_val_is_true(plist_t boolnode);
+
+ /**
+ * Helper function to test if a given #PLIST_INT node's value is negative
+ *
+ * @param intnode node of type PLIST_INT
+ * @return 1 if the node's value is negative, or 0 if positive.
+ */
+ int plist_int_val_is_negative(plist_t intnode);
+
+ /**
+ * Helper function to compare the value of a PLIST_INT node against
+ * a given signed integer value.
+ *
+ * @param uintnode node of type PLIST_INT
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * 1 if the node's value is greater than cmpval,
+ * or -1 if the node's value is less than cmpval.
+ */
+ int plist_int_val_compare(plist_t uintnode, int64_t cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_INT node against
+ * a given unsigned integer value.
+ *
+ * @param uintnode node of type PLIST_INT
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * 1 if the node's value is greater than cmpval,
+ * or -1 if the node's value is less than cmpval.
+ */
+ int plist_uint_val_compare(plist_t uintnode, uint64_t cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_UID node against
+ * a given value.
+ *
+ * @param uidnode node of type PLIST_UID
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * 1 if the node's value is greater than cmpval,
+ * or -1 if the node's value is less than cmpval.
+ */
+ int plist_uid_val_compare(plist_t uidnode, uint64_t cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_REAL node against
+ * a given value.
+ *
+ * @note WARNING: Comparing floating point values can give inaccurate
+ * results because of the nature of floating point values on computer
+ * systems. While this function is designed to be as accurate as
+ * possible, please don't rely on it too much.
+ *
+ * @param realnode node of type PLIST_REAL
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are (almost) equal,
+ * 1 if the node's value is greater than cmpval,
+ * or -1 if the node's value is less than cmpval.
+ */
+ int plist_real_val_compare(plist_t realnode, double cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_DATE node against
+ * a given set of seconds and fraction of a second since epoch.
+ *
+ * @param datenode node of type PLIST_DATE
+ * @param cmpsec number of seconds since epoch to compare against
+ * @param cmpusec fraction of a second in microseconds to compare against
+ * @return 0 if the node's date is equal to the supplied values,
+ * 1 if the node's date is greater than the supplied values,
+ * or -1 if the node's date is less than the supplied values.
+ */
+ int plist_date_val_compare(plist_t datenode, int32_t cmpsec, int32_t cmpusec);
+
+ /**
+ * Helper function to compare the value of a PLIST_STRING node against
+ * a given value.
+ * This function basically behaves like strcmp.
+ *
+ * @param strnode node of type PLIST_STRING
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_string_val_compare(plist_t strnode, const char* cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_STRING node against
+ * a given value, while not comparing more than n characters.
+ * This function basically behaves like strncmp.
+ *
+ * @param strnode node of type PLIST_STRING
+ * @param cmpval value to compare against
+ * @param n maximum number of characters to compare
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_string_val_compare_with_size(plist_t strnode, const char* cmpval, size_t n);
+
+ /**
+ * Helper function to match a given substring in the value of a
+ * PLIST_STRING node.
+ *
+ * @param strnode node of type PLIST_STRING
+ * @param substr value to match
+ * @return 1 if the node's value contains the given substring,
+ * or 0 if not.
+ */
+ int plist_string_val_contains(plist_t strnode, const char* substr);
+
+ /**
+ * Helper function to compare the value of a PLIST_KEY node against
+ * a given value.
+ * This function basically behaves like strcmp.
+ *
+ * @param keynode node of type PLIST_KEY
+ * @param cmpval value to compare against
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_key_val_compare(plist_t keynode, const char* cmpval);
+
+ /**
+ * Helper function to compare the value of a PLIST_KEY node against
+ * a given value, while not comparing more than n characters.
+ * This function basically behaves like strncmp.
+ *
+ * @param keynode node of type PLIST_KEY
+ * @param cmpval value to compare against
+ * @param n maximum number of characters to compare
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_key_val_compare_with_size(plist_t keynode, const char* cmpval, size_t n);
+
+ /**
+ * Helper function to match a given substring in the value of a
+ * PLIST_KEY node.
+ *
+ * @param keynode node of type PLIST_KEY
+ * @param substr value to match
+ * @return 1 if the node's value contains the given substring,
+ * or 0 if not.
+ */
+ int plist_key_val_contains(plist_t keynode, const char* substr);
+
+ /**
+ * Helper function to compare the data of a PLIST_DATA node against
+ * a given blob and size.
+ * This function basically behaves like memcmp after making sure the
+ * size of the node's data value is equal to the size of cmpval (n),
+ * making this a "full match" comparison.
+ *
+ * @param datanode node of type PLIST_DATA
+ * @param cmpval data blob to compare against
+ * @param n size of data blob passed in cmpval
+ * @return 0 if the node's data blob and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_data_val_compare(plist_t datanode, const uint8_t* cmpval, size_t n);
+
+ /**
+ * Helper function to compare the data of a PLIST_DATA node against
+ * a given blob and size, while no more than n bytes are compared.
+ * This function basically behaves like memcmp after making sure the
+ * size of the node's data value is at least n, making this a
+ * "starts with" comparison.
+ *
+ * @param datanode node of type PLIST_DATA
+ * @param cmpval data blob to compare against
+ * @param n size of data blob passed in cmpval
+ * @return 0 if the node's value and cmpval are equal,
+ * > 0 if the node's value is lexicographically greater than cmpval,
+ * or < 0 if the node's value is lexicographically less than cmpval.
+ */
+ int plist_data_val_compare_with_size(plist_t datanode, const uint8_t* cmpval, size_t n);
+
+ /**
+ * Helper function to match a given data blob within the value of a
+ * PLIST_DATA node.
+ *
+ * @param datanode node of type PLIST_KEY
+ * @param cmpval data blob to match
+ * @param n size of data blob passed in cmpval
+ * @return 1 if the node's value contains the given data blob
+ * or 0 if not.
+ */
+ int plist_data_val_contains(plist_t datanode, const uint8_t* cmpval, size_t n);
+
+ /**
+ * Free memory allocated by relevant libplist API calls:
+ * - plist_to_xml()
+ * - plist_to_bin()
+ * - plist_get_key_val()
+ * - plist_get_string_val()
+ * - plist_get_data_val()
+ *
+ * @param ptr pointer to the memory to free
+ *
+ * @note Do not use this function to free plist_t nodes, use plist_free()
+ * instead.
+ */
+ void plist_mem_free(void* ptr);
+
+ /*@}*/
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Info.plist b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Info.plist
new file mode 100644
index 000000000..228311e3f
Binary files /dev/null and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/Info.plist differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/StosJIT b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/StosJIT
new file mode 100755
index 000000000..4b24b750d
Binary files /dev/null and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/StosJIT differ
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/_CodeSignature/CodeResources b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/_CodeSignature/CodeResources
new file mode 100644
index 000000000..8cc62f1fe
--- /dev/null
+++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/_CodeSignature/CodeResources
@@ -0,0 +1,205 @@
+
+
+
+
+ files
+
+ .DS_Store
+
+ 7Mfr8shT4pXWBr/plN+uNkIabdM=
+
+ Headers/StosJIT-Swift.h
+
+ h9vaTwhC6FlnyKmIkaxLQGlFd1g=
+
+ Headers/StosJIT.h
+
+ ggHr5wlLNIIPydwUL9Vxm6abxjo=
+
+ Headers/idevice.h
+
+ mHDz7368FsBID56/epJ2NgIkha4=
+
+ Headers/plist.h
+
+ bL/f0MQDpLfvIcI1zxPwMuJ/PfI=
+
+ Info.plist
+
+ ZTTwPKlta/gjXAr1HIHmyAxeU4E=
+
+ Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo
+
+ nihJghwM5m7kxkQD7UvrWyHkLy8=
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json
+
+ gcwBsH4BgyFY4sVtNt+/xOKS3vY=
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftdoc
+
+ YPtkDrAuBiPPEp4ZdRdBVlFXnRM=
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftmodule
+
+ 9cIInnjJzJFtY+CZm2iNo5qL3MQ=
+
+ Modules/module.modulemap
+
+ cnpvYzvLIwWcxkQodj5uLbHkyRk=
+
+
+ files2
+
+ Headers/StosJIT-Swift.h
+
+ hash2
+
+ 1obIr4IjMvtcyNyYIV/Nh/5wahcA1cFjc4n4XVlNt2I=
+
+
+ Headers/StosJIT.h
+
+ hash2
+
+ yY9KyrRdOYRdlb7G6wVMU2hogasXMjwV5r8jUIk44ok=
+
+
+ Headers/idevice.h
+
+ hash2
+
+ zR9/TB9Dnv3uRC8qqGvaQ6c2yyOFUURmrHKLdEiUh/g=
+
+
+ Headers/plist.h
+
+ hash2
+
+ yFbGsiXBBp91tfsSFtS0Utt2Gpc3MEDFiMVXKG9q1rs=
+
+
+ Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo
+
+ hash2
+
+ +Ehvco7cQbAaF7zufvBYTiGXFp37Hjym/Pav514sGPk=
+
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json
+
+ hash2
+
+ Qnesa0n4URGWAopawg9bGx36dUwkYV00BoCJ8LFzlyg=
+
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftdoc
+
+ hash2
+
+ k7F2Xs2hh9iMbK8IE8TMtN6gjQ9kWs30NUKHeupq6VE=
+
+
+ Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftmodule
+
+ hash2
+
+ gMDYNHcBPCNwZw2A5mEUiCyYAS9VhtQG0z+/WqAUrOQ=
+
+
+ Modules/module.modulemap
+
+ hash2
+
+ FGwGKs5SNvpCyiIWiOP4eml9m2e3KISmtCJVtNnUnUc=
+
+
+
+ rules
+
+ ^.*
+
+ ^.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Base\.lproj/
+
+ weight
+ 1010
+
+ ^version.plist$
+
+
+ rules2
+
+ .*\.dSYM($|/)
+
+ weight
+ 11
+
+ ^(.*/)?\.DS_Store$
+
+ omit
+
+ weight
+ 2000
+
+ ^.*
+
+ ^.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Base\.lproj/
+
+ weight
+ 1010
+
+ ^Info\.plist$
+
+ omit
+
+ weight
+ 20
+
+ ^PkgInfo$
+
+ omit
+
+ weight
+ 20
+
+ ^embedded\.provisionprofile$
+
+ weight
+ 20
+
+ ^version\.plist$
+
+ weight
+ 20
+
+
+
+
diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib
index 4f3386894..8996b7ca5 100755
Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib differ
diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/Info.plist b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/Info.plist
deleted file mode 100644
index d0cb291b6..000000000
--- a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/Info.plist
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- AvailableLibraries
-
-
- BinaryPath
- MoltenVK.framework/MoltenVK
- LibraryIdentifier
- ios-arm64
- LibraryPath
- MoltenVK.framework
- SupportedArchitectures
-
- arm64
-
- SupportedPlatform
- ios
-
-
- CFBundlePackageType
- XFWK
- XCFrameworkFormatVersion
- 1.0
-
-
diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/Info.plist b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/Info.plist
deleted file mode 100644
index 2e0914e03..000000000
Binary files a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/Info.plist and /dev/null differ
diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK
deleted file mode 100755
index 4f3386894..000000000
Binary files a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK and /dev/null differ
diff --git a/src/MeloNX/MeloNX/Info.plist b/src/MeloNX/MeloNX/Info.plist
index bb9bd4980..8c026ae5d 100644
--- a/src/MeloNX/MeloNX/Info.plist
+++ b/src/MeloNX/MeloNX/Info.plist
@@ -38,8 +38,9 @@
UIBackgroundModes
- audio
+ location
processing
+ audio
UIFileSharingEnabled
diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs
index 5a76bb291..63dce4c3d 100644
--- a/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Cache/NoWxCache.cs
@@ -11,7 +11,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
class NoWxCache : IDisposable
{
private const int CodeAlignment = 4; // Bytes.
- private const int SharedCacheSize = 2047 * 1024 * 1024;
+ private const int SharedCacheSize = 512 * 1024 * 1024;
private const int LocalCacheSize = 128 * 1024 * 1024;
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there
diff --git a/src/Ryujinx.Cpu/LightningJit/Cache/WriteZeroCache.cs b/src/Ryujinx.Cpu/LightningJit/Cache/WriteZeroCache.cs
new file mode 100644
index 000000000..212ee2f8b
--- /dev/null
+++ b/src/Ryujinx.Cpu/LightningJit/Cache/WriteZeroCache.cs
@@ -0,0 +1,622 @@
+using ARMeilleure.Memory;
+using Ryujinx.Common;
+using Ryujinx.Memory;
+using System;
+using System.Runtime.InteropServices;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Ryujinx.Cpu.LightningJit.Cache
+{
+ class WriteZeroCache : IDisposable
+ {
+ private const int CodeAlignment = 4;
+ private const int InitialCacheSize = 2 * 1024 * 1024;
+ private const int GrowthCacheSize = 2 * 1024 * 1024;
+ private const int MaxSharedCacheSize = 512 * 1024 * 1024;
+ private const int MaxLocalCacheSize = 128 * 1024 * 1024;
+
+ [DllImport("StosJIT.framework/StosJIT", EntryPoint = "writeZeroToMemory")]
+ public static extern bool WriteZeroToMemory(ulong addr, int length);
+
+ // How many calls to the same function we allow until we pad the shared cache to force the function to become available there
+ // and allow the guest to take the fast path.
+ private const int MinCallsForPad = 8;
+
+ private class MemoryCache : IDisposable
+ {
+ private readonly ReservedRegion _region;
+ private readonly CacheMemoryAllocator _cacheAllocator;
+ public readonly IJitMemoryAllocator Allocator;
+ private readonly ulong _maxSize;
+ private ulong _currentSize;
+
+ private readonly Dictionary> _reusePages;
+ private readonly object _reuselock = new object();
+
+ public CacheMemoryAllocator CacheAllocator => _cacheAllocator;
+ public IntPtr Pointer => _region.Block.Pointer;
+ public ulong CurrentSize => _currentSize;
+ public ulong MaxSize => _maxSize;
+
+ public MemoryCache(IJitMemoryAllocator allocator, ulong maxSize)
+ {
+ Allocator = allocator;
+ _maxSize = maxSize;
+ _currentSize = InitialCacheSize;
+
+
+ _region = new(allocator, maxSize);
+ _cacheAllocator = new((int)maxSize);
+
+ _reusePages = new Dictionary>();
+
+ _region.Block.MapAsRw(0, _currentSize);
+ _region.ExpandIfNeeded(_currentSize);
+
+ WriteZeroToMemory((ulong)_region.Block.Pointer.ToInt64(), (int)_currentSize);
+ }
+
+ public bool TryGetReusablePage(int size, out int offset)
+ {
+ lock (_reuselock)
+ {
+ if (_reusePages.TryGetValue(size, out var exactOffsets) && exactOffsets.Count > 0)
+ {
+ offset = exactOffsets.First();
+ exactOffsets.Remove(offset);
+ return true;
+ }
+
+ var largerSizes = _reusePages.Where(kvp => kvp.Key > size && kvp.Value.Count > 0)
+ .OrderBy(kvp => kvp.Key)
+ .FirstOrDefault();
+
+ if (largerSizes.Value != null && largerSizes.Value.Count > 0)
+ {
+ int largerSize = largerSizes.Key;
+ var largerOffsets = largerSizes.Value;
+
+ offset = largerOffsets.First();
+ largerOffsets.Remove(offset);
+
+ int remainingSize = largerSize - size;
+ if (remainingSize > 0)
+ {
+ AddReusablePage(offset + size, remainingSize);
+ }
+
+ return true;
+ }
+
+ offset = -1;
+ return false;
+ }
+ }
+
+ public void AddReusablePage(int offset, int size)
+ {
+ if (size < (int)MemoryBlock.GetPageSize())
+ {
+ return;
+ }
+
+ lock (_reuselock)
+ {
+ if (!_reusePages.TryGetValue(size, out var offsets))
+ {
+ offsets = new HashSet();
+ _reusePages[size] = offsets;
+ }
+ offsets.Add(offset);
+ }
+ }
+
+ public int Allocate(int codeSize)
+ {
+ codeSize = AlignCodeSize(codeSize);
+
+ if (codeSize >= (int)MemoryBlock.GetPageSize() &&
+ (codeSize % (int)MemoryBlock.GetPageSize() == 0) &&
+ TryGetReusablePage(codeSize, out int reuseOffset))
+ {
+ ReprotectAsRw(reuseOffset, codeSize);
+ return reuseOffset;
+ }
+
+ int allocOffset = _cacheAllocator.Allocate(codeSize);
+
+ if (allocOffset < 0)
+ {
+ throw new OutOfMemoryException("JIT Cache exhausted.");
+ }
+
+
+ ulong requiredSize = (ulong)allocOffset + (ulong)codeSize;
+ if (requiredSize > _currentSize)
+ {
+ ulong neededGrowth = requiredSize - _currentSize;
+ ulong growthIncrements = (neededGrowth + GrowthCacheSize - 1) / GrowthCacheSize;
+ ulong newSize = _currentSize + (growthIncrements * GrowthCacheSize);
+
+ newSize = Math.Min(newSize, _maxSize);
+
+ if (newSize <= _currentSize || requiredSize > newSize)
+ {
+ throw new OutOfMemoryException("JIT Cache exhausted, cannot grow further.");
+ }
+
+ _region.Block.MapAsRw(_currentSize, newSize - _currentSize);
+ _region.ExpandIfNeeded(newSize);
+
+ WriteZeroToMemory((ulong)(_region.Block.Pointer.ToInt64() + (long)_currentSize), (int)(newSize - _currentSize));
+
+ _currentSize = newSize;
+ }
+
+ return allocOffset;
+ }
+
+ public void Free(int offset, int size)
+ {
+ if (size >= (int)MemoryBlock.GetPageSize() && (size % (int)MemoryBlock.GetPageSize() == 0) &&
+ (offset % (int)MemoryBlock.GetPageSize() == 0))
+ {
+ AddReusablePage(offset, size);
+ }
+ else
+ {
+ _cacheAllocator.Free(offset, size);
+ }
+ }
+
+ public void ReprotectAsRw(int offset, int size)
+ {
+ Debug.Assert(offset >= 0 && (offset & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
+ Debug.Assert(size > 0 && (size & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
+
+ _region.Block.MapAsRw((ulong)offset, (ulong)size);
+ }
+
+ public void ReprotectAsRx(int offset, int size)
+ {
+ Debug.Assert(offset >= 0 && (offset & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
+ Debug.Assert(size > 0 && (size & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
+
+ _region.Block.MapAsRx((ulong)offset, (ulong)size);
+
+ if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
+ {
+ JitSupportDarwin.SysIcacheInvalidate(_region.Block.Pointer + offset, size);
+ }
+ else
+ {
+ throw new PlatformNotSupportedException();
+ }
+ }
+
+ public void ClearReusePool()
+ {
+ lock (_reuselock)
+ {
+ _reusePages.Clear();
+ }
+ }
+
+ private static int AlignCodeSize(int codeSize)
+ {
+ return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ ClearReusePool();
+ _region.Dispose();
+ _cacheAllocator.Clear();
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ private readonly IStackWalker _stackWalker;
+ private readonly Translator _translator;
+ private readonly List _sharedCaches;
+ private readonly List _localCaches;
+ private readonly Dictionary _pendingMaps;
+ private readonly object _lock;
+
+ class ThreadLocalCacheEntry
+ {
+ public readonly int Offset;
+ public readonly int Size;
+ public readonly IntPtr FuncPtr;
+ public readonly int CacheIndex;
+ private int _useCount;
+
+ public ThreadLocalCacheEntry(int offset, int size, IntPtr funcPtr, int cacheIndex)
+ {
+ Offset = offset;
+ Size = size;
+ FuncPtr = funcPtr;
+ CacheIndex = cacheIndex;
+ _useCount = 0;
+ }
+
+ public int IncrementUseCount()
+ {
+ return ++_useCount;
+ }
+ }
+
+ [ThreadStatic]
+ private static Dictionary _threadLocalCache;
+
+ public WriteZeroCache(IJitMemoryAllocator allocator, IStackWalker stackWalker, Translator translator)
+ {
+ _stackWalker = stackWalker;
+ _translator = translator;
+ _sharedCaches = new List { new(allocator, MaxSharedCacheSize) };
+ _localCaches = new List { new(allocator, MaxLocalCacheSize) };
+ _pendingMaps = new Dictionary();
+ _lock = new();
+ }
+
+ private PageAlignedRangeList GetPendingMapForCache(int cacheIndex)
+ {
+ ulong cacheKey = (ulong)cacheIndex;
+ if (!_pendingMaps.TryGetValue(cacheKey, out var pendingMap))
+ {
+ pendingMap = new PageAlignedRangeList(
+ (offset, size) => _sharedCaches[cacheIndex].ReprotectAsRx(offset, size),
+ (address, func) => RegisterFunction(address, func));
+ _pendingMaps[cacheKey] = pendingMap;
+ }
+ return pendingMap;
+ }
+
+ private bool HasInAnyPendingMap(ulong guestAddress)
+ {
+ foreach (var pendingMap in _pendingMaps.Values)
+ {
+ if (pendingMap.Has(guestAddress))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private int AllocateInSharedCache(int codeLength)
+ {
+ for (int i = 0; i < _sharedCaches.Count; i++)
+ {
+ try
+ {
+ return (i << 28) | _sharedCaches[i].Allocate(codeLength);
+ }
+ catch (OutOfMemoryException)
+ {
+ // Try next cache
+ }
+ }
+
+ // All existing caches are full, create a new one
+ lock (_lock)
+ {
+ var allocator = _sharedCaches[0].Allocator;
+ _sharedCaches.Add(new(allocator, MaxSharedCacheSize));
+ return (_sharedCaches.Count - 1) << 28 | _sharedCaches[_sharedCaches.Count - 1].Allocate(codeLength);
+ }
+ }
+
+ private int AllocateInLocalCache(int codeLength)
+ {
+ for (int i = 0; i < _localCaches.Count; i++)
+ {
+ try
+ {
+ return (i << 28) | _localCaches[i].Allocate(codeLength);
+ }
+ catch (OutOfMemoryException)
+ {
+ // Try next cache
+ }
+ }
+
+ lock (_lock)
+ {
+ var allocator = _localCaches[0].Allocator;
+ _localCaches.Add(new(allocator, MaxLocalCacheSize));
+ return (_localCaches.Count - 1) << 28 | _localCaches[_localCaches.Count - 1].Allocate(codeLength);
+ }
+ }
+
+ private static (int cacheIndex, int offset) SplitCacheOffset(int combinedOffset)
+ {
+ return (combinedOffset >> 28, combinedOffset & 0xFFFFFFF);
+ }
+
+ public unsafe IntPtr Map(IntPtr framePointer, ReadOnlySpan code, ulong guestAddress, ulong guestSize)
+ {
+ if (TryGetThreadLocalFunction(guestAddress, out IntPtr funcPtr))
+ {
+ return funcPtr;
+ }
+
+ lock (_lock)
+ {
+ if (!HasInAnyPendingMap(guestAddress) && !_translator.Functions.ContainsKey(guestAddress))
+ {
+ int combinedOffset = AllocateInSharedCache(code.Length);
+ var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset);
+
+ MemoryCache cache = _sharedCaches[cacheIndex];
+ funcPtr = cache.Pointer + funcOffset;
+
+ code.CopyTo(new Span((void*)funcPtr, code.Length));
+
+ TranslatedFunction function = new(funcPtr, guestSize);
+
+ GetPendingMapForCache(cacheIndex).Add(funcOffset, code.Length, guestAddress, function);
+ }
+
+ ClearThreadLocalCache(framePointer);
+
+ return AddThreadLocalFunction(code, guestAddress);
+ }
+ }
+
+ public unsafe IntPtr MapPageAligned(ReadOnlySpan code)
+ {
+ lock (_lock)
+ {
+ int cacheIndex;
+ int funcOffset;
+ IntPtr mappedFuncPtr = IntPtr.Zero;
+
+ for (cacheIndex = 0; cacheIndex < _sharedCaches.Count; cacheIndex++)
+ {
+ try
+ {
+ var pendingMap = GetPendingMapForCache(cacheIndex);
+
+ pendingMap.Pad(_sharedCaches[cacheIndex].CacheAllocator);
+
+ int sizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
+ funcOffset = _sharedCaches[cacheIndex].Allocate(sizeAligned);
+
+ Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0);
+
+ IntPtr funcPtr1 = _sharedCaches[cacheIndex].Pointer + funcOffset;
+
+ code.CopyTo(new Span((void*)funcPtr1, code.Length));
+
+ _sharedCaches[cacheIndex].ReprotectAsRx(funcOffset, sizeAligned);
+
+ return funcPtr1;
+ }
+ catch (OutOfMemoryException)
+ {
+ // Try next cache
+ }
+ }
+
+ var allocator = _sharedCaches[0].Allocator;
+ var newCache = new MemoryCache(allocator, MaxSharedCacheSize);
+ _sharedCaches.Add(newCache);
+ cacheIndex = _sharedCaches.Count - 1;
+
+ var newPendingMap = GetPendingMapForCache(cacheIndex);
+
+ newPendingMap.Pad(newCache.CacheAllocator);
+
+ int newSizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
+ funcOffset = newCache.Allocate(newSizeAligned);
+
+ Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0);
+
+ IntPtr funcPtr = newCache.Pointer + funcOffset;
+ code.CopyTo(new Span((void*)funcPtr, code.Length));
+
+ newCache.ReprotectAsRx(funcOffset, newSizeAligned);
+
+ return funcPtr;
+ }
+ }
+
+ private bool TryGetThreadLocalFunction(ulong guestAddress, out IntPtr funcPtr)
+ {
+ if ((_threadLocalCache ??= new()).TryGetValue(guestAddress, out var entry))
+ {
+ if (entry.IncrementUseCount() >= MinCallsForPad)
+ {
+ // Function is being called often, let's make it available in the shared cache so that the guest code
+ // can take the fast path and stop calling the emulator to get the function from the thread local cache.
+ // To do that we pad all "pending" function until they complete a page of memory, allowing us to reprotect them as RX.
+
+ lock (_lock)
+ {
+ foreach (var pendingMap in _pendingMaps.Values)
+ {
+ // Get the cache index from the pendingMap key
+ if (_pendingMaps.FirstOrDefault(x => x.Value == pendingMap).Key is ulong cacheIndex)
+ {
+ // Use the correct shared cache for padding based on the cache index
+ pendingMap.Pad(_sharedCaches[(int)cacheIndex].CacheAllocator);
+ }
+ }
+ }
+ }
+
+ funcPtr = entry.FuncPtr;
+
+ return true;
+ }
+
+ funcPtr = IntPtr.Zero;
+
+ return false;
+ }
+
+ private void ClearThreadLocalCache(IntPtr framePointer)
+ {
+ // Try to delete functions that are already on the shared cache
+ // and no longer being executed.
+
+ if (_threadLocalCache == null)
+ {
+ return;
+ }
+
+ IntPtr[] cachePointers = new IntPtr[_localCaches.Count];
+ int[] cacheSizes = new int[_localCaches.Count];
+
+ for (int i = 0; i < _localCaches.Count; i++)
+ {
+ cachePointers[i] = _localCaches[i].Pointer;
+ cacheSizes[i] = (int)_localCaches[i].CurrentSize;
+ }
+
+ IntPtr[] sharedPointers = new IntPtr[_sharedCaches.Count];
+ int[] sharedSizes = new int[_sharedCaches.Count];
+
+ for (int i = 0; i < _sharedCaches.Count; i++)
+ {
+ sharedPointers[i] = _sharedCaches[i].Pointer;
+ sharedSizes[i] = (int)_sharedCaches[i].CurrentSize;
+ }
+
+ IEnumerable callStack = null;
+ for (int i = 0; i < _localCaches.Count; i++)
+ {
+ callStack = _stackWalker.GetCallStack(
+ framePointer,
+ cachePointers[i],
+ cacheSizes[i],
+ sharedPointers[i],
+ sharedSizes[i]
+ );
+ }
+
+ List<(ulong, ThreadLocalCacheEntry)> toDelete = new();
+
+ foreach ((ulong address, ThreadLocalCacheEntry entry) in _threadLocalCache)
+ {
+ bool canDelete = !HasInAnyPendingMap(address);
+ if (!canDelete)
+ {
+ continue;
+ }
+
+ foreach (ulong funcAddress in callStack)
+ {
+ if (funcAddress >= (ulong)entry.FuncPtr && funcAddress < (ulong)entry.FuncPtr + (ulong)entry.Size)
+ {
+ canDelete = false;
+ break;
+ }
+ }
+
+ if (canDelete)
+ {
+ toDelete.Add((address, entry));
+ }
+ }
+
+ int pageSize = (int)MemoryBlock.GetPageSize();
+
+ foreach ((ulong address, ThreadLocalCacheEntry entry) in toDelete)
+ {
+ _threadLocalCache.Remove(address);
+
+ int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
+ var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
+
+ _localCaches[cacheIndex].Free(offset, sizeAligned);
+ _localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
+ }
+ }
+
+ public void ClearEntireThreadLocalCache()
+ {
+ if (_threadLocalCache == null)
+ {
+ return;
+ }
+
+ int pageSize = (int)MemoryBlock.GetPageSize();
+
+ foreach ((_, ThreadLocalCacheEntry entry) in _threadLocalCache)
+ {
+ int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
+ var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
+
+ _localCaches[cacheIndex].Free(offset, sizeAligned);
+ _localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
+ }
+
+ _threadLocalCache.Clear();
+ _threadLocalCache = null;
+ }
+
+ private unsafe IntPtr AddThreadLocalFunction(ReadOnlySpan code, ulong guestAddress)
+ {
+ int alignedSize = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
+ int combinedOffset = AllocateInLocalCache(alignedSize);
+ var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset);
+
+ Debug.Assert((funcOffset & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
+
+ IntPtr funcPtr = _localCaches[cacheIndex].Pointer + funcOffset;
+ code.CopyTo(new Span((void*)funcPtr, code.Length));
+
+ (_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr, cacheIndex));
+
+ _localCaches[cacheIndex].ReprotectAsRx(funcOffset, alignedSize);
+
+ return funcPtr;
+ }
+
+ private void RegisterFunction(ulong address, TranslatedFunction func)
+ {
+ TranslatedFunction oldFunc = _translator.Functions.GetOrAdd(address, func.GuestSize, func);
+
+ Debug.Assert(oldFunc == func);
+
+ _translator.RegisterFunction(address, func);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ foreach (var cache in _localCaches)
+ {
+ cache.Dispose();
+ }
+
+ foreach (var cache in _sharedCaches)
+ {
+ cache.Dispose();
+ }
+
+ _localCaches.Clear();
+ _sharedCaches.Clear();
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Cpu/LightningJit/Translator.cs b/src/Ryujinx.Cpu/LightningJit/Translator.cs
index bbacecef7..85593036e 100644
--- a/src/Ryujinx.Cpu/LightningJit/Translator.cs
+++ b/src/Ryujinx.Cpu/LightningJit/Translator.cs
@@ -11,6 +11,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
+using System.IO;
namespace Ryujinx.Cpu.LightningJit
{
@@ -40,6 +41,7 @@ namespace Ryujinx.Cpu.LightningJit
private readonly ConcurrentQueue> _oldFuncs;
private readonly NoWxCache _noWxCache;
+ private readonly WriteZeroCache _writeZeroCache;
private bool _disposed;
internal TranslatorCache Functions { get; }
@@ -55,17 +57,44 @@ namespace Ryujinx.Cpu.LightningJit
if (IsNoWxPlatform)
{
- _noWxCache = new(new JitMemoryAllocator(), CreateStackWalker(), this);
+ if (File.Exists("/System/Library/CoreServices/SystemVersion.plist"))
+ {
+ string content = File.ReadAllText("/System/Library/CoreServices/SystemVersion.plist");
+ if (content.Contains("22E5200s") && content.Contains("18.4") && content.Contains("Beta"))
+ {
+ // iOS 18.4db1 (22E5200s) disables traditional JIT (R/X) and needs a debugger to fill to the page to make the executable region a debug map.
+ // Apple has confirmed that this change will be coming to later iOS releases.
+ // Credit to JJTech for figuring out a workaround: https://gist.github.com/JJTech0130/142aee0f7bda9c61a421140d17afbdeb
+ Console.WriteLine($"User is using iOS 18.4db1 (22E5200s), enabling Debugger Memory Writing");
+ _writeZeroCache = new(new JitMemoryAllocator(), CreateStackWalker(), this);
+ Functions = new TranslatorCache();
+ FunctionTable = new AddressTable(for64Bits ? _levels64Bit : _levels32Bit);
+ Stubs = new TranslatorStubs(FunctionTable, _writeZeroCache);
+ }
+ else
+ {
+ _noWxCache = new(new JitMemoryAllocator(), CreateStackWalker(), this);
+ Functions = new TranslatorCache();
+ FunctionTable = new AddressTable(for64Bits ? _levels64Bit : _levels32Bit);
+ Stubs = new TranslatorStubs(FunctionTable, _noWxCache);
+ }
+ }
+ else
+ {
+ _noWxCache = new(new JitMemoryAllocator(), CreateStackWalker(), this);
+ Functions = new TranslatorCache();
+ FunctionTable = new AddressTable(for64Bits ? _levels64Bit : _levels32Bit);
+ Stubs = new TranslatorStubs(FunctionTable, _noWxCache);
+ }
}
else
{
JitCache.Initialize(new JitMemoryAllocator(forJit: true));
+ Functions = new TranslatorCache();
+ FunctionTable = new AddressTable(for64Bits ? _levels64Bit : _levels32Bit);
+ Stubs = new TranslatorStubs(FunctionTable, (NoWxCache)null);
}
- Functions = new TranslatorCache();
- FunctionTable = new AddressTable(for64Bits ? _levels64Bit : _levels32Bit);
- Stubs = new TranslatorStubs(FunctionTable, _noWxCache);
-
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
if (memory.Type.IsHostMappedOrTracked())
@@ -96,6 +125,7 @@ namespace Ryujinx.Cpu.LightningJit
NativeInterface.UnregisterThread();
_noWxCache?.ClearEntireThreadLocalCache();
+ _writeZeroCache?.ClearEntireThreadLocalCache();
}
internal IntPtr GetOrTranslatePointer(IntPtr framePointer, ulong address, ExecutionMode mode)
@@ -103,9 +133,13 @@ namespace Ryujinx.Cpu.LightningJit
if (_noWxCache != null)
{
CompiledFunction func = Compile(address, mode);
-
return _noWxCache.Map(framePointer, func.Code, address, (ulong)func.GuestCodeLength);
}
+ else if (_writeZeroCache != null)
+ {
+ CompiledFunction func = Compile(address, mode);
+ return _writeZeroCache.Map(framePointer, func.Code, address, (ulong)func.GuestCodeLength);
+ }
return GetOrTranslate(address, mode).FuncPointer;
}
@@ -205,6 +239,10 @@ namespace Ryujinx.Cpu.LightningJit
{
_noWxCache.Dispose();
}
+ else if (_writeZeroCache != null)
+ {
+ _writeZeroCache.Dispose();
+ }
else
{
ClearJitCache();
diff --git a/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs b/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs
index 914712bb1..91f3ec4f1 100644
--- a/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs
+++ b/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs
@@ -25,6 +25,8 @@ namespace Ryujinx.Cpu.LightningJit
private readonly AddressTable _functionTable;
private readonly NoWxCache _noWxCache;
+ private readonly WriteZeroCache _writeZeroCache;
+
private readonly GetFunctionAddressDelegate _getFunctionAddressRef;
private readonly IntPtr _getFunctionAddress;
private readonly Lazy _dispatchStub;
@@ -92,6 +94,26 @@ namespace Ryujinx.Cpu.LightningJit
_dispatchLoop = new(GenerateDispatchLoop, isThreadSafe: true);
}
+ ///
+ /// Initializes a new instance of the class with the specified
+ /// instance.
+ ///
+ /// Function table used to store pointers to the functions that the guest code will call
+ /// Cache used on iOS versions that need a debugger to make a debug map
+ /// is null
+ public TranslatorStubs(AddressTable functionTable, WriteZeroCache writeZeroCache)
+ {
+ ArgumentNullException.ThrowIfNull(functionTable);
+
+ _functionTable = functionTable;
+ _writeZeroCache = writeZeroCache;
+ _getFunctionAddressRef = NativeInterface.GetFunctionAddress;
+ _getFunctionAddress = Marshal.GetFunctionPointerForDelegate(_getFunctionAddressRef);
+ _slowDispatchStub = new(GenerateSlowDispatchStub, isThreadSafe: true);
+ _dispatchStub = new(GenerateDispatchStub, isThreadSafe: true);
+ _dispatchLoop = new(GenerateDispatchLoop, isThreadSafe: true);
+ }
+
///
/// Releases all resources used by the instance.
///
@@ -361,6 +383,10 @@ namespace Ryujinx.Cpu.LightningJit
{
return _noWxCache.MapPageAligned(code);
}
+ else if (_writeZeroCache != null)
+ {
+ return _writeZeroCache.MapPageAligned(code);
+ }
else
{
return JitCache.Map(code);
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
index a1e6db971..82489673f 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
@@ -159,7 +159,14 @@ namespace Ryujinx.Graphics.GAL.Multithreading
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RunCommand(Span memory, ThreadedRenderer threaded, IRenderer renderer)
{
- _lookup[memory[^1]](memory, threaded, renderer);
+ try
+ {
+ _lookup[memory[^1]](memory, threaded, renderer);
+ }
+ catch (Exception ex)
+ {
+ // I have no idea what i'm doing, doing this to see if i can avoid MoltenVK crashes in games.
+ }
}
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs
index 7523913ec..6610a4f0e 100644
--- a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs
+++ b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs
@@ -106,6 +106,8 @@ namespace Ryujinx.Graphics.Vulkan
var externalMemoryBuffer = new ExternalMemoryBufferCreateInfo
{
SType = StructureType.ExternalMemoryBufferCreateInfo,
+ // For MoltenVK 1.3
+ // ExternalMemoryHandleTypeFlagsExt.MtlBufferBitExt
HandleTypes = ExternalMemoryHandleTypeFlags.HostAllocationBitExt,
};
@@ -676,4 +678,10 @@ namespace Ryujinx.Graphics.Vulkan
Dispose(true);
}
}
+
+ public static class ExternalMemoryHandleTypeFlagsExt
+ {
+ public const ExternalMemoryHandleTypeFlags MtlBufferBitExt = (ExternalMemoryHandleTypeFlags)0x00010000;
+ public const ExternalMemoryHandleTypeFlags MtlHeapBitExt = (ExternalMemoryHandleTypeFlags)0x00040000;
+ }
}
diff --git a/src/Ryujinx.Graphics.Vulkan/Constants.cs b/src/Ryujinx.Graphics.Vulkan/Constants.cs
index 20ce65818..8103e2de8 100644
--- a/src/Ryujinx.Graphics.Vulkan/Constants.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Constants.cs
@@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Vulkan
public const int MaxShaderStages = 5;
public const int MaxUniformBuffersPerStage = 18;
public const int MaxStorageBuffersPerStage = 16;
- public const int MaxTexturesPerStage = 64;
+ public const int MaxTexturesPerStage = 31;
public const int MaxImagesPerStage = 16;
public const int MaxUniformBufferBindings = MaxUniformBuffersPerStage * MaxShaderStages;
public const int MaxStorageBufferBindings = MaxStorageBuffersPerStage * MaxShaderStages;
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs
index 8d7faa653..4872d9b60 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetCollection.cs
@@ -47,25 +47,25 @@ namespace Ryujinx.Graphics.Vulkan
}
}
+
public unsafe void UpdateBuffers(int setIndex, int baseBinding, ReadOnlySpan bufferInfo, DescriptorType type)
{
-
-
- // Proceed if all checks pass
- fixed (DescriptorBufferInfo* pBufferInfo = bufferInfo)
+ for (int i = 0; i < bufferInfo.Length; i++)
{
- var writeDescriptorSet = new WriteDescriptorSet
+ fixed (DescriptorBufferInfo* pBufferInfo = &bufferInfo[i])
{
- SType = StructureType.WriteDescriptorSet,
- DstSet = _descriptorSets[setIndex],
- DstBinding = (uint)baseBinding,
- DescriptorType = type,
- DescriptorCount = (uint)bufferInfo.Length,
- PBufferInfo = pBufferInfo
- };
+ var writeDescriptorSet = new WriteDescriptorSet
+ {
+ SType = StructureType.WriteDescriptorSet,
+ DstSet = _descriptorSets[setIndex],
+ DstBinding = (uint)(baseBinding + i),
+ DescriptorType = type,
+ DescriptorCount = 1,
+ PBufferInfo = pBufferInfo
+ };
- // Update descriptor sets
- _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null);
+ _holder.Api.UpdateDescriptorSets(_holder.Device, 1, writeDescriptorSet, 0, null);
+ }
}
}
@@ -73,7 +73,7 @@ namespace Ryujinx.Graphics.Vulkan
{
if (imageInfo.ImageView.Handle != 0UL)
{
-
+
var writeDescriptorSet = new WriteDescriptorSet
{
SType = StructureType.WriteDescriptorSet,
@@ -90,19 +90,22 @@ namespace Ryujinx.Graphics.Vulkan
public unsafe void UpdateImages(int setIndex, int baseBinding, ReadOnlySpan imageInfo, DescriptorType type)
{
- fixed (DescriptorImageInfo* pImageInfo = imageInfo)
+ for (int i = 0; i < imageInfo.Length; i++)
{
- var writeDescriptorSet = new WriteDescriptorSet
+ fixed (DescriptorImageInfo* pImageInfo = &imageInfo[i])
{
- SType = StructureType.WriteDescriptorSet,
- DstSet = _descriptorSets[setIndex],
- DstBinding = (uint)baseBinding,
- DescriptorType = type,
- DescriptorCount = (uint)imageInfo.Length,
- PImageInfo = pImageInfo,
- };
+ var writeDescriptorSet = new WriteDescriptorSet
+ {
+ SType = StructureType.WriteDescriptorSet,
+ DstSet = _descriptorSets[setIndex],
+ DstBinding = (uint)(baseBinding + i),
+ DescriptorType = type,
+ DescriptorCount = 1,
+ PImageInfo = pImageInfo,
+ };
- _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null);
+ _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null);
+ }
}
}
@@ -112,7 +115,7 @@ namespace Ryujinx.Graphics.Vulkan
{
return;
}
-
+
fixed (DescriptorImageInfo* pImageInfo = imageInfo)
{
for (int i = 0; i < imageInfo.Length; i++)
@@ -120,33 +123,24 @@ namespace Ryujinx.Graphics.Vulkan
bool nonNull = imageInfo[i].ImageView.Handle != 0 && imageInfo[i].Sampler.Handle != 0;
if (nonNull)
{
- int count = 1;
-
- while (i + count < imageInfo.Length &&
- imageInfo[i + count].ImageView.Handle != 0 &&
- imageInfo[i + count].Sampler.Handle != 0)
- {
- count++;
- }
-
var writeDescriptorSet = new WriteDescriptorSet
{
SType = StructureType.WriteDescriptorSet,
DstSet = _descriptorSets[setIndex],
DstBinding = (uint)(baseBinding + i),
- DescriptorType = DescriptorType.CombinedImageSampler,
+ DescriptorType = type,
DescriptorCount = 1,
- PImageInfo = pImageInfo,
+ PImageInfo = pImageInfo + i,
};
- _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null);
- i += count - 1;
+ _holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null);
}
}
}
}
+
public unsafe void UpdateBufferImage(int setIndex, int bindingIndex, BufferView texelBufferView, DescriptorType type)
{
if (texelBufferView.Handle != 0UL)
@@ -174,31 +168,22 @@ namespace Ryujinx.Graphics.Vulkan
fixed (BufferView* pTexelBufferView = texelBufferView)
{
- for (uint i = 0; i < texelBufferView.Length;)
+ for (int i = 0; i < texelBufferView.Length; i++)
{
- uint count = 1;
-
- if (texelBufferView[(int)i].Handle != 0UL)
+ if (texelBufferView[i].Handle != 0UL)
{
- while (i + count < texelBufferView.Length && texelBufferView[(int)(i + count)].Handle != 0UL)
- {
- count++;
- }
-
var writeDescriptorSet = new WriteDescriptorSet
{
SType = StructureType.WriteDescriptorSet,
DstSet = _descriptorSets[setIndex],
- DstBinding = (uint)baseBinding + i,
+ DstBinding = (uint)baseBinding + (uint)i,
DescriptorType = type,
- DescriptorCount = 1,
+ DescriptorCount = 1,
PTexelBufferView = pTexelBufferView + i,
};
_holder.Api.UpdateDescriptorSets(_holder.Device, 1, in writeDescriptorSet, 0, null);
}
-
- i += count;
}
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs
index 117f79bb4..224f80b53 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetTemplate.cs
@@ -31,9 +31,16 @@ namespace Ryujinx.Graphics.Vulkan
_gd = gd;
_device = device;
- // Create a template from the set usages. Assumes the descriptor set is updated in segment order then binding order.
+ // Calculate total number of individual descriptors
+ int totalDescriptors = 0;
+ for (int seg = 0; seg < segments.Length; seg++)
+ {
+ totalDescriptors += segments[seg].Count;
+ }
- DescriptorUpdateTemplateEntry* entries = stackalloc DescriptorUpdateTemplateEntry[segments.Length];
+
+ DescriptorUpdateTemplateEntry* entries = stackalloc DescriptorUpdateTemplateEntry[totalDescriptors];
+ int entryIndex = 0;
nuint structureOffset = 0;
for (int seg = 0; seg < segments.Length; seg++)
@@ -42,45 +49,36 @@ namespace Ryujinx.Graphics.Vulkan
int binding = segment.Binding;
int count = segment.Count;
+ DescriptorType descriptorType = segment.Type.Convert();
- if (IsBufferType(segment.Type))
+ // Create separate entries for each descriptor in this segment
+ for (int i = 0; i < count; i++)
{
- entries[seg] = new DescriptorUpdateTemplateEntry()
+ nuint stride;
+ if (IsBufferType(segment.Type))
{
- DescriptorType = segment.Type.Convert(),
- DstBinding = (uint)binding,
- DescriptorCount = (uint)count,
+ stride = (nuint)Unsafe.SizeOf();
+ }
+ else if (IsBufferTextureType(segment.Type))
+ {
+ stride = (nuint)Unsafe.SizeOf();
+ }
+ else
+ {
+ stride = (nuint)Unsafe.SizeOf();
+ }
+
+ entries[entryIndex] = new DescriptorUpdateTemplateEntry()
+ {
+ DescriptorType = descriptorType,
+ DstBinding = (uint)(binding + i),
+ DescriptorCount = 1, // Always 1 descriptor per entry
Offset = structureOffset,
- Stride = (nuint)Unsafe.SizeOf()
+ Stride = stride
};
- structureOffset += (nuint)(Unsafe.SizeOf() * count);
- }
- else if (IsBufferTextureType(segment.Type))
- {
- entries[seg] = new DescriptorUpdateTemplateEntry()
- {
- DescriptorType = segment.Type.Convert(),
- DstBinding = (uint)binding,
- DescriptorCount = (uint)count,
- Offset = structureOffset,
- Stride = (nuint)Unsafe.SizeOf()
- };
-
- structureOffset += (nuint)(Unsafe.SizeOf() * count);
- }
- else
- {
- entries[seg] = new DescriptorUpdateTemplateEntry()
- {
- DescriptorType = segment.Type.Convert(),
- DstBinding = (uint)binding,
- DescriptorCount = (uint)count,
- Offset = structureOffset,
- Stride = (nuint)Unsafe.SizeOf()
- };
-
- structureOffset += (nuint)(Unsafe.SizeOf() * count);
+ structureOffset += stride;
+ entryIndex++;
}
}
@@ -89,7 +87,7 @@ namespace Ryujinx.Graphics.Vulkan
var info = new DescriptorUpdateTemplateCreateInfo()
{
SType = StructureType.DescriptorUpdateTemplateCreateInfo,
- DescriptorUpdateEntryCount = (uint)segments.Length,
+ DescriptorUpdateEntryCount = (uint)totalDescriptors,
PDescriptorUpdateEntries = entries,
TemplateType = DescriptorUpdateTemplateType.DescriptorSet,
@@ -124,23 +122,6 @@ namespace Ryujinx.Graphics.Vulkan
int entry = 0;
nuint structureOffset = 0;
- void AddBinding(int binding, int count)
- {
- entries[entry++] = new DescriptorUpdateTemplateEntry()
- {
- DescriptorType = DescriptorType.UniformBuffer,
- DstBinding = (uint)binding,
- DescriptorCount = (uint)count,
- Offset = structureOffset,
- Stride = (nuint)Unsafe.SizeOf()
- };
-
- structureOffset += (nuint)(Unsafe.SizeOf() * count);
- }
-
- int startBinding = 0;
- int bindingCount = 0;
-
foreach (ResourceDescriptor descriptor in descriptors.Descriptors)
{
for (int i = 0; i < descriptor.Count; i++)
@@ -149,28 +130,21 @@ namespace Ryujinx.Graphics.Vulkan
if ((updateMask & (1L << binding)) != 0)
{
- if (bindingCount > 0 && (RenderdocPushCountBug || startBinding + bindingCount != binding))
+ entries[entry] = new DescriptorUpdateTemplateEntry()
{
- AddBinding(startBinding, bindingCount);
+ DescriptorType = DescriptorType.UniformBuffer,
+ DstBinding = (uint)binding,
+ DescriptorCount = 1, // Always 1 descriptor per entry
+ Offset = structureOffset,
+ Stride = (nuint)Unsafe.SizeOf()
+ };
- bindingCount = 0;
- }
-
- if (bindingCount == 0)
- {
- startBinding = binding;
- }
-
- bindingCount++;
+ structureOffset += (nuint)Unsafe.SizeOf();
+ entry++;
}
}
}
- if (bindingCount > 0)
- {
- AddBinding(startBinding, bindingCount);
- }
-
Size = (int)structureOffset;
var info = new DescriptorUpdateTemplateCreateInfo()
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
index f47b1c87d..e7fb5f1f7 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
@@ -3,10 +3,10 @@ using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
using Silk.NET.Vulkan;
using System;
-using System.Buffers;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using Buffer = Silk.NET.Vulkan.Buffer;
using CompareOp = Ryujinx.Graphics.GAL.CompareOp;
using Format = Ryujinx.Graphics.GAL.Format;
using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo;
@@ -141,11 +141,11 @@ namespace Ryujinx.Graphics.Vulkan
_bufferTextureRefs = new TextureBuffer[Constants.MaxTextureBindings * 2];
_bufferImageRefs = new TextureBuffer[Constants.MaxImageBindings * 2];
- _textureArrayRefs = Array.Empty>();
- _imageArrayRefs = Array.Empty>();
+ _textureArrayRefs = [];
+ _imageArrayRefs = [];
- _textureArrayExtraRefs = Array.Empty>();
- _imageArrayExtraRefs = Array.Empty>();
+ _textureArrayExtraRefs = [];
+ _imageArrayExtraRefs = [];
_uniformBuffers = new DescriptorBufferInfo[Constants.MaxUniformBufferBindings];
_storageBuffers = new DescriptorBufferInfo[Constants.MaxStorageBufferBindings];
@@ -156,7 +156,7 @@ namespace Ryujinx.Graphics.Vulkan
_uniformSetPd = new int[Constants.MaxUniformBufferBindings];
- var initialImageInfo = new DescriptorImageInfo
+ DescriptorImageInfo initialImageInfo = new()
{
ImageLayout = ImageLayout.General,
};
@@ -217,7 +217,7 @@ namespace Ryujinx.Graphics.Vulkan
if (isMainPipeline)
{
- FeedbackLoopHazards = new();
+ FeedbackLoopHazards = [];
}
}
@@ -235,7 +235,7 @@ namespace Ryujinx.Graphics.Vulkan
// Check stage bindings
- _uniformMirrored.Union(_uniformSet).SignalSet((int binding, int count) =>
+ _uniformMirrored.Union(_uniformSet).SignalSet((binding, count) =>
{
for (int i = 0; i < count; i++)
{
@@ -257,7 +257,7 @@ namespace Ryujinx.Graphics.Vulkan
}
});
- _storageMirrored.Union(_storageSet).SignalSet((int binding, int count) =>
+ _storageMirrored.Union(_storageSet).SignalSet((binding, count) =>
{
for (int i = 0; i < count; i++)
{
@@ -301,13 +301,13 @@ namespace Ryujinx.Graphics.Vulkan
{
for (int i = 0; i < segment.Count; i++)
{
- ref var texture = ref _textureRefs[segment.Binding + i];
+ ref TextureRef texture = ref _textureRefs[segment.Binding + i];
texture.View?.PrepareForUsage(cbs, texture.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards);
}
}
else
{
- ref var arrayRef = ref _textureArrayRefs[segment.Binding];
+ ref ArrayRef arrayRef = ref _textureArrayRefs[segment.Binding];
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
}
@@ -322,13 +322,13 @@ namespace Ryujinx.Graphics.Vulkan
{
for (int i = 0; i < segment.Count; i++)
{
- ref var image = ref _imageRefs[segment.Binding + i];
+ ref ImageRef image = ref _imageRefs[segment.Binding + i];
image.View?.PrepareForUsage(cbs, image.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards);
}
}
else
{
- ref var arrayRef = ref _imageArrayRefs[segment.Binding];
+ ref ArrayRef arrayRef = ref _imageArrayRefs[segment.Binding];
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
}
@@ -337,7 +337,7 @@ namespace Ryujinx.Graphics.Vulkan
for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < _program.BindingSegments.Length; setIndex++)
{
- var bindingSegments = _program.BindingSegments[setIndex];
+ ResourceBindingSegment[] bindingSegments = _program.BindingSegments[setIndex];
if (bindingSegments.Length == 0)
{
@@ -348,18 +348,18 @@ namespace Ryujinx.Graphics.Vulkan
if (segment.IsArray)
{
- if (segment.Type == ResourceType.Texture ||
- segment.Type == ResourceType.Sampler ||
- segment.Type == ResourceType.TextureAndSampler ||
- segment.Type == ResourceType.BufferTexture)
+ if (segment.Type is ResourceType.Texture or
+ ResourceType.Sampler or
+ ResourceType.TextureAndSampler or
+ ResourceType.BufferTexture)
{
- ref var arrayRef = ref _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
+ ref ArrayRef arrayRef = ref _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
}
- else if (segment.Type == ResourceType.Image || segment.Type == ResourceType.BufferImage)
+ else if (segment.Type is ResourceType.Image or ResourceType.BufferImage)
{
- ref var arrayRef = ref _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
+ ref ArrayRef arrayRef = ref _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
}
@@ -424,8 +424,8 @@ namespace Ryujinx.Graphics.Vulkan
{
for (int i = 0; i < buffers.Length; i++)
{
- var assignment = buffers[i];
- var buffer = assignment.Range;
+ BufferAssignment assignment = buffers[i];
+ BufferRange buffer = assignment.Range;
int index = assignment.Binding;
Auto vkBuffer = buffer.Handle == BufferHandle.Null
@@ -440,7 +440,7 @@ namespace Ryujinx.Graphics.Vulkan
Range = (ulong)buffer.Size,
};
- var newRef = new BufferRef(vkBuffer, ref buffer);
+ BufferRef newRef = new(vkBuffer, ref buffer);
ref DescriptorBufferInfo currentInfo = ref _storageBuffers[index];
@@ -460,7 +460,7 @@ namespace Ryujinx.Graphics.Vulkan
{
for (int i = 0; i < buffers.Length; i++)
{
- var vkBuffer = buffers[i];
+ Auto vkBuffer = buffers[i];
int index = first + i;
ref BufferRef currentBufferRef = ref _storageBufferRefs[index];
@@ -633,8 +633,8 @@ namespace Ryujinx.Graphics.Vulkan
{
for (int i = 0; i < buffers.Length; i++)
{
- var assignment = buffers[i];
- var buffer = assignment.Range;
+ BufferAssignment assignment = buffers[i];
+ BufferRange buffer = assignment.Range;
int index = assignment.Binding;
Auto vkBuffer = buffer.Handle == BufferHandle.Null
@@ -678,7 +678,7 @@ namespace Ryujinx.Graphics.Vulkan
return;
}
- var program = _program;
+ ShaderCollection program = _program;
if (_dirty.HasFlag(DirtyFlags.Uniform))
{
@@ -699,13 +699,27 @@ namespace Ryujinx.Graphics.Vulkan
if (_dirty.HasFlag(DirtyFlags.Texture))
{
- if (program.UpdateTexturesWithoutTemplate)
+ if (true)
{
- UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp);
+ try
+ {
+ UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp);
+ }
+ catch (Exception e)
+ {
+ UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp);
+ }
}
else
{
- UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp);
+ try
+ {
+ UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp);
+ }
+ catch (Exception e)
+ {
+ UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp);
+ }
}
}
@@ -750,7 +764,7 @@ namespace Ryujinx.Graphics.Vulkan
if (info.Buffer.Handle == 0)
{
info.Buffer = dummyBuffer?.Get(cbs).Value ?? default;
- info.Offset = 0;
+ // info.Offset = 0;
info.Range = Vk.WholeSize;
}
@@ -760,14 +774,14 @@ namespace Ryujinx.Graphics.Vulkan
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateAndBind(CommandBufferScoped cbs, ShaderCollection program, int setIndex, PipelineBindPoint pbp)
{
- var bindingSegments = program.BindingSegments[setIndex];
+ ResourceBindingSegment[] bindingSegments = program.BindingSegments[setIndex];
if (bindingSegments.Length == 0)
{
return;
}
- var dummyBuffer = _dummyBuffer?.GetBuffer();
+ Auto dummyBuffer = _dummyBuffer?.GetBuffer();
if (_updateDescriptorCacheCbIndex)
{
@@ -775,7 +789,7 @@ namespace Ryujinx.Graphics.Vulkan
program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex);
}
- var dsc = program.GetNewDescriptorSetCollection(setIndex, out var isNew).Get(cbs);
+ DescriptorSetCollection dsc = program.GetNewDescriptorSetCollection(setIndex, out bool isNew).Get(cbs);
if (!program.HasMinimalLayout)
{
@@ -785,6 +799,13 @@ namespace Ryujinx.Graphics.Vulkan
}
}
+ var dummyImageInfo = new DescriptorImageInfo
+ {
+ ImageView = _dummyTexture.GetImageView().Get(cbs).Value,
+ Sampler = _dummySampler.GetSampler().Get(cbs).Value,
+ ImageLayout = ImageLayout.General
+ };
+
DescriptorSetTemplate template = program.Templates[setIndex];
DescriptorSetTemplateWriter tu = _templateUpdater.Begin(template);
@@ -824,7 +845,7 @@ namespace Ryujinx.Graphics.Vulkan
if (_storageSet.Set(index))
{
- ref var info = ref _storageBuffers[index];
+ ref DescriptorBufferInfo info = ref _storageBuffers[index];
bool mirrored = UpdateBuffer(cbs,
ref info,
@@ -850,20 +871,20 @@ namespace Ryujinx.Graphics.Vulkan
for (int i = 0; i < count; i++)
{
- ref var texture = ref textures[i];
- ref var refs = ref _textureRefs[binding + i];
+ ref DescriptorImageInfo texture = ref textures[i];
+ ref TextureRef refs = ref _textureRefs[binding + i];
texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default;
texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default;
if (texture.ImageView.Handle == 0)
{
- texture.ImageView = _dummyTexture.GetImageView().Get(cbs).Value;
+ texture.ImageView = dummyImageInfo.ImageView;
}
if (texture.Sampler.Handle == 0)
{
- texture.Sampler = _dummySampler.GetSampler().Get(cbs).Value;
+ texture.Sampler = dummyImageInfo.Sampler;
}
}
@@ -903,7 +924,7 @@ namespace Ryujinx.Graphics.Vulkan
for (int i = 0; i < count; i++)
{
- images[i].ImageView = _imageRefs[binding + i].ImageView?.Get(cbs).Value ?? default;
+ images[i].ImageView = _imageRefs[binding + i].ImageView?.Get(cbs).Value ?? dummyImageInfo.ImageView;
}
tu.Push(images[..count]);
@@ -934,12 +955,13 @@ namespace Ryujinx.Graphics.Vulkan
}
}
- var sets = dsc.GetSets();
+ DescriptorSet[] sets = dsc.GetSets();
_templateUpdater.Commit(_gd, _device, sets[0]);
_gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan.Empty);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateAndBindTexturesWithoutTemplate(CommandBufferScoped cbs, ShaderCollection program, PipelineBindPoint pbp)
{
int setIndex = PipelineBase.TextureSetIndex;
@@ -950,11 +972,12 @@ namespace Ryujinx.Graphics.Vulkan
return;
}
- if (_updateDescriptorCacheCbIndex)
+ var dummyImageInfo = new DescriptorImageInfo
{
- _updateDescriptorCacheCbIndex = false;
- program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex);
- }
+ ImageView = _dummyTexture.GetImageView().Get(cbs).Value,
+ Sampler = _dummySampler.GetSampler().Get(cbs).Value,
+ ImageLayout = ImageLayout.General
+ };
var dsc = program.GetNewDescriptorSetCollection(setIndex, out _).Get(cbs);
@@ -967,71 +990,78 @@ namespace Ryujinx.Graphics.Vulkan
{
if (segment.Type != ResourceType.BufferTexture)
{
- Span textures = _textures;
-
for (int i = 0; i < count; i++)
{
- ref var texture = ref textures[i];
- ref var refs = ref _textureRefs[binding + i];
+ int index = binding + i;
+ ref var textureRef = ref _textureRefs[index];
- texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default;
- texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default;
+ var imageView = textureRef.ImageView?.Get(cbs).Value ?? dummyImageInfo.ImageView;
+ var sampler = textureRef.Sampler?.Get(cbs).Value ?? dummyImageInfo.Sampler;
- if (texture.ImageView.Handle == 0)
+ var imageInfo = new DescriptorImageInfo
{
- texture.ImageView = _dummyTexture.GetImageView().Get(cbs).Value;
- }
+ ImageView = imageView.Handle != 0 ? imageView : dummyImageInfo.ImageView,
+ Sampler = sampler.Handle != 0 ? sampler : dummyImageInfo.Sampler,
+ ImageLayout = ImageLayout.General
+ };
- if (texture.Sampler.Handle == 0)
- {
- texture.Sampler = _dummySampler.GetSampler().Get(cbs).Value;
- }
-
- if (OperatingSystem.IsIOS()) {
- Span singleTexture = textures.Slice(i, 1);
- dsc.UpdateImages(0, binding + i, singleTexture, DescriptorType.CombinedImageSampler);
- }
- }
- if (!OperatingSystem.IsIOS()) {
- dsc.UpdateImages(0, binding, textures[..count], DescriptorType.CombinedImageSampler);
+ dsc.UpdateImages(0, index, new[] { imageInfo }, DescriptorType.CombinedImageSampler);
}
}
else
{
- Span bufferTextures = _bufferTextures;
-
for (int i = 0; i < count; i++)
{
- bufferTextures[i] = _bufferTextureRefs[binding + i]?.GetBufferView(cbs, false) ?? default;
+ int index = binding + i;
+ var bufferView = _bufferTextureRefs[index]?.GetBufferView(cbs, false) ?? default;
+ dsc.UpdateBufferImages(0, index, new[] { bufferView }, DescriptorType.UniformTexelBuffer);
}
-
- dsc.UpdateBufferImages(0, binding, bufferTextures[..count], DescriptorType.UniformTexelBuffer);
}
}
else
{
+ var arrayRef = _textureArrayRefs[binding];
+
if (segment.Type != ResourceType.BufferTexture)
{
- // Span images = _imageRefs.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler);
-
- if (OperatingSystem.IsIOS())
+ var imageInfos = arrayRef.Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler);
+ if (imageInfos != null)
{
- dsc.UpdateImages(0, binding, _textureArrayRefs[binding].Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler), DescriptorType.CombinedImageSampler);
+ for (int i = 0; i < imageInfos.Length && i < count; i++)
+ {
+ dsc.UpdateImages(0, binding + i, new[] { imageInfos[i] }, DescriptorType.CombinedImageSampler);
+ }
}
else
{
- dsc.UpdateImages(0, binding, _textureArrayRefs[binding].Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler), DescriptorType.CombinedImageSampler);
+ for (int i = 0; i < count; i++)
+ {
+ dsc.UpdateImages(0, binding + i, new[] { dummyImageInfo }, DescriptorType.CombinedImageSampler);
+ }
}
}
else
{
- dsc.UpdateBufferImages(0, binding, _textureArrayRefs[binding].Array.GetBufferViews(cbs), DescriptorType.UniformTexelBuffer);
+ var bufferViews = arrayRef.Array.GetBufferViews(cbs);
+ if (bufferViews != null)
+ {
+ for (int i = 0; i < bufferViews.Length && i < count; i++)
+ {
+ dsc.UpdateBufferImages(0, binding + i, new[] { bufferViews[i] }, DescriptorType.UniformTexelBuffer);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < count; i++)
+ {
+ dsc.UpdateBufferImages(0, binding + i, new[] { default(BufferView) }, DescriptorType.UniformTexelBuffer);
+ }
+ }
}
}
}
var sets = dsc.GetSets();
-
_gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan.Empty);
}
@@ -1039,8 +1069,8 @@ namespace Ryujinx.Graphics.Vulkan
private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs)
{
int sequence = _pdSequence;
- var bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex];
- var dummyBuffer = _dummyBuffer?.GetBuffer();
+ ResourceBindingSegment[] bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex];
+ Auto dummyBuffer = _dummyBuffer?.GetBuffer();
long updatedBindings = 0;
DescriptorSetTemplateWriter writer = _templateUpdater.Begin(32 * Unsafe.SizeOf());
@@ -1087,12 +1117,12 @@ namespace Ryujinx.Graphics.Vulkan
private void Initialize(CommandBufferScoped cbs, int setIndex, DescriptorSetCollection dsc)
{
// We don't support clearing texture descriptors currently.
- if (setIndex != PipelineBase.UniformSetIndex && setIndex != PipelineBase.StorageSetIndex)
+ if (setIndex is not PipelineBase.UniformSetIndex and not PipelineBase.StorageSetIndex)
{
return;
}
- var dummyBuffer = _dummyBuffer?.GetBuffer().Get(cbs).Value ?? default;
+ Buffer dummyBuffer = _dummyBuffer?.GetBuffer().Get(cbs).Value ?? default;
foreach (ResourceBindingSegment segment in _program.ClearSegments[setIndex])
{
@@ -1104,7 +1134,7 @@ namespace Ryujinx.Graphics.Vulkan
{
for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < program.BindingSegments.Length; setIndex++)
{
- var bindingSegments = program.BindingSegments[setIndex];
+ ResourceBindingSegment[] bindingSegments = program.BindingSegments[setIndex];
if (bindingSegments.Length == 0)
{
@@ -1117,10 +1147,10 @@ namespace Ryujinx.Graphics.Vulkan
{
DescriptorSet[] sets = null;
- if (segment.Type == ResourceType.Texture ||
- segment.Type == ResourceType.Sampler ||
- segment.Type == ResourceType.TextureAndSampler ||
- segment.Type == ResourceType.BufferTexture)
+ if (segment.Type is ResourceType.Texture or
+ ResourceType.Sampler or
+ ResourceType.TextureAndSampler or
+ ResourceType.BufferTexture)
{
sets = _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets(
_device,
@@ -1131,7 +1161,7 @@ namespace Ryujinx.Graphics.Vulkan
_dummyTexture,
_dummySampler);
}
- else if (segment.Type == ResourceType.Image || segment.Type == ResourceType.BufferImage)
+ else if (segment.Type is ResourceType.Image or ResourceType.BufferImage)
{
sets = _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets(
_device,
@@ -1202,4 +1232,4 @@ namespace Ryujinx.Graphics.Vulkan
Dispose(true);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Graphics.Vulkan/ImageArray.cs b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs
index 019286d28..205be5aeb 100644
--- a/src/Ryujinx.Graphics.Vulkan/ImageArray.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs
@@ -186,6 +186,8 @@ namespace Ryujinx.Graphics.Vulkan
return sets;
}
+ var dsc = program.GetNewDescriptorSetCollection(setIndex, out var isNew).Get(cbs);
+
DescriptorSetTemplate template = program.Templates[setIndex];
DescriptorSetTemplateWriter tu = templateUpdater.Begin(template);
@@ -201,6 +203,8 @@ namespace Ryujinx.Graphics.Vulkan
templateUpdater.Commit(_gd, device, sets[0]);
+ sets = dsc.GetSets();
+
return sets;
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 5fc8351a5..addad83fd 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -1241,7 +1241,7 @@ namespace Ryujinx.Graphics.Vulkan
int vbSize = vertexBuffer.Buffer.Size;
- if ((Gd.Vendor == Vendor.Amd || !OperatingSystem.IsIOSVersionAtLeast(17)) && !Gd.IsMoltenVk && vertexBuffer.Stride > 0)
+ if (Gd.Vendor == Vendor.Amd && !Gd.IsMoltenVk && vertexBuffer.Stride > 0)
{
// AMD has a bug where if offset + stride * count is greater than
// the size, then the last attribute will have the wrong value.
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
index a726b9edb..352a69521 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
@@ -409,7 +409,7 @@ namespace Ryujinx.Graphics.Vulkan
{
SType = StructureType.PipelineVertexInputStateCreateInfo,
VertexAttributeDescriptionCount = VertexAttributeDescriptionsCount,
- PVertexAttributeDescriptions = isMoltenVk ? pVertexAttributeDescriptions2 : pVertexAttributeDescriptions,
+ PVertexAttributeDescriptions = pVertexAttributeDescriptions2, // isMoltenVk ? pVertexAttributeDescriptions2 : pVertexAttributeDescriptions,
VertexBindingDescriptionCount = VertexBindingDescriptionsCount,
PVertexBindingDescriptions = pVertexBindingDescriptions,
};
@@ -521,6 +521,7 @@ namespace Ryujinx.Graphics.Vulkan
uint blendEnables = 0;
+
if (gd.IsMoltenVk && Internal.AttachmentIntegerFormatMask != 0)
{
// Blend can't be enabled for integer formats, so let's make sure it is disabled.
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureArray.cs b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs
index 99238b1f5..d2e16fc32 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureArray.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs
@@ -159,8 +159,8 @@ namespace Ryujinx.Graphics.Vulkan
}
texture.ImageLayout = ImageLayout.General;
- texture.ImageView = refs.View?.Get(cbs).Value ?? default;
- texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default;
+ texture.ImageView = refs.View?.Get(cbs).Value ?? dummyTexture.GetImageView().Get(cbs).Value;
+ texture.Sampler = refs.Sampler?.Get(cbs).Value ?? dummySampler.GetSampler().Get(cbs).Value;
if (texture.ImageView.Handle == 0)
{
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
index aa16d883e..fe73f3c7d 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
@@ -606,26 +606,67 @@ namespace Ryujinx.Graphics.Vulkan
{
return new TextureView(_gd, _device, info, Storage, FirstLayer + firstLayer, FirstLevel + firstLevel);
}
-
+
public byte[] GetData(int x, int y, int width, int height)
{
+ const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
+
int size = width * height * Info.BytesPerPixel;
- using var bufferHolder = _gd.BufferManager.Create(_gd, size);
-
- using (var cbs = _gd.CommandBufferPool.Rent())
- {
- var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
- var image = GetImage().Get(cbs).Value;
-
- CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height);
- }
-
- bufferHolder.WaitForFences();
byte[] bitmap = new byte[size];
- GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span.Empty).CopyTo(bitmap);
+
+ if (size <= MaxChunkSize)
+ {
+ using var bufferHolder = _gd.BufferManager.Create(_gd, size);
+ using (var cbs = _gd.CommandBufferPool.Rent())
+ {
+ var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
+ var image = GetImage().Get(cbs).Value;
+ CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height);
+ }
+
+ bufferHolder.WaitForFences();
+ GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span.Empty).CopyTo(bitmap);
+ return bitmap;
+ }
+
+
+ int dataPerPixel = Info.BytesPerPixel;
+ int rowStride = width * dataPerPixel;
+ int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
+ int originalHeight = height;
+ int currentY = y;
+ int bitmapOffset = 0;
+
+ while (currentY < y + originalHeight)
+ {
+ int chunkHeight = Math.Min(rowsPerChunk, y + originalHeight - currentY);
+
+ if (chunkHeight <= 0)
+ break;
+
+ int chunkSize = chunkHeight * rowStride;
+
+ // Process this chunk
+ using var bufferHolder = _gd.BufferManager.Create(_gd, chunkSize);
+ using (var cbs = _gd.CommandBufferPool.Rent())
+ {
+ var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
+ var image = GetImage().Get(cbs).Value;
+ CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, chunkSize, true, 0, 0, x, currentY, width, chunkHeight);
+ }
+
+ bufferHolder.WaitForFences();
+ GetDataFromBuffer(bufferHolder.GetDataStorage(0, chunkSize), chunkSize, Span.Empty)
+ .CopyTo(new Span(bitmap, bitmapOffset, chunkSize));
+
+ currentY += chunkHeight;
+ bitmapOffset += chunkSize;
+ }
+
return bitmap;
}
+
public PinnedSpan GetData()
{
BackgroundResource resources = _gd.BackgroundResources.Get();
@@ -738,14 +779,28 @@ namespace Ryujinx.Graphics.Vulkan
return GetDataFromBuffer(result, size, result);
}
- private ReadOnlySpan GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer, int level)
+ private ReadOnlySpan GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer = 0, int level = 0)
{
+ const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
+
int size = GetBufferDataLength(Info.GetMipSize(level));
- Span result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
- return GetDataFromBuffer(result, size, result);
+ if (size <= MaxChunkSize)
+ {
+ Span result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
+ return GetDataFromBuffer(result, size, result);
+ }
+
+ byte[] fullResult = new byte[size];
+
+ Span fullTextureData = flushBuffer.GetTextureData(cbp, this, size, layer, level);
+
+ GetDataFromBuffer(fullTextureData, size, fullTextureData).CopyTo(fullResult);
+
+ return fullResult;
}
+
///
public void SetData(MemoryOwner data)
{
@@ -769,7 +824,7 @@ namespace Ryujinx.Graphics.Vulkan
private void SetData(ReadOnlySpan data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle? region = null)
{
- const int MaxChunkSize = 1024 * 1024;
+ const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
int bufferDataLength = GetBufferDataLength(data.Length);
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index a77dba8e6..231d61718 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -422,7 +422,7 @@ namespace Ryujinx.Graphics.Vulkan
features2.Features.ShaderStorageImageMultisample,
_physicalDevice.IsDeviceExtensionPresent(ExtConditionalRendering.ExtensionName),
isDynamicStateSupported,
- features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue
+ features2.Features.MultiViewport && !IsMoltenVk, // Workaround for AMD on MoltenVK issue
!IsMoltenVk ? featuresRobustness2.NullDescriptor : false,
supportsPushDescriptors && !IsMoltenVk,
propertiesPushDescriptor.MaxPushDescriptors,
@@ -647,6 +647,14 @@ namespace Ryujinx.Graphics.Vulkan
Format.Bc7Srgb,
Format.Bc7Unorm);
+ if (!OperatingSystem.IsIOSVersionAtLeast(16, 4))
+ {
+ // On iOS 16.3.1 and earlier, these formats are not supported.
+ supportsBc123CompressionFormat = false;
+ supportsBc45CompressionFormat = false;
+ supportsBc67CompressionFormat = false;
+ }
+
bool supportsEtc2CompressionFormat = FormatCapabilities.OptimalFormatsSupport(compressedFormatFeatureFlags,
Format.Etc2RgbaSrgb,
Format.Etc2RgbaUnorm,
@@ -714,7 +722,7 @@ namespace Ryujinx.Graphics.Vulkan
SystemMemoryType memoryType;
- if (IsSharedMemory)
+ if (IsSharedMemory && !IsMoltenVk)
{
memoryType = SystemMemoryType.UnifiedMemory;
}
diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs
index 955fee4b5..63ab44299 100644
--- a/src/Ryujinx.HLE/HLEConfiguration.cs
+++ b/src/Ryujinx.HLE/HLEConfiguration.cs
@@ -21,25 +21,25 @@ namespace Ryujinx.HLE
/// The virtual file system used by the FS service.
///
/// This cannot be changed after instantiation.
- internal readonly VirtualFileSystem VirtualFileSystem;
+ public readonly VirtualFileSystem VirtualFileSystem;
///
/// The manager for handling a LibHac Horizon instance.
///
/// This cannot be changed after instantiation.
- internal readonly LibHacHorizonManager LibHacHorizonManager;
+ public readonly LibHacHorizonManager LibHacHorizonManager;
///
/// The account manager used by the account service.
///
/// This cannot be changed after instantiation.
- internal readonly AccountManager AccountManager;
+ public readonly AccountManager AccountManager;
///
/// The content manager used by the NCM service.
///
/// This cannot be changed after instantiation.
- internal readonly ContentManager ContentManager;
+ public readonly ContentManager ContentManager;
///
/// The persistent information between run for multi-application capabilities.
@@ -51,93 +51,93 @@ namespace Ryujinx.HLE
/// The GPU renderer to use for all GPU operations.
///
/// This cannot be changed after instantiation.
- internal readonly IRenderer GpuRenderer;
+ public readonly IRenderer GpuRenderer;
///
/// The audio device driver to use for all audio operations.
///
/// This cannot be changed after instantiation.
- internal readonly IHardwareDeviceDriver AudioDeviceDriver;
+ public readonly IHardwareDeviceDriver AudioDeviceDriver;
///
/// The handler for various UI related operations needed outside of HLE.
///
/// This cannot be changed after instantiation.
- internal readonly IHostUIHandler HostUIHandler;
+ public readonly IHostUIHandler HostUIHandler;
///
/// Control the memory configuration used by the emulation context.
///
/// This cannot be changed after instantiation.
- internal readonly MemoryConfiguration MemoryConfiguration;
+ public readonly MemoryConfiguration MemoryConfiguration;
///
/// The system language to use in the settings service.
///
/// This cannot be changed after instantiation.
- internal readonly SystemLanguage SystemLanguage;
+ public readonly SystemLanguage SystemLanguage;
///
/// The system region to use in the settings service.
///
/// This cannot be changed after instantiation.
- internal readonly RegionCode Region;
+ public readonly RegionCode Region;
///
/// Control the initial state of the vertical sync in the SurfaceFlinger service.
///
- internal readonly bool EnableVsync;
+ public readonly bool EnableVsync;
///
/// Control the initial state of the docked mode.
///
- internal readonly bool EnableDockedMode;
+ public readonly bool EnableDockedMode;
///
/// Control if the Profiled Translation Cache (PTC) should be used.
///
- internal readonly bool EnablePtc;
+ public readonly bool EnablePtc;
///
/// Control if the guest application should be told that there is a Internet connection available.
///
- public bool EnableInternetAccess { internal get; set; }
+ public bool EnableInternetAccess;
///
/// Control LibHac's integrity check level.
///
/// This cannot be changed after instantiation.
- internal readonly IntegrityCheckLevel FsIntegrityCheckLevel;
+ public readonly IntegrityCheckLevel FsIntegrityCheckLevel;
///
/// Control LibHac's global access logging level. Value must be between 0 and 3.
///
/// This cannot be changed after instantiation.
- internal readonly int FsGlobalAccessLogMode;
+ public readonly int FsGlobalAccessLogMode;
///
/// The system time offset to apply to the time service steady and local clocks.
///
/// This cannot be changed after instantiation.
- internal readonly long SystemTimeOffset;
+ public readonly long SystemTimeOffset;
///
/// The system timezone used by the time service.
///
/// This cannot be changed after instantiation.
- internal readonly string TimeZone;
+ public readonly string TimeZone;
///
/// Type of the memory manager used on CPU emulation.
///
- public MemoryManagerMode MemoryManagerMode { internal get; set; }
+ public MemoryManagerMode MemoryManagerMode { get; set; }
///
/// Control the initial state of the ignore missing services setting.
/// If this is set to true, when a missing service is encountered, it will try to automatically handle it instead of throwing an exception.
///
/// TODO: Update this again.
- public bool IgnoreMissingServices { internal get; set; }
+ public bool IgnoreMissingServices { get; set; }
///
/// Aspect Ratio applied to the renderer window by the SurfaceFlinger service.
@@ -152,22 +152,22 @@ namespace Ryujinx.HLE
///
/// Use Hypervisor over JIT if available.
///
- internal readonly bool UseHypervisor;
+ public readonly bool UseHypervisor;
///
/// Multiplayer LAN Interface ID (device GUID)
///
- public string MultiplayerLanInterfaceId { internal get; set; }
+ public string MultiplayerLanInterfaceId { get; set; }
///
/// Multiplayer Mode
///
- public MultiplayerMode MultiplayerMode { internal get; set; }
+ public MultiplayerMode MultiplayerMode { get; set; }
///
/// An action called when HLE force a refresh of output after docked mode changed.
///
- public Action RefreshInputConfig { internal get; set; }
+ public Action RefreshInputConfig { get; set; }
public HLEConfiguration(VirtualFileSystem virtualFileSystem,
LibHacHorizonManager libHacHorizonManager,
diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs
index fc02ea172..d0f1f8da3 100644
--- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs
@@ -1,10 +1,14 @@
using Ryujinx.Common;
+using System.Runtime.InteropServices;
using System;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletProxy
{
class ILibraryAppletSelfAccessor : IpcService
{
+ [DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void TriggerCallback(string cIdentifier);
+
private readonly AppletStandalone _appletStandalone = new();
public ILibraryAppletSelfAccessor(ServiceCtx context)
@@ -45,6 +49,14 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
return ResultCode.Success;
}
+ [CommandCmif(1)]
+ public ResultCode PushOutData(ServiceCtx context)
+ {
+ TriggerCallback("exit-emulation");
+ return ResultCode.Success;
+ }
+
+
[CommandCmif(11)]
// GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo
public ResultCode GetLibraryAppletInfo(ServiceCtx context)
diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IStorage.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IStorage.cs
index 311084aa1..664ff07bd 100644
--- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IStorage.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IStorage.cs
@@ -11,6 +11,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
Data = data;
}
+ public byte[] GetData()
+ {
+ return Data;
+ }
+
[CommandCmif(0)]
// Open() -> object
public ResultCode Open(ServiceCtx context)
diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs
index 8dfe33a63..e615cc8ce 100644
--- a/src/Ryujinx.Headless.SDL2/Options.cs
+++ b/src/Ryujinx.Headless.SDL2/Options.cs
@@ -29,6 +29,17 @@ namespace Ryujinx.Headless.SDL2
[Option("exclusive-fullscreen-height", Required = false, Default = 1080, HelpText = "Set vertical resolution for exclusive fullscreen mode.")]
public int ExclusiveFullscreenHeight { get; set; }
+ // Host Information
+
+ [Option("device-model", Required = false, HelpText = "Set the current iDevice Model")]
+ public string DeviceModel { get; set; }
+
+ [Option("has-memory-entitlement", Required = false, HelpText = "If the increased memory entitlement exists.")]
+ public bool MemoryEnt { get; set; }
+
+ [Option("device-display-name", Required = false, HelpText = "Set the current iDevice display name.")]
+ public string DisplayName { get; set; }
+
// Input
[Option("correct-controller", Required = false, Default = false, HelpText = "Makes the on-screen controller (iOS) buttons correspond to what they show.")]
@@ -88,6 +99,33 @@ namespace Ryujinx.Headless.SDL2
[Option("input-id-handheld", Required = false, HelpText = "Set the input id in use for the Handheld Player.")]
public string InputIdHandheld { get; set; }
+ [Option("input-dsu-server-1", Required = false, HelpText = "Set the input DSU server:port in use for Player 1.")]
+ public string InputDSUServer1 { get; set; }
+
+ [Option("input-dsu-server-2", Required = false, HelpText = "Set the input DSU server:port in use for Player 2.")]
+ public string InputDSUServer2 { get; set; }
+
+ [Option("input-dsu-server-3", Required = false, HelpText = "Set the input DSU server:port in use for Player 3.")]
+ public string InputDSUServer3 { get; set; }
+
+ [Option("input-dsu-server-4", Required = false, HelpText = "Set the input DSU server:port in use for Player 4.")]
+ public string InputDSUServer4 { get; set; }
+
+ [Option("input-dsu-server-5", Required = false, HelpText = "Set the input DSU server:port in use for Player 5.")]
+ public string InputDSUServer5 { get; set; }
+
+ [Option("input-dsu-server-6", Required = false, HelpText = "Set the input DSU server:port in use for Player 6.")]
+ public string InputDSUServer6 { get; set; }
+
+ [Option("input-dsu-server-7", Required = false, HelpText = "Set the input DSU server:port in use for Player 7.")]
+ public string InputDSUServer7 { get; set; }
+
+ [Option("input-dsu-server-8", Required = false, HelpText = "Set the input DSU server:port in use for Player 8.")]
+ public string InputDSUServer8 { get; set; }
+
+ [Option("input-dsu-server-handheld", Required = false, HelpText = "Set the input DSU server:port in use for the Handheld Player.")]
+ public string InputDSUServerHandheld { get; set; }
+
[Option("enable-keyboard", Required = false, Default = false, HelpText = "Enable or disable keyboard support (Independent from controllers binding).")]
public bool EnableKeyboard { get; set; }
@@ -196,7 +234,7 @@ namespace Ryujinx.Headless.SDL2
[Option("aspect-ratio", Required = false, Default = AspectRatio.Fixed16x9, HelpText = "Aspect Ratio applied to the renderer window.")]
public AspectRatio AspectRatio { get; set; }
- [Option("backend-threading", Required = false, Default = BackendThreading.Auto, HelpText = "Whether or not backend threading is enabled. The \"Auto\" setting will determine whether threading should be enabled at runtime.")]
+ [Option("backend-threading", Required = false, Default = BackendThreading.On, HelpText = "Whether or not backend threading is enabled. The \"Auto\" setting will determine whether threading should be enabled at runtime.")]
public BackendThreading BackendThreading { get; set; }
[Option("disable-macro-hle", Required = false, HelpText = "Disables high-level emulation of Macro code. Leaving this enabled improves performance but may cause graphical glitches in some games.")]
diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
index f99547296..d04acc69c 100644
--- a/src/Ryujinx.Headless.SDL2/Program.cs
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -93,6 +93,7 @@ using Ryujinx.Input.HLE;
using Silk.NET.Vulkan;
using System;
using System.IO;
+using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using SDL2;
@@ -266,7 +267,6 @@ namespace Ryujinx.Headless.SDL2
[UnmanagedCallersOnly(EntryPoint = "initialize")]
public static unsafe void Initialize()
{
-
AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
if (_virtualFileSystem == null)
@@ -287,16 +287,6 @@ namespace Ryujinx.Headless.SDL2
{
_contentManager = new ContentManager(_virtualFileSystem);
}
-
- if (_accountManager == null)
- {
- _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, "");
- }
-
- if (_userChannelPersistence == null)
- {
- _userChannelPersistence = new UserChannelPersistence();
- }
}
static void Main(string[] args)
@@ -403,11 +393,32 @@ namespace Ryujinx.Headless.SDL2
return String.Empty;
}
+ [UnmanagedCallersOnly(EntryPoint = "pause_emulation")]
+ public static void PauseEmulation(bool shouldPause)
+ {
+ if (_window != null && _window.Device != null)
+ {
+ if (!shouldPause)
+ {
+ _window.Device.SetVolume(1);
+ _window._isPaused = false;
+ _window._pauseEvent.Set();
+ }
+ else
+ {
+ _window.Device.SetVolume(0);
+ _window._isPaused = true;
+ _window._pauseEvent.Reset();
+ }
+ }
+ }
+
[UnmanagedCallersOnly(EntryPoint = "stop_emulation")]
public static void StopEmulation()
{
if (_window != null)
{
+ _window.Exit();
}
}
@@ -883,23 +894,13 @@ namespace Ryujinx.Headless.SDL2
return gameInfo;
}
- private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option)
+ private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, string inputDSUServer, PlayerIndex index, Options option)
{
if (inputId == null)
{
- if (index == PlayerIndex.Player1)
- {
- Logger.Info?.Print(LogClass.Application, $"{index} not configured, defaulting to default keyboard.");
+ Logger.Info?.Print(LogClass.Application, $"{index} not configured");
- // Default to keyboard
- inputId = "0";
- }
- else
- {
- Logger.Info?.Print(LogClass.Application, $"{index} not configured");
-
- return null;
- }
+ return null;
}
IGamepad gamepad;
@@ -990,12 +991,26 @@ namespace Ryujinx.Headless.SDL2
{
bool isNintendoStyle = true; // gamepadName.Contains("Nintendo") || gamepadName.Contains("Joycons");
+ ControllerType currentController;
+ if (index == PlayerIndex.Handheld)
+ {
+ currentController = ControllerType.Handheld;
+ }
+ else if (gamepadName.Contains("Joycons") || gamepadName.Contains("Backbone"))
+ {
+ currentController = ControllerType.JoyconPair;
+ }
+ else
+ {
+ currentController = ControllerType.ProController;
+ }
+
config = new StandardControllerInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL2,
Id = null,
- ControllerType = ControllerType.JoyconPair,
+ ControllerType = currentController,
DeadzoneLeft = 0.1f,
DeadzoneRight = 0.1f,
RangeLeft = 1.0f,
@@ -1056,9 +1071,31 @@ namespace Ryujinx.Headless.SDL2
{
StrongRumble = 1f,
WeakRumble = 1f,
- EnableRumble = false,
+ EnableRumble = true,
},
};
+
+ // Setup DSU Motion
+ if (config is StandardControllerInputConfig standardConfig && !string.IsNullOrWhiteSpace(inputDSUServer))
+ {
+ var serverString = inputDSUServer.Trim();
+
+ var parts = serverString.Split(new[] { ':' }, 2);
+ if (parts.Length == 2 && int.TryParse(parts[1], out var port))
+ {
+ var slot = index == PlayerIndex.Handheld ? 0 : (int)index;
+ standardConfig.Motion = new CemuHookMotionConfigController
+ {
+ MotionBackend = MotionInputBackendType.CemuHook,
+ EnableMotion = true,
+ Sensitivity = 100,
+ GyroDeadzone = 1,
+ Slot = slot,
+ DsuServerHost = parts[0],
+ DsuServerPort = port,
+ };
+ }
+ }
}
}
else
@@ -1119,39 +1156,24 @@ namespace Ryujinx.Headless.SDL2
static void Load(Options option)
{
+ _libHacHorizonManager = new LibHacHorizonManager();
+ _libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
+ _libHacHorizonManager.InitializeArpServer();
+ _libHacHorizonManager.InitializeBcatServer();
+ _libHacHorizonManager.InitializeSystemClients();
- if (_virtualFileSystem == null)
- {
- _virtualFileSystem = VirtualFileSystem.CreateInstance();
- }
+ // _contentManager = new ContentManager(_virtualFileSystem);
- if (_libHacHorizonManager == null)
- {
- _libHacHorizonManager = new LibHacHorizonManager();
- _libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
- _libHacHorizonManager.InitializeArpServer();
- _libHacHorizonManager.InitializeBcatServer();
- _libHacHorizonManager.InitializeSystemClients();
- }
+ _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
- if (_contentManager == null)
- {
- _contentManager = new ContentManager(_virtualFileSystem);
- }
+ _userChannelPersistence = new UserChannelPersistence();
- if (_accountManager == null)
- {
- _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
- }
+ _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
- if (_userChannelPersistence == null)
+ if (OperatingSystem.IsIOS())
{
- _userChannelPersistence = new UserChannelPersistence();
- }
-
- if (_inputManager == null)
- {
- _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
+ Logger.Info?.Print(LogClass.Application, $"Current Device: {option.DisplayName} ({option.DeviceModel}) {Environment.OSVersion.Version}");
+ Logger.Info?.Print(LogClass.Application, $"Increased Memory Limit: {option.MemoryEnt}");
}
GraphicsConfig.EnableShaderCache = true;
@@ -1202,9 +1224,14 @@ namespace Ryujinx.Headless.SDL2
return;
}
+
- if (option.InputPath == "MiiMaker") {
- string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
+ Match match = Regex.Match(option.InputPath, @"0x[0-9A-Fa-f]+");
+ if (match.Success)
+ {
+ string hexStr = match.Value.Substring(2);
+ ulong id = Convert.ToUInt64(hexStr, 16);
+ string contentPath = _contentManager.GetInstalledContentPath(id, StorageId.BuiltInSystem, NcaContentType.Program);
option.InputPath = contentPath;
}
@@ -1213,9 +1240,9 @@ namespace Ryujinx.Headless.SDL2
_enableKeyboard = option.EnableKeyboard;
_enableMouse = option.EnableMouse;
- static void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option)
+ static void LoadPlayerConfiguration(string inputProfileName, string inputId, string inputDSUServer, PlayerIndex index, Options option)
{
- InputConfig inputConfig = HandlePlayerConfiguration(inputProfileName, inputId, index, option);
+ InputConfig inputConfig = HandlePlayerConfiguration(inputProfileName, inputId, inputDSUServer, index, option);
if (inputConfig != null)
{
@@ -1223,15 +1250,15 @@ namespace Ryujinx.Headless.SDL2
}
}
- LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1, option);
- LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2, option);
- LoadPlayerConfiguration(option.InputProfile3Name, option.InputId3, PlayerIndex.Player3, option);
- LoadPlayerConfiguration(option.InputProfile4Name, option.InputId4, PlayerIndex.Player4, option);
- LoadPlayerConfiguration(option.InputProfile5Name, option.InputId5, PlayerIndex.Player5, option);
- LoadPlayerConfiguration(option.InputProfile6Name, option.InputId6, PlayerIndex.Player6, option);
- LoadPlayerConfiguration(option.InputProfile7Name, option.InputId7, PlayerIndex.Player7, option);
- LoadPlayerConfiguration(option.InputProfile8Name, option.InputId8, PlayerIndex.Player8, option);
- LoadPlayerConfiguration(option.InputProfileHandheldName, option.InputIdHandheld, PlayerIndex.Handheld, option);
+ LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, option.InputDSUServer1, PlayerIndex.Player1, option);
+ LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, option.InputDSUServer2, PlayerIndex.Player2, option);
+ LoadPlayerConfiguration(option.InputProfile3Name, option.InputId3, option.InputDSUServer3, PlayerIndex.Player3, option);
+ LoadPlayerConfiguration(option.InputProfile4Name, option.InputId4, option.InputDSUServer4, PlayerIndex.Player4, option);
+ LoadPlayerConfiguration(option.InputProfile5Name, option.InputId5, option.InputDSUServer5, PlayerIndex.Player5, option);
+ LoadPlayerConfiguration(option.InputProfile6Name, option.InputId6, option.InputDSUServer6, PlayerIndex.Player6, option);
+ LoadPlayerConfiguration(option.InputProfile7Name, option.InputId7, option.InputDSUServer7, PlayerIndex.Player7, option);
+ LoadPlayerConfiguration(option.InputProfile8Name, option.InputId8, option.InputDSUServer8, PlayerIndex.Player8, option);
+ LoadPlayerConfiguration(option.InputProfileHandheldName, option.InputIdHandheld, option.InputDSUServerHandheld, PlayerIndex.Handheld, option);
if (_inputConfiguration.Count == 0)
{
@@ -1694,5 +1721,137 @@ namespace Ryujinx.Headless.SDL2
span.Clear();
Encoding.UTF8.GetBytes(source, span);
}
+
+ [UnmanagedCallersOnly(EntryPoint = "update_settings_external")]
+ public static unsafe int UpdateSettingsExternal(int argCount, IntPtr* pArgs)
+ {
+ string[] args = new string[argCount];
+
+ try
+ {
+ for (int i = 0; i < argCount; i++)
+ {
+ args[i] = Marshal.PtrToStringAnsi(pArgs[i]);
+ }
+
+ Options parsedOptions = null;
+ Parser.Default.ParseArguments(args)
+ .WithParsed(opts => parsedOptions = opts);
+
+ if (parsedOptions == null)
+ {
+ Console.WriteLine("Failed to parse options.");
+ return -1;
+ }
+
+ ApplyDynamicSettings(parsedOptions);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e.ToString());
+ return -1;
+ }
+
+ return 0;
+ }
+
+ private static void ApplyDynamicSettings(Options options)
+ {
+ Graphics.Gpu.GraphicsConfig.ResScale = options.ResScale;
+ Graphics.Gpu.GraphicsConfig.MaxAnisotropy = options.MaxAnisotropy;
+ Graphics.Gpu.GraphicsConfig.EnableShaderCache = !options.DisableShaderCache;
+ Graphics.Gpu.GraphicsConfig.EnableTextureRecompression = options.EnableTextureRecompression;
+ Graphics.Gpu.GraphicsConfig.EnableMacroHLE = !options.DisableMacroHLE;
+
+ if (_window != null)
+ {
+ _window.IsFullscreen = options.IsFullscreen;
+ _window.DisplayId = options.DisplayId;
+ _window.IsExclusiveFullscreen = options.IsExclusiveFullscreen;
+ _window.ExclusiveFullscreenWidth = options.ExclusiveFullscreenWidth;
+ _window.ExclusiveFullscreenHeight = options.ExclusiveFullscreenHeight;
+ _window.AntiAliasing = options.AntiAliasing;
+ _window.ScalingFilter = options.ScalingFilter;
+ _window.ScalingFilterLevel = options.ScalingFilterLevel;
+ _window._aspectRatio = options.AspectRatio;
+ }
+
+ if (_emulationContext != null)
+ {
+ _emulationContext.SetVolume(options.AudioVolume);
+
+ _emulationContext.System.State.SetLanguage(options.SystemLanguage);
+ _emulationContext.System.State.SetRegion(options.SystemRegion);
+ _emulationContext.EnableDeviceVsync = !options.DisableVSync;
+ _emulationContext.System.State.DockedMode = !options.DisableDockedMode;
+ _emulationContext.System.EnablePtc = !options.DisablePTC;
+ _emulationContext.System.FsIntegrityCheckLevel = !options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None;
+ _emulationContext.System.GlobalAccessLogMode = options.FsGlobalAccessLogMode;
+ _emulationContext.Configuration.IgnoreMissingServices = options.IgnoreMissingServices;
+ _emulationContext.Configuration.AspectRatio = options.AspectRatio;
+ _emulationContext.Configuration.EnableInternetAccess = options.EnableInternetAccess;
+ _emulationContext.Configuration.MemoryManagerMode = options.MemoryManagerMode;
+ _emulationContext.Configuration.MultiplayerLanInterfaceId = options.MultiplayerLanInterfaceId;
+ }
+
+ Logger.SetEnable(LogLevel.Debug, options.LoggingEnableDebug);
+ Logger.SetEnable(LogLevel.Stub, !options.LoggingDisableStub);
+ Logger.SetEnable(LogLevel.Info, !options.LoggingDisableInfo);
+ Logger.SetEnable(LogLevel.Warning, !options.LoggingDisableWarning);
+ Logger.SetEnable(LogLevel.Error, options.LoggingEnableError);
+ Logger.SetEnable(LogLevel.Trace, options.LoggingEnableTrace);
+ Logger.SetEnable(LogLevel.Guest, !options.LoggingDisableGuest);
+ Logger.SetEnable(LogLevel.AccessLog, options.LoggingEnableFsAccessLog);
+ }
+
+
+ // Old :3
+ private static void ReplaceEmulationContextConfiguration(Switch emu, Options options)
+ {
+ var oldConfig = emu.Configuration;
+
+ var newConfig = new HLEConfiguration(
+ _virtualFileSystem,
+ _libHacHorizonManager,
+ _contentManager,
+ _accountManager,
+ _userChannelPersistence,
+ oldConfig.GpuRenderer,
+ oldConfig.AudioDeviceDriver,
+ oldConfig.MemoryConfiguration,
+ oldConfig.HostUIHandler,
+ options.SystemLanguage,
+ options.SystemRegion,
+ !options.DisableVSync,
+ !options.DisableDockedMode,
+ !options.DisablePTC,
+ options.EnableInternetAccess,
+ !options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
+ options.FsGlobalAccessLogMode,
+ options.SystemTimeOffset,
+ options.SystemTimeZone,
+ options.MemoryManagerMode,
+ options.IgnoreMissingServices,
+ options.AspectRatio,
+ options.AudioVolume,
+ options.UseHypervisor,
+ options.MultiplayerLanInterfaceId,
+ Ryujinx.Common.Configuration.Multiplayer.MultiplayerMode.LdnMitm
+ );
+
+ var configField = typeof(Switch).GetField("k__BackingField", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
+ if (configField != null)
+ {
+ configField.SetValue(emu, newConfig);
+ }
+
+ emu.System.State.SetLanguage(newConfig.SystemLanguage);
+ emu.System.State.SetRegion(newConfig.Region);
+ emu.EnableDeviceVsync = newConfig.EnableVsync;
+ emu.System.State.DockedMode = newConfig.EnableDockedMode;
+ emu.System.EnablePtc = newConfig.EnablePtc;
+ emu.System.FsIntegrityCheckLevel = newConfig.FsIntegrityCheckLevel;
+ emu.System.GlobalAccessLogMode = newConfig.FsGlobalAccessLogMode;
+ }
}
}
diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs
index 5f8eda72a..624089254 100644
--- a/src/Ryujinx.Headless.SDL2/WindowBase.cs
+++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs
@@ -44,6 +44,9 @@ namespace Ryujinx.Headless.SDL2
_mainThreadActions.Enqueue(action);
}
+ public bool _isPaused;
+ public ManualResetEvent _pauseEvent;
+
public NpadManager NpadManager;
public TouchScreenManager TouchScreenManager;
public Switch Device;
@@ -82,7 +85,7 @@ namespace Ryujinx.Headless.SDL2
private string _gpuDriverName;
- private readonly AspectRatio _aspectRatio;
+ public AspectRatio _aspectRatio;
private readonly bool _enableMouse;
public WindowBase(
@@ -104,6 +107,7 @@ namespace Ryujinx.Headless.SDL2
_gpuCancellationTokenSource = new CancellationTokenSource();
_exitEvent = new ManualResetEvent(false);
_gpuDoneEvent = new ManualResetEvent(false);
+ _pauseEvent = new ManualResetEvent(true);
_aspectRatio = aspectRatio;
_enableMouse = enableMouse;
HostUITheme = new HeadlessHostUiTheme();
@@ -158,7 +162,7 @@ namespace Ryujinx.Headless.SDL2
private void InitializeWindow()
{
- if (this is Ryujinx.Headless.SDL2.Vulkan.MoltenVKWindow) {
+ if (this is Vulkan.MoltenVKWindow) {
string message = $"Not using SDL Windows, Skipping...";
Logger.Info?.Print(LogClass.Application, message);
@@ -298,6 +302,8 @@ namespace Ryujinx.Headless.SDL2
return;
}
+ _pauseEvent.WaitOne();
+
_ticks += _chrono.ElapsedTicks;
_chrono.Restart();
@@ -378,7 +384,6 @@ namespace Ryujinx.Headless.SDL2
{
while (_isActive)
{
-
UpdateFrame();
SDL_PumpEvents();
diff --git a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs
index 0ffae6c2e..d69ea835a 100644
--- a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs
+++ b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs
@@ -9,8 +9,18 @@ namespace Ryujinx.Input.SDL2
{
private readonly Dictionary _gamepadsInstanceIdsMapping;
private readonly List _gamepadsIds;
+ private readonly object _lock = new object();
- public ReadOnlySpan GamepadsIds => _gamepadsIds.ToArray();
+ public ReadOnlySpan GamepadsIds
+ {
+ get
+ {
+ lock (_lock)
+ {
+ return _gamepadsIds.ToArray();
+ }
+ }
+ }
public string DriverName => "SDL2";
@@ -35,7 +45,7 @@ namespace Ryujinx.Input.SDL2
}
}
- private static string GenerateGamepadId(int joystickIndex)
+ private string GenerateGamepadId(int joystickIndex)
{
Guid guid = SDL_JoystickGetDeviceGUID(joystickIndex);
@@ -44,14 +54,16 @@ namespace Ryujinx.Input.SDL2
return null;
}
+ // Include joystickIndex at the start of the ID to maintain compatibility with GetJoystickIndexByGamepadId
return joystickIndex + "-" + guid;
}
- private static int GetJoystickIndexByGamepadId(string id)
+ private int GetJoystickIndexByGamepadId(string id)
{
string[] data = id.Split("-");
- if (data.Length != 6 || !int.TryParse(data[0], out int joystickIndex))
+ // Parse the joystick index from the ID string
+ if (data.Length < 2 || !int.TryParse(data[0], out int joystickIndex))
{
return -1;
}
@@ -64,7 +76,11 @@ namespace Ryujinx.Input.SDL2
if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id))
{
_gamepadsInstanceIdsMapping.Remove(joystickInstanceId);
- _gamepadsIds.Remove(id);
+
+ lock (_lock)
+ {
+ _gamepadsIds.Remove(id);
+ }
OnGamepadDisconnected?.Invoke(id);
}
@@ -74,6 +90,13 @@ namespace Ryujinx.Input.SDL2
{
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE)
{
+ if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId))
+ {
+ // Sometimes a JoyStick connected event fires after the app starts even though it was connected before
+ // so it is rejected to avoid doubling the entries.
+ return;
+ }
+
string id = GenerateGamepadId(joystickDeviceId);
if (id == null)
@@ -81,16 +104,21 @@ namespace Ryujinx.Input.SDL2
return;
}
- // Sometimes a JoyStick connected event fires after the app starts even though it was connected before
- // so it is rejected to avoid doubling the entries.
- if (_gamepadsIds.Contains(id))
+ // Check if we already have this gamepad ID in our list
+ lock (_lock)
{
- return;
+ if (_gamepadsIds.Contains(id))
+ {
+ return;
+ }
}
if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id))
{
- _gamepadsIds.Add(id);
+ lock (_lock)
+ {
+ _gamepadsIds.Add(id);
+ }
OnGamepadConnected?.Invoke(id);
}
@@ -103,13 +131,17 @@ namespace Ryujinx.Input.SDL2
{
SDL2Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected;
SDL2Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected;
-
+
+ // Simulate a full disconnect when disposing
foreach (string id in _gamepadsIds)
{
OnGamepadDisconnected?.Invoke(id);
}
- _gamepadsIds.Clear();
+ lock (_lock)
+ {
+ _gamepadsIds.Clear();
+ }
SDL2Driver.Instance.Dispose();
}
@@ -130,11 +162,6 @@ namespace Ryujinx.Input.SDL2
return null;
}
- if (id != GenerateGamepadId(joystickIndex))
- {
- return null;
- }
-
IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex);
if (gamepadHandle == IntPtr.Zero)
@@ -145,4 +172,4 @@ namespace Ryujinx.Input.SDL2
return new SDL2Gamepad(gamepadHandle, id);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
similarity index 100%
rename from src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
rename to src/Ryujinx.UI.Common/App/ApplicationLibrary.cs