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