Compare commits

..

42 Commits

Author SHA1 Message Date
Stossy11
bff023563b some changes™ 2025-04-28 19:59:32 +10:00
Stossy11
90859393a3 Set controller code back and add back Software JIT Cache Regions 2025-04-28 19:53:37 +10:00
c5c79c26ea
Refactor settings to include update check option and adjust API URLs 2025-04-18 15:25:23 +12:00
61fca7892f Merge pull request 'Adds version number to settings and general UI fixes/tweaks' (#27) from show-version-number into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#27
2025-04-13 09:41:56 +00:00
6b045f3e6f fix bundle id again
(i suck)
2025-04-13 09:38:52 +00:00
f33e8ed879 fix team 2025-04-13 09:37:58 +00:00
c32873a734 fix bundle id 2025-04-13 09:36:51 +00:00
c6415d7e32
Refactor navigation button labels and enhance SettingsView with app version display and keyboard dismissal functionality 2025-04-13 21:31:10 +12:00
5c18cb1bbb
Enforce Gitignore 2025-04-13 21:31:00 +12:00
Stossy11
fc68e3d413 Set Bundle ID back 2025-04-10 22:34:31 +10:00
Stossy11
e382a35387 Add Fixed Handheld mode, Location to keep game running in the background, New Airplay Menu amd more 2025-04-10 22:30:56 +10:00
Stossy11
15171a703a Add JIT entitlement to source 2025-04-08 14:02:34 +10:00
Stossy11
4530a8839b Remove patreon in Source 2025-04-08 13:57:44 +10:00
Stossy11
4671ec67a2 Fix Icon in Source 2025-04-08 13:54:21 +10:00
Stossy11
a5fe1a34c5 Fix Source 2025-04-08 13:53:21 +10:00
Stossy11
b9282a25e8 Update a lot, new logging and such 2025-04-08 13:23:41 +10:00
Stossy11
0bb5389370 Add Sensitivity, Add Device Model, Memory Limit and more to logs, Disable JitStreamer EB in favour of StikJIT, Change Cache Size, Update Model Name in settings 2025-04-02 18:59:35 +11:00
Stossy11
8b81cb39d7 Fully Fix File Importer 2025-03-29 17:37:42 +11:00
Stossy11
ccdb8b76a8 Hopefully fix File Picker 2025-03-28 07:58:52 +11:00
37020a5026 Update LICENSE.txt 2025-03-24 08:49:09 +00:00
259f6c6872 Update LICENSE.txt 2025-03-24 08:39:21 +00:00
Stossy11
2b7e29fa21 Implement new Virtual Controller Joystick. Add Game Requirements 2025-03-24 17:26:25 +11:00
8917ebf708 revert d326f5a00b651fa331e37faa68fc9019b415f31e
im dumb

revert Fix typos

i should git blame it 😭
2025-03-23 02:25:23 +00:00
d326f5a00b Fix typos
i should git blame it 😭
2025-03-23 02:22:18 +00:00
3721a77cc4 Merge pull request 'Update to newer app icon (fits better into the bounding box)' (#23) from CycloKid/MeloNX:XC-ios-ht into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#23

melonx
2025-03-22 01:08:51 +00:00
667d54ed2d okay NOW we're in business🤑 2025-03-20 15:11:56 +00:00
1b70bfea8b its not resized properly. shit. brb. 2025-03-20 15:07:43 +00:00
33b8571414 Replace app icon with the new one 🤑 2025-03-20 15:06:16 +00:00
33af004d85 Delete src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png 2025-03-20 15:02:30 +00:00
Stossy11
54cb7eb953 Updated JitStreamer Implementation, Reimplemented Texture Chunks, Reworked Alerts and more 2025-03-20 21:33:28 +11:00
ceab2f0ac8 Fix spelling mistake in README 2025-03-16 10:05:09 +00:00
7986859398 Update README.md 2025-03-16 10:02:34 +00:00
c4506da8a1 Update README.md 2025-03-15 21:10:43 +00:00
9f72c9da10 Update README.md 2025-03-15 21:03:36 +00:00
ba0c49f545 Update README With Experimental Free Developer Account guide 2025-03-15 21:02:49 +00:00
80148ac69a :sus64: 2025-03-13 06:42:26 +00:00
Stossy11
7417ddfeef Update version number 2025-03-13 17:18:42 +11:00
Stossy11
f66590203a Fix Setup 2025-03-13 09:40:18 +11:00
stossy11
f091e6c5ea Update README.md 2025-03-12 22:36:03 +00:00
stossy11
75a66586b2 Update README.md 2025-03-12 22:32:21 +00:00
Stossy11
3207e1e739 Merge pull request 'Fix for updates and DLCs' (#21) from XITRIX/MeloNX:updates-fix into XC-ios-ht 2025-03-13 09:30:28 +11:00
Stossy11
a6b4f2d91f Rewrite the menu Code, Add Metal HUD to advanced options and more 2025-03-13 09:05:21 +11:00
89 changed files with 6259 additions and 3292 deletions

View File

@ -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

View File

@ -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 <https://gnu.org/>
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

View File

@ -6,11 +6,6 @@
<h1 align="center">MeloNX</h1>
## Documentation
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
<p align="center">
MeloNX enables Nintendo Switch game emulation on iOS using the Ryujinx iOS code base.
</p>
@ -28,11 +23,10 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
## FAQ
- MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but may have issues or not work at all.
- MeloNX needs Xcode or a Paid Apple Developer Account. SideStore support may come soon (SideStore Side Issue)
- MeloNX needs JIT
- MeloNX cannot be Sideloaded normally and requires the use of the following Installation Guide(s).
- MeloNX requires JIT
- Recommended Device: iPhone 15 Pro or newer.
- Low-End Recommended Device**: iPhone 13 Pro.
- Lowest Supported Device: iPhone XR
- Low-End Recommended Device: iPhone 13 Pro.
## How to install
@ -54,14 +48,67 @@ 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)
5. **Add Necessary Files**
If having Issues installing firmware (Make sure your Keys are installed first)
- If needed, install firmware and keys from **Ryujinx Desktop**.
- If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders
### Free Developer Account (Experimental)
### Xcode
1. **Sideload MeloNX**
- Use [SideStore](https://sidestore.io/) or [AltStore](https://altstore.io/) (**NOT** AltStore PAL).
2. **Sideload the Entitlement App**
- Install [this app](https://github.com/hugeBlack/GetMoreRam/releases/download/nightly/Entitlement.ipa) using [SideStore](https://sidestore.io/) or [AltStore](https://altstore.io/) (**NOT** AltStore PAL).
3. **Sign In to Your Account**
- Open **Settings** in the entitlement app and sign in with your Apple ID.
4. **Refresh App IDs**
- Navigate to the **App IDs** page.
- Tap **Refresh** to update the list.
5. **Enable Increased Memory Limit**
- Select **MeloNX** (should be like "com.stossy11.MeloNX" or some variation) from the list.
- Tap **Add Increased Memory Limit**.
6. **Reinstall MeloNX**
- Delete the existing installation.
- Sideload the app again using SideStore or AltStore.
7. **Verify Increased Memory Limit**
- Open MeloNX and check if the **Increased Memory Limit** is enabled.
8. **Add Necessary Files**
If having Issues installing firmware (Make sure your keys are installed first)
- If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders
9. **Enable JIT**
- Use your preferred method to enable Just-In-Time (JIT) compilation.
- We recommend using [JitStreamer](https://jkcoxson.com/jitstreamer)
### TrollStore
As Said in FAQ:
> MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but may have issues or not work at all.
1. **Install MeloNX with TrollStore**
2. **Add Necessary Files**
3. **Enable TrollStore JIT**
- MeloNX includes automatic JIT using the TrollStore URL Scheme
- Open MeloNX Settings
- Scroll down and enable the "TrollStore JIT" toggle
- Profit
### Free Developer Account (Xcode)
**NOTE: These Xcode builds are nightly and may have unfinished features.**
@ -70,8 +117,8 @@ If having Issues installing firmware (Make sure your Keys are installed first)
2. **Add Necessary Files**
If having Issues installing firmware (Make sure your Keys are installed first)
- If needed, install firmware and keys from **Ryujinx Desktop**.
If having Issues installing firmware (Make sure your keys are installed first)
- If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
- Copy the **bis** and **system** folders
## Features
@ -99,8 +146,8 @@ If having Issues installing firmware (Make sure your Keys are installed first)
- **Input**
We currently have support for keyboard, touch input, JoyCon input support, and nearly all controllers.
Motion controls are not natively supported for now.
Motion controls are natively supported in most cases.
- **DLC & Modifications**
MeloNX supports DLC + Game Update Add-ons.

View File

@ -41,4 +41,6 @@ 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"

49
source.json Normal file
View File

@ -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": []
}

View File

@ -8,6 +8,6 @@
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
VERSION = 1.5.0
VERSION = 1.7.0
DOTNET = /usr/local/share/dotnet/dotnet

View File

@ -24,8 +24,6 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
256C91642D8126E300F9736D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 256C91632D8126E300F9736D /* Alamofire */; };
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
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 */; };
@ -33,6 +31,13 @@
/* 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 */;
@ -47,13 +52,6 @@
remoteGlobalIDString = 4E80A98C2CD6F54500029585;
remoteInfo = MeloNX;
};
4EE019E62D7CF7D600B7D583 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
};
BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@ -117,7 +115,7 @@
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
CodeSignOnCopy,
);
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework" = (
"Dependencies/Dynamic Libraries/RyujinxHelper.framework" = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
@ -130,10 +128,6 @@
"Dependencies/Dynamic Libraries/libavutil.dylib" = (
CodeSignOnCopy,
);
Dependencies/XCFrameworks/MoltenVK.xcframework = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
Dependencies/XCFrameworks/SDL2.xcframework = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
@ -178,7 +172,7 @@
"Dependencies/Dynamic Libraries/libavutil.dylib",
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework",
"Dependencies/Dynamic Libraries/RyujinxHelper.framework",
Dependencies/XCFrameworks/libavcodec.xcframework,
Dependencies/XCFrameworks/libavfilter.xcframework,
Dependencies/XCFrameworks/libavformat.xcframework,
@ -187,7 +181,6 @@
Dependencies/XCFrameworks/libswresample.xcframework,
Dependencies/XCFrameworks/libswscale.xcframework,
Dependencies/XCFrameworks/libteakra.xcframework,
Dependencies/XCFrameworks/MoltenVK.xcframework,
Dependencies/XCFrameworks/SDL2.xcframework,
);
};
@ -204,9 +197,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
256C91642D8126E300F9736D /* Alamofire in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
);
@ -296,16 +287,14 @@
buildRules = (
);
dependencies = (
4EE019E72D7CF7D600B7D583 /* PBXTargetDependency */,
4E2953AC2D803BC9000497CD /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
4E80A98F2CD6F54500029585 /* MeloNX */,
);
name = MeloNX;
packageProductDependencies = (
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
256C91632D8126E300F9736D /* Alamofire */,
);
productName = MeloNX;
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
@ -365,7 +354,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1610;
LastUpgradeCheck = 1620;
TargetAttributes = {
4E80A98C2CD6F54500029585 = {
CreatedOnToolsVersion = 16.1;
@ -396,9 +385,7 @@
mainGroup = 4E80A9842CD6F54500029585;
minimizedProjectReferenceProxies = 1;
packageReferences = (
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */,
);
preferredProjectObjectVersion = 56;
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
@ -457,7 +444,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/native/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n";
shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/publish/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n";
};
/* End PBXShellScriptBuildPhase section */
@ -486,6 +473,12 @@
/* 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 */;
@ -496,11 +489,6 @@
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
};
4EE019E72D7CF7D600B7D583 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
targetProxy = 4EE019E62D7CF7D600B7D583 /* PBXContainerItemProxy */;
};
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
@ -655,7 +643,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 4TD3JXVDW7;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -716,8 +704,34 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(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 = s;
GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
@ -731,7 +745,7 @@
INFOPLIST_KEY_UIRequiresFullScreen = YES;
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",
@ -840,13 +854,74 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(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 = xyz.belladev.MeloNX;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
SWIFT_VERSION = 5.0;
@ -864,7 +939,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 4TD3JXVDW7;
DEVELOPMENT_TEAM = 95J8WZ4TN8;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
@ -925,8 +1000,34 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(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 = s;
GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
@ -940,7 +1041,7 @@
INFOPLIST_KEY_UIRequiresFullScreen = YES;
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",
@ -1049,13 +1150,74 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(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 = xyz.belladev.MeloNX;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
SWIFT_VERSION = 5.0;
@ -1251,22 +1413,6 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.10.2;
};
};
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.5.0;
};
};
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mchoe/SwiftSVG";
@ -1278,16 +1424,6 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
256C91632D8126E300F9736D /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = 256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
isa = XCSwiftPackageProductDependency;
package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;
productName = SwiftUIJoystick;
};
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
isa = XCSwiftPackageProductDependency;
package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */;

View File

@ -1,15 +1,6 @@
{
"originHash" : "587a0e7c5c7d612a2c16a973e66df9a6a582b963cb51df7c89fd96cb28ef4a63",
"originHash" : "fedf09a893a63378a2e53f631cd833ae83a0c9ee7338eb8d153b04fd34aaf805",
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire",
"state" : {
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
"version" : "5.10.2"
}
},
{
"identity" : "swiftsvg",
"kind" : "remoteSourceControl",
@ -18,15 +9,6 @@
"branch" : "master",
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
}
},
{
"identity" : "swiftuijoystick",
"kind" : "remoteSourceControl",
"location" : "https://github.com/michael94ellis/SwiftUIJoystick",
"state" : {
"revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65",
"version" : "1.5.0"
}
}
],
"version" : 3

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array/>
</plist>

View File

@ -1,114 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A99C2CD6F54700029585"
BuildableName = "MeloNXTests.xctest"
BlueprintName = "MeloNXTests"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A9A62CD6F54700029585"
BuildableName = "MeloNXUITests.xctest"
BlueprintName = "MeloNXUITests"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
showGraphicsOverview = "Yes"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Debug"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Debug"
revealArchiveInOrganizer = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/set_current_version.sh&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;"
shellToInvoke = "/bin/bash">
</ActionContent>
</ExecutionAction>
</PreActions>
</ArchiveAction>
</Scheme>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "1.7">
LastUpgradeVersion = "1620"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
@ -62,10 +62,13 @@
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugXPCServices = "NO"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
showGraphicsOverview = "Yes"
allowLocationSimulation = "YES">
allowLocationSimulation = "YES"
queueDebuggingEnabled = "No"
consoleMode = "0"
structuredConsoleMode = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
@ -105,8 +108,17 @@
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/set_current_version.sh&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;"
scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/set_current_version.sh&quot;&#10;&#10;echo &quot;hi&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;"
shellToInvoke = "/bin/bash">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
BuildableName = "MeloNX.app"
BlueprintName = "MeloNX"
ReferencedContainer = "container:MeloNX.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>

View File

@ -11,7 +11,7 @@
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/get_dotnet.sh&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;"
scriptText = "REPO_DIR=&quot;$(cd &quot;${SRCROOT}/../../&quot; &amp;&amp; pwd)&quot;&#10;SCRIPT_PATH=&quot;$REPO_DIR/distribution/ios/get_dotnet.sh&quot;&#10;&#10;echo &quot;Xcode is located at: $DEVELOPER_DIR&quot;&#10;&#10;sh &quot;${SCRIPT_PATH}&quot;&#10;"
shellToInvoke = "/bin/bash">
<EnvironmentBuildable>
<BuildableReference

View File

@ -31,12 +31,12 @@ func SecTaskCopyValuesForEntitlements(
func checkAppEntitlements(_ ents: [String]) -> [String: Any] {
guard let task = SecTaskCreateFromSelf(nil) else {
print("Failed to create SecTask")
// print("Failed to create SecTask")
return [:]
}
guard let entitlements = SecTaskCopyValuesForEntitlements(task, ents as CFArray, nil) else {
print("Failed to get entitlements")
// print("Failed to get entitlements")
return [:]
}
@ -45,12 +45,12 @@ func checkAppEntitlements(_ ents: [String]) -> [String: Any] {
func checkAppEntitlement(_ ent: String) -> Bool {
guard let task = SecTaskCreateFromSelf(nil) else {
print("Failed to create SecTask")
// print("Failed to create SecTask")
return false
}
guard let entitlements = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
print("Failed to get entitlements")
// print("Failed to get entitlements")
return false
}

View File

@ -50,6 +50,8 @@ char* installed_firmware_version();
void set_native_window(void *layerPtr);
void pause_emulation(bool shouldPause);
void stop_emulation();
void initialize();

View File

@ -34,7 +34,7 @@ func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
}
if result != KERN_SUCCESS {
print("Failed to reach \(address)")
// print("Failed to reach \(address)")
return false
}

View File

@ -6,39 +6,121 @@
//
import Foundation
import Network
import UIKit
func enableJITEB() {
guard let bundleID = Bundle.main.bundleIdentifier else {
return
func enableJITEB() {
if UserDefaults.standard.bool(forKey: "waitForVPN") {
waitForVPNConnection { connected in
if connected {
enableJITEBRequest()
}
}
} else {
enableJITEBRequest()
}
}
func enableJITEBRequest() {
let pid = Int(getpid())
// print(pid)
let address = URL(string: "http://[fd00::]:9172/launch_app/\(bundleID)")!
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
var request = URLRequest(url: address)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: address) { data, response, error in
if error != nil {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
presentAlert(title: "Request Error", message: error.localizedDescription)
return
}
guard let httpResponse = response as? HTTPURLResponse else {
return
}
DispatchQueue.main.async {
showLaunchAppAlert(jsonData: data!, in: UIApplication.shared.windows.last!.rootViewController!)
if let data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
showLaunchAppAlert(jsonData: data, in: windowScene.windows.last!.rootViewController!)
} else {
fatalError("Unable to get Window")
}
}
return
}
task.resume()
}
func waitForVPNConnection(timeout: TimeInterval = 30, interval: TimeInterval = 1, _ completion: @escaping (Bool) -> Void) {
let startTime = Date()
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
timer.schedule(deadline: .now(), repeating: interval)
timer.setEventHandler {
pingSite { connected in
if connected {
timer.cancel()
DispatchQueue.main.async {
completion(true)
}
} else if Date().timeIntervalSince(startTime) > timeout {
timer.cancel()
DispatchQueue.main.async {
completion(false)
}
}
}
}
timer.resume()
}
func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping (Bool) -> Void) {
guard let url = URL(string: host) else {
completion(false)
return
}
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 2.0
config.timeoutIntervalForResource = 2.0
let session = URLSession(configuration: config)
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = session.dataTask(with: request) { _, response, error in
if let error = error {
// print("Ping failed: \(error.localizedDescription)")
completion(false)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
completion(true)
} else {
let httpResponse = response as? HTTPURLResponse
completion(false)
}
}
task.resume()
}
func presentAlert(title: String, message: String, completion: (() -> Void)? = nil) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let lastWindow = windowScene.windows.last {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completion?()
})
DispatchQueue.main.async {
lastWindow.rootViewController?.present(alert, animated: true)
}
}
}
struct LaunchApp: Codable {
let ok: Bool
let error: String?
let launching: Bool
let position: Int?
let mounting: Bool
let success: Bool
let message: String
}
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
@ -47,28 +129,23 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
var message = ""
if let error = result.error {
message = "Error: \(error)"
} else if result.mounting {
message = "App is mounting..."
} else if result.launching {
message = "App is launching..."
if !result.success {
message += "\n\(result.message)"
let alert = UIAlertController(title: "JIT Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
DispatchQueue.main.async {
viewController.present(alert, animated: true)
}
} else {
message = "App launch status unknown."
}
if let position = result.position {
message += "\nPosition: \(position)"
}
let alert = UIAlertController(title: "Launch Status", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
DispatchQueue.main.async {
viewController.present(alert, animated: true)
// print("Hopefully JIT is enabled now...")
Ryujinx.shared.ryuIsJITEnabled()
}
} catch {
// print(String(data: jsonData, encoding: .utf8))
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))

View File

@ -0,0 +1,19 @@
//
// EnableJIT.swift
// MeloNX
//
// Created by Stossy11 on 10/02/2025.
//
import Foundation
import Network
import UIKit
func enableJITStik() {
let bundleid = Bundle.main.bundleIdentifier ?? "Unknown"
let address = URL(string: "stikjit://enable-jit?bundle-id=\(bundleid)")!
if UIApplication.shared.canOpenURL(address) {
UIApplication.shared.open(address)
}
}

View File

@ -49,52 +49,50 @@ class NativeController: Hashable {
// 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)")
guard let userdata else { return 0 }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics)
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
}
// Open a game controller for the virtual joystick
let joystick = SDL_JoystickFromInstanceID(instanceID)
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
}
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)
@ -141,7 +139,7 @@ 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)
@ -179,7 +177,7 @@ class NativeController: Hashable {
try highFreqPlayer.start(atTime: 0.2)
} catch {
print("Error creating haptic patterns: \(error)")
// print("Error creating haptic patterns: \(error)")
}
}
@ -208,7 +206,7 @@ class NativeController: Hashable {
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

View File

@ -41,41 +41,39 @@ class VirtualController {
// 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))
}
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
}
// Open a game controller for the virtual joystick
let joystick = SDL_JoystickFromInstanceID(instanceID)
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
}
}
@ -109,7 +107,7 @@ class VirtualController {
}
guard let engine else {
return print("Error creating haptic patterns: hapticEngine is nil")
return // print("Error creating haptic patterns: hapticEngine is nil")
}
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
@ -119,7 +117,7 @@ class VirtualController {
try highFreqPlayer.start(atTime: 0)
} catch {
print("Error creating haptic patterns: \(error)")
// print("Error creating haptic patterns: \(error)")
}
}
@ -133,10 +131,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))
@ -150,7 +146,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
@ -191,6 +187,24 @@ enum VirtualControllerButton: Int {
case dPadRight
case leftTrigger
case rightTrigger
var isTrigger: Bool {
switch self {
case .leftTrigger, .rightTrigger, .leftShoulder, .rightShoulder:
return true
default:
return false
}
}
var isSmall: Bool {
switch self {
case .back, .start, .guide:
return true
default:
return false
}
}
}
enum ThumbstickType: Int {

View File

@ -35,8 +35,8 @@ class MemoryUsageMonitor: ObservableObject {
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"))
}
}

View File

@ -10,9 +10,7 @@ import Foundation
class MTLHud {
var canMetalHud: Bool {
return openMetalDylib()
}
@Published var canMetalHud: Bool = false
var isEnabled: Bool {
if let getenv = getenv("MTL_HUD_ENABLED") {
@ -24,7 +22,17 @@ class MTLHud {
static let shared = MTLHud()
private init() {
openMetalDylib()
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") {
enable()
} else {
@ -35,16 +43,15 @@ class MTLHud {
func openMetalDylib() -> Bool {
let path = "/usr/lib/libMTLHud.dylib"
// Load the dynamic library
if dlopen(path, RTLD_NOW) != nil {
// Library loaded successfully
print("Library loaded from \(path)")
// print("Library loaded from \(path)")
canMetalHud = true
return true
} else {
// Handle error
if let error = String(validatingUTF8: dlerror()) {
print("Error loading library: \(error)")
// print("Error loading library: \(error)")
}
canMetalHud = false
return false
}
}

View File

@ -11,6 +11,93 @@ import GameController
import MetalKit
import Metal
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
var name: String
@ -31,13 +118,14 @@ struct iOSNav<Content: View>: View {
}
class Ryujinx {
private var isRunning = false
class Ryujinx : ObservableObject {
@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
@ -45,6 +133,10 @@ class Ryujinx {
@Published var defMLContentSize: CGFloat?
var thread: Thread = Thread { }
@Published var jitenabled = false
var shouldMetal: Bool {
metalLayer == nil
}
@ -55,6 +147,22 @@ class Ryujinx {
self.games = loadGames()
}
func runloop(_ cool: @escaping () -> Void) {
if UserDefaults.standard.bool(forKey: "runOnMainThread") {
RunLoop.main.perform {
cool()
}
} else {
thread = Thread {
cool()
}
thread.qualityOfService = .userInteractive
thread.name = "MeloNX"
thread.start()
}
}
public struct Configuration : Codable, Equatable {
var gamepath: String
var inputids: [String]
@ -145,7 +253,8 @@ class Ryujinx {
self.config = config
RunLoop.current.perform { [self] in
runloop { [self] in
isRunning = true
@ -175,12 +284,95 @@ class Ryujinx {
}
} catch {
self.isRunning = false
Self.log("Emulation failed to start: \(error)")
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) {
assert(true, parsedLogs.exceptionType)
}
}
} else {
DispatchQueue.main.async {
presentAlert(title: "MeloNX Crashed!", message: "Unknown Error") {
assert(true, "Exception was not detected")
}
}
}
}
}
}
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..<logs.count).reversed() {
let line = logs[i]
let pattern = "([\\w\\.]+Exception): ([^\\s]+(?:\\s+[^\\s]+)*)"
guard let regex = try? NSRegularExpression(pattern: pattern, options: []),
let match = regex.firstMatch(in: line, options: [], range: NSRange(location: 0, length: line.count)) else {
continue
}
// Extract exception type and message if pattern matches
if let exceptionTypeRange = Range(match.range(at: 1), in: line),
let messageRange = Range(match.range(at: 2), in: line) {
let exceptionType = String(line[exceptionTypeRange])
var message = String(line[messageRange])
if let atIndex = message.range(of: "\\s+at\\s+", options: .regularExpression) {
message = String(message[..<atIndex.lowerBound])
}
message = message.trimmingCharacters(in: .whitespacesAndNewlines)
return ExceptionInfo(exceptionType: exceptionType, message: message, lineIndex: i)
}
}
return nil
}
func stop() throws {
guard isRunning else {
throw RyujinxError.notRunning
@ -191,13 +383,9 @@ class Ryujinx {
self.emulationUIView = nil
self.metalLayer = nil
stop_emulation()
thread.cancel()
}
var running: Bool {
return isRunning
}
func loadGames() -> [Game] {
let fileManager = FileManager.default
@ -209,7 +397,7 @@ class Ryujinx {
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] = []
@ -234,13 +422,13 @@ class Ryujinx {
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
}
@ -264,12 +452,31 @@ class Ryujinx {
// 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])
if config.nintendoinput {
args.append("--correct-controller")
}
@ -360,18 +567,13 @@ class Ryujinx {
}
func fetchFirmwareVersion() -> String {
do {
let firmwareVersionPointer = installed_firmware_version()
if let pointer = firmwareVersionPointer {
let firmwareVersion = String(cString: pointer)
DispatchQueue.main.async {
self.firmwareversion = firmwareVersion
}
return firmwareVersion
let firmwareVersionPointer = installed_firmware_version()
if let pointer = firmwareVersionPointer {
let firmwareVersion = String(cString: pointer)
DispatchQueue.main.async {
self.firmwareversion = firmwareVersion
}
} catch {
print(error)
return firmwareVersion
}
return "0"
@ -379,7 +581,7 @@ class Ryujinx {
func installFirmware(firmwarePath: String) {
guard let cString = firmwarePath.cString(using: .utf8) else {
print("Invalid firmware path")
// print("Invalid firmware path")
return
}
@ -395,12 +597,12 @@ class Ryujinx {
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)))
@ -452,7 +654,7 @@ class Ryujinx {
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)
@ -483,84 +685,163 @@ class Ryujinx {
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)")
}
}
func repeatuntilfindLayer() {
Task { @MainActor in
while self.metalLayer == nil {
let layer = self.getMetalLayer(nil)
if layer != nil {
self.metalLayer = layer
break
}
Thread.sleep(forTimeInterval: 0.1)
}
}
}
@MainActor
func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? {
var window = window
if window == nil {
window = SDL_GetWindowFromID(1)
}
var windowInfo = SDL_SysWMinfo()
SDL_GetWindowWMInfo(window, &windowInfo)
guard let uiWindow = windowInfo.info.uikit.window,
let rootView = uiWindow.takeUnretainedValue().rootViewController?.view else {
print("Unable to get root view")
return nil
}
func findMetalLayer(in view: UIView) -> CAMetalLayer? {
if let metalLayer = view.layer as? CAMetalLayer {
return metalLayer
}
for subview in view.subviews {
if let metalLayer = findMetalLayer(in: subview) {
return metalLayer
}
}
return nil
}
if let existingLayer = findMetalLayer(in: rootView) {
print("Found Metal Layer")
return existingLayer
}
print("found nothing")
return nil
}
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()
}
}
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)
}()
}

View File

@ -32,10 +32,10 @@ struct LaunchGameIntentDef: AppIntent {
let ryujinx = Ryujinx.shared.games
let name = findClosestGameName(input: gameName, games: ryujinx.flatMap(\.titleName))
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)
}

View File

@ -57,28 +57,21 @@ public struct Game: Identifiable, Equatable, Hashable {
gameTemp.icon = UIImage(data: imageData)
} else {
print("Invalid image size.")
// print("Invalid image size.")
}
return gameTemp
}
func createImage(from gameInfo: GameInfo) -> UIImage? {
// Access the struct
let gameInfoValue = gameInfo
// Get the image data
let imageSize = Int(gameInfoValue.ImageSize)
guard imageSize > 0, imageSize <= 1024 * 1024 else {
print("Invalid image size.")
// print("Invalid image size.")
return nil
}
// Convert the ImageData byte array to Swift's Data
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
// Create a UIImage (or NSImage on macOS)
print(imageData)
return UIImage(data: imageData)
}
}

View File

@ -5,6 +5,7 @@
// Created by Bella on 12/03/2025.
//
struct LatestVersionResponse: Codable {
let version_number: String
let version_number_stripped: String
@ -34,4 +35,4 @@ struct LatestVersionResponse: Codable {
download_link: "https://example.com"
)
#endif
}
}

View File

@ -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)
}

View File

@ -0,0 +1,47 @@
//
// AppCodableStorage.swift
// MeloNX
//
// Created by Stossy11 on 12/04/2025.
//
import SwiftUI
@propertyWrapper
struct AppCodableStorage<Value: Codable & Equatable>: 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<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in self.wrappedValue = newValue }
)
}
}

View File

@ -1,390 +0,0 @@
//
// ContentView.swift
// MeloNX
//
// Created by Stossy11 on 3/11/2024.
//
import SwiftUI
// import SDL2
import GameController
import Darwin
import UIKit
import MetalKit
// import SDL
struct MoltenVKSettings: Codable, Hashable {
let string: String
var value: String
}
struct ContentView: View {
// Games
@State private var game: Game?
// Controllers
@State private var controllersList: [Controller] = []
@State private var currentControllers: [Controller] = []
@State var onscreencontroller: Controller = Controller(id: "", name: "")
@State var nativeControllers: [GCController: NativeController] = [:]
@State private var isVirtualControllerActive: Bool = false
@AppStorage("isVirtualController") var isVCA: Bool = true
// Settings and Configuration
@State private var config: Ryujinx.Configuration
@State var settings: [MoltenVKSettings]
@AppStorage("useTrollStore") var useTrollStore: Bool = false
// JIT
@AppStorage("jitStreamerEB") var jitStreamerEB: 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("ignoreJIT") var ignoreJIT: Bool = false
// Loading Animation
@AppStorage("showlogsloading") var showlogsloading: Bool = true
@State private var clumpOffset: CGFloat = -100
private let clumpWidth: CGFloat = 100
private let animationDuration: Double = 1.0
@State private var isAnimating = false
@State var isLoading = true
@State var jitNotEnabled = false
// MARK: - Initialization
init() {
var defaultConfig = loadSettings()
if defaultConfig == nil {
saveSettings(config: .init(gamepath: ""))
defaultConfig = loadSettings()
}
_config = State(initialValue: defaultConfig!)
let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings.
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
MoltenVKSettings(string: "MVK_CONFIG_LOG_LEVEL", value: "0"),
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"),
// MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"),
// Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64)
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"),
]
_settings = State(initialValue: defaultSettings)
initializeSDL()
}
// MARK: - Body
var body: some View {
if game != nil, !jitNotEnabled {
// This is when the game starts to stop the animation
ZStack {
if #available(iOS 16, *) {
EmulationView(startgame: $game)
.persistentSystemOverlays(.hidden)
} else {
EmulationView(startgame: $game)
}
if isLoading {
ZStack {
Color.black
.opacity(0.8)
emulationView
.ignoresSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea(.all)
}
}
} else if game != nil, ignoreJIT {
ZStack {
if #available(iOS 16, *) {
EmulationView(startgame: $game)
.persistentSystemOverlays(.hidden)
} else {
EmulationView(startgame: $game)
}
if isLoading {
ZStack {
Color.black
.opacity(0.8)
emulationView
.ignoresSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea(.all)
}
}
} else if game != nil {
Text("")
.sheet(isPresented: $jitNotEnabled) {
JITPopover() {
jitNotEnabled = false
}
.interactiveDismissDisabled()
}
} else {
// This is the main menu view that includes the Settings and the Game Selector
mainMenuView
.onAppear() {
quits = false
loadSettings()
isLoading = true
initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
}
.onOpenURL() { url in
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" {
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleName == text })
}
}
}
}
}
private func initControllerObservers() {
NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main) { notification in
if let controller = notification.object as? GCController {
print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller)
refreshControllersList()
}
}
NotificationCenter.default.addObserver(
forName: .GCControllerDidDisconnect,
object: nil,
queue: .main) { notification in
if let controller = notification.object as? GCController {
print("Controller disconnected: \(controller.productCategory)")
nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil
refreshControllersList()
}
}
}
// MARK: - View Components
private var emulationView: some View {
GeometryReader { screenGeometry in
ZStack {
HStack(spacing: screenGeometry.size.width * 0.04) {
if let icon = game?.icon {
Image(uiImage: icon)
.resizable()
.frame(
width: min(screenGeometry.size.width * 0.25, 250),
height: min(screenGeometry.size.width * 0.25, 250)
)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 5)
}
VStack(alignment: .leading, spacing: screenGeometry.size.height * 0.015) {
Text("Loading \(game?.titleName ?? "Game")")
.font(.system(size: min(screenGeometry.size.width * 0.04, 32)))
.foregroundColor(.white)
GeometryReader { geometry in
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
ZStack(alignment: .leading) {
Rectangle()
.cornerRadius(10)
.frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
.foregroundColor(.gray.opacity(0.3))
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
Rectangle()
.cornerRadius(10)
.frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
.foregroundColor(.blue)
.shadow(color: .blue.opacity(0.5), radius: 4, x: 0, y: 2)
.offset(x: isAnimating ? containerWidth : -clumpWidth)
.animation(
Animation.linear(duration: 1.0)
.repeatForever(autoreverses: false),
value: isAnimating
)
}
.clipShape(RoundedRectangle(cornerRadius: 16))
.onAppear {
isAnimating = true
setupEmulation()
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if get_current_fps() != 0 {
withAnimation {
isLoading = false
isAnimating = false
}
timer.invalidate()
}
}
}
}
.frame(height: min(screenGeometry.size.height * 0.015, 12))
.frame(width: min(screenGeometry.size.width * 0.35, 350))
}
}
.padding(.horizontal, screenGeometry.size.width * 0.06)
.padding(.vertical, screenGeometry.size.height * 0.05)
.position(
x: screenGeometry.size.width / 2,
y: screenGeometry.size.height * 0.5
)
}
if showlogsloading {
LogFileView(isfps: true)
.frame(alignment: .topLeading)
}
}
}
private var mainMenuView: some View {
MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
refreshControllersList()
}
Air.play(AnyView(
VStack {
Image(systemName: "gamecontroller")
.font(.system(size: 300))
.foregroundColor(.gray)
.padding(.bottom, 10)
Text("Select Game")
.font(.system(size: 150))
.bold()
}
))
jitNotEnabled = !isJITEnabled()
if jitNotEnabled {
useTrollStore ? askForJIT() : jitStreamerEB ? enableJITEB() : print("no JIT")
}
}
}
// MARK: - Helper Methods
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; // Initialises SDL2 for Events, Game Controller, Joystick, Audio and Video.
private func initializeSDL() {
setMoltenVKSettings()
SDL_SetMainReady() // Sets SDL Ready
SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true
SDL_Init(SdlInitFlags) // Initialises SDL2
initialize()
}
private func setupEmulation() {
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
DispatchQueue.main.async {
start(displayid: 1)
}
}
private func refreshControllersList() {
controllersList = Ryujinx.shared.getConnectedControllers()
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
self.onscreencontroller = onscreen
}
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 {
let controller = controllersList[0]
currentControllers.append(controller)
} else if (controllersList.count - 1) >= 1 {
for controller in controllersList {
if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
currentControllers.append(controller)
}
}
}
}
private func start(displayid: UInt32) {
guard let game else { return }
config.gamepath = game.fileURL.path
config.inputids = Array(Set(currentControllers.map(\.id)))
if mVKPreFillBuffer {
let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2")
setenv(setting.string, setting.value, 1)
}
if syncqsubmits {
let setting = MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "2")
setenv(setting.string, setting.value, 1)
}
if config.inputids.isEmpty {
config.inputids.append("0")
}
do {
try Ryujinx.shared.start(with: config)
} catch {
print("Error: \(error.localizedDescription)")
}
}
// Sets MoltenVK Environment Variables
private func setMoltenVKSettings() {
settings.forEach { setting in
setenv(setting.string, setting.value, 1)
}
}
}
extension Array {
@inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
for index in self.indices {
try body(&self[index])
}
}
}

View File

@ -1,290 +0,0 @@
//
// ControllerView.swift
// Pomelo-V2
//
// Created by Stossy11 on 16/7/2024.
//
import SwiftUI
import GameController
import SwiftUIJoystick
import CoreMotion
struct ControllerView: View {
var body: some View {
GeometryReader { geometry in
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
VStack {
Spacer()
VStack {
HStack {
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
Spacer()
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this works
ABXYView()
}
}
}
HStack {
ButtonView(button: .start) // Adding the + button
.padding(.horizontal, 40)
ButtonView(button: .back) // Adding the - button
.padding(.horizontal, 40)
}
}
}
} else {
// could be landscape
VStack {
Spacer()
VStack {
HStack {
// gotta fuckin add + and - now
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
HStack {
// Spacer()
VStack {
// Spacer()
ButtonView(button: .back) // Adding the - button
}
Spacer()
VStack {
// Spacer()
ButtonView(button: .start) // Adding the + button
}
// Spacer()
}
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this work s
ABXYView()
}
}
}
}
// .padding(.bottom, geometry.size.height / 11) // also extremally broken (
}
}
}
.padding()
}
}
struct ShoulderButtonsViewLeft: View {
@State var width: CGFloat = 160
@State var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack {
ButtonView(button: .leftTrigger)
.padding(.horizontal)
ButtonView(button: .leftShoulder)
.padding(.horizontal)
}
.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 var width: CGFloat = 160
@State var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack {
ButtonView(button: .rightShoulder)
.padding(.horizontal)
ButtonView(button: .rightTrigger)
.padding(.horizontal)
}
.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 var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack {
ButtonView(button: .dPadUp)
HStack {
ButtonView(button: .dPadLeft)
Spacer(minLength: 20)
ButtonView(button: .dPadRight)
}
ButtonView(button: .dPadDown)
.padding(.horizontal)
}
.frame(width: size, height: size)
.onAppear() {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
size *= CGFloat(controllerScale)
}
}
}
struct ABXYView: View {
@State var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack {
ButtonView(button: .X)
HStack {
ButtonView(button: .Y)
Spacer(minLength: 20)
ButtonView(button: .A)
}
ButtonView(button: .B)
.padding(.horizontal)
}
.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 var width: CGFloat = 45
@State var height: CGFloat = 45
@State var isPressed = false
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
Image(systemName: buttonText)
.resizable()
.frame(width: width, height: height)
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
.opacity(isPressed ? 0.4 : 0.7)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
if !self.isPressed {
self.isPressed = true
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.heavy)
}
}
.onEnded { _ in
self.isPressed = false
Ryujinx.shared.virtualController.setButtonState(0, for: button)
}
)
.onAppear() {
if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder {
width = 65
}
if button == .back || button == .start || button == .guide {
width = 35
height = 35
}
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 .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" // System symbol for +
case .back:
return "minus.circle.fill" // System symbol for -
case .guide:
return "house.circle.fill"
// This should be all the cases
default:
return ""
}
}
}

View File

@ -1,56 +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
@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(Color.gray)
.opacity(0.7)
},
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)
}
}
}
}
}

View File

@ -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())
}
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -0,0 +1,436 @@
//
// 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()
ZStack {
JoystickController(showBackground: $hideDpad)
if !hideDpad {
DPadView()
.animation(.easeInOut(duration: 0.2), value: hideDpad)
}
}
}
VStack(spacing: 15) {
ShoulderButtonsViewRight()
ZStack {
JoystickController(iscool: true, showBackground: $hideABXY)
if !hideABXY {
ABXYView()
.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()
ZStack {
JoystickController(showBackground: $hideDpad)
if !hideDpad {
DPadView()
.animation(.easeInOut(duration: 0.2), value: hideDpad)
}
}
}
Spacer()
centerButtons
Spacer()
VStack(spacing: 20) {
ShoulderButtonsViewRight()
ZStack {
JoystickController(iscool: true, showBackground: $hideABXY)
if !hideABXY {
ABXYView()
.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 : 1.0)
.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
}
}

View File

@ -15,7 +15,6 @@ class Haptics {
private init() { }
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
print("haptics")
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
}

View File

@ -0,0 +1,87 @@
//
// Joystick.swift
// MeloNX
//
// Created by Stossy11 on 21/03/2025.
//
import SwiftUI
struct Joystick: View {
@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.5
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)
if showBackground {
Circle()
.fill(Color.gray.opacity(0.4))
.frame(width: boundarySize, height: boundarySize)
.animation(.easeInOut(duration: 0.1), value: showBackground)
}
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)
}
.frame(width: boundarySize, height: boundarySize)
.onChange(of: showBackground) { newValue in
if newValue {
joystickSize *= 1.4
} else {
joystickSize = (boundarySize * 0.2)
}
}
}
}

View File

@ -0,0 +1,43 @@
//
// 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
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)
}
}
}
}
}

View File

@ -19,6 +19,9 @@ struct EmulationView: View {
@Binding var startgame: Game?
@Environment(\.scenePhase) var scenePhase
@State private var isInBackground = false
@AppStorage("location-enabled") var locationenabled: Bool = false
var body: some View {
ZStack {
if isAirplaying {
@ -26,7 +29,7 @@ struct EmulationView: View {
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
.onAppear {
Air.play(AnyView(MetalView().ignoresSafeArea()))
Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
}
} else {
MetalView() // The Emulation View
@ -62,38 +65,51 @@ struct EmulationView: View {
Spacer()
}
Spacer()
if ssb {
HStack {
Button {
if let screenshot = Ryujinx.shared.emulationUIView?.screenshot() {
UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
Image(systemName: "arrow.left.circle")
.resizable()
.frame(width: 50, height: 50)
.onTapGesture {
startgame = nil
stop_emulation()
try? Ryujinx.shared.stop()
}
} label: {
Image(systemName: "square.and.arrow.up")
}
.frame(width: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45, height: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45)
.padding()
.padding()
Spacer()
}
}
Spacer()
}
}
}
.onAppear {
LocationManager.sharedInstance.startUpdatingLocation()
Air.shared.connectionCallbacks.append { cool in
DispatchQueue.main.async {
isAirplaying = cool
print(cool)
// print(cool)
}
}
}
.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
}
}
}
}

View File

@ -87,40 +87,48 @@ class MeloMTKView: MTKView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
for touch in touches {
let location = touch.location(in: self)
if scaleToTargetResolution(location) == nil {
ignoredTouches.insert(touch)
continue
if !disabled {
for touch in touches {
let location = touch.location(in: self)
if scaleToTargetResolution(location) == nil {
ignoredTouches.insert(touch)
continue
}
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))
}
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<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
for touch in touches {
if ignoredTouches.contains(touch) {
ignoredTouches.remove(touch)
continue
}
if let index = activeTouches.firstIndex(of: touch) {
activeTouches.remove(at: index)
if !disabled {
for touch in touches {
if ignoredTouches.contains(touch) {
ignoredTouches.remove(touch)
continue
}
print("Touch ended for index \(index)")
touch_ended(Int32(index))
if let index = activeTouches.firstIndex(of: touch) {
activeTouches.remove(at: index)
// // print("Touch ended for index \(index)")
touch_ended(Int32(index))
}
}
}
}
@ -128,26 +136,30 @@ class MeloMTKView: MTKView {
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
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))
if !disabled {
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))
}
continue
}
if let index = activeTouches.firstIndex(of: touch) {
// // print("Touch moved to: \(scaledLocation)")
touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
}
continue
}
if let index = activeTouches.firstIndex(of: touch) {
print("Touch moved to: \(scaledLocation)")
touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
}
}
}

View File

@ -15,7 +15,7 @@ struct MetalView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
if Ryujinx.shared.emulationUIView == nil {
var view = MeloMTKView()
let view = MeloMTKView()
guard let metalLayer = view.layer as? CAMetalLayer else {
fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer")
@ -34,13 +34,19 @@ struct MetalView: UIViewRepresentable {
return view
}
let uiview = UIView()
uiview.layer.addSublayer(Ryujinx.shared.metalLayer!)
uiview.contentScaleFactor = Ryujinx.shared.metalLayer!.contentsScale
return uiview
if Double(UIDevice.current.systemVersion)! < 17.0 {
let uiview = MTKView()
let layer = Ryujinx.shared.metalLayer!
layer.frame = uiview.bounds
uiview.layer.addSublayer(layer)
return uiview
} else {
return Ryujinx.shared.emulationUIView!
}
}
func updateUIView(_ uiView: UIView, context: Context) {

View File

@ -10,7 +10,7 @@ import MetalKit
struct TouchView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
var view = MeloMTKView()
let view = MeloMTKView()
return view
}

View File

@ -1,504 +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
@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)
}
}
}
.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<CChar>(mutating: fileExtension)
var 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)")
}
}
}

View File

@ -1,117 +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
self.readLatestLogFile()
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
if showingLogs {
self.readLatestLogFile()
}
if isfps {
if get_current_fps() != 0 {
stopLogFileWatching()
timer.invalidate()
}
}
}
}
private func stopLogFileWatching() {
showingLogs = false
}
}

View File

@ -1,765 +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
@State private var showResolutionInfo = false
@State private var showAnisotropicInfo = false
@State private var showControllerInfo = false
@State private var searchText = ""
@AppStorage("portal") var gamepo = false
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("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<Bool> {
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<Bool> {
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: $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 {
if let mainWindow = UIApplication.shared.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 iOS 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 mainWindow = UIApplication.shared.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: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
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) / (1024 * 1024 * 1024)))", 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: $mVKPreFillBuffer) {
labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape")
}.tint(.blue)
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)
}
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. (Please don't touch this if you don't know what you're doing). \n \n\(gamepo ? "the cake is a lie" : "")")
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.listStyle(.insetGrouped)
.onAppear {
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
}
}

View File

@ -0,0 +1,543 @@
//
// ContentView.swift
// MeloNX
//
// Created by Stossy11 on 3/11/2024.
//
import SwiftUI
import GameController
import Darwin
import UIKit
import MetalKit
import CoreLocation
struct MoltenVKSettings: Codable, Hashable {
let string: String
var value: String
}
struct ContentView: View {
// MARK: - Properties
// Games
@State private var game: Game?
// Controllers
@State private var controllersList: [Controller] = []
@State private var currentControllers: [Controller] = []
@State var onscreencontroller: Controller = Controller(id: "", name: "")
@State var nativeControllers: [GCController: NativeController] = [:]
@State private var isVirtualControllerActive: Bool = false
@AppStorage("isVirtualController") var isVCA: Bool = true
// Settings and Configuration
@State private var config: Ryujinx.Configuration
@State var settings: [MoltenVKSettings]
@AppStorage("useTrollStore") var useTrollStore: Bool = false
// JIT
@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 = false
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
// Loading Animation
@AppStorage("showlogsloading") var showlogsloading: Bool = true
@State private var clumpOffset: CGFloat = -100
private let clumpWidth: CGFloat = 100
private let animationDuration: Double = 1.0
@State private var isAnimating = false
@State var isLoading = true
@StateObject var ryujinx = Ryujinx.shared
// MARK: - SDL
var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO
// 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"),
MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"),
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"),
]
_settings = State(initialValue: defaultSettings)
// print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
initializeSDL()
}
// MARK: - Body
var body: some View {
if game != nil && (ryujinx.jitenabled || ignoreJIT) {
gameView
} else if game != nil && !ryujinx.jitenabled {
jitErrorView
} else {
mainMenuView
}
}
// MARK: - View Components
private var gameView: some View {
ZStack {
if #available(iOS 16, *) {
EmulationView(startgame: $game)
.persistentSystemOverlays(.hidden)
} else {
EmulationView(startgame: $game)
}
if isLoading {
ZStack {
Color.black.opacity(0.8)
emulationView.ignoresSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea(.all)
}
}
}
private var jitErrorView: some View {
Text("")
.fullScreenCover(isPresented:Binding(
get: { !ryujinx.jitenabled },
set: { newValue in
ryujinx.jitenabled = newValue
ryujinx.ryuIsJITEnabled()
})
) {
JITPopover() {
ryujinx.jitenabled = false
}
// .interactiveDismissDisabled()
}
}
private var mainMenuView: some View {
MainTabView(
startemu: $game,
config: $config,
MVKconfig: $settings,
controllersList: $controllersList,
currentControllers: $currentControllers,
onscreencontroller: $onscreencontroller
)
.onAppear {
quits = false
let _ = loadSettings()
isLoading = true
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
refreshControllersList()
}
// print(MTLHud.shared.isEnabled)
initControllerObservers()
Air.play(AnyView(
ControllerListView(game: $game)
))
checkJitStatus()
}
.onOpenURL { url in
handleDeepLink(url)
}
}
private var emulationView: some View {
GeometryReader { screenGeometry in
ZStack {
gameLoadingContent(screenGeometry: screenGeometry)
HStack{
VStack {
if showlogsloading {
LogFileView(isfps: true)
.frame(alignment: .topLeading)
}
Spacer()
}
Spacer()
}
}
}
}
// MARK: - Helper Methods
private func gameLoadingContent(screenGeometry: GeometryProxy) -> some View {
HStack(spacing: screenGeometry.size.width * 0.04) {
if let icon = game?.icon {
Image(uiImage: icon)
.resizable()
.frame(
width: min(screenGeometry.size.width * 0.25, 250),
height: min(screenGeometry.size.width * 0.25, 250)
)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 5)
}
VStack(alignment: .leading, spacing: screenGeometry.size.height * 0.015) {
Text("Loading \(game?.titleName ?? "Game")")
.font(.system(size: min(screenGeometry.size.width * 0.04, 32)))
.foregroundColor(.white)
loadingProgressBar(screenGeometry: screenGeometry)
}
}
.padding(.horizontal, screenGeometry.size.width * 0.06)
.padding(.vertical, screenGeometry.size.height * 0.05)
.position(
x: screenGeometry.size.width / 2,
y: screenGeometry.size.height * 0.5
)
}
private func loadingProgressBar(screenGeometry: GeometryProxy) -> some View {
GeometryReader { geometry in
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
ZStack(alignment: .leading) {
Rectangle()
.cornerRadius(10)
.frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
.foregroundColor(.gray.opacity(0.3))
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
Rectangle()
.cornerRadius(10)
.frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
.foregroundColor(.blue)
.shadow(color: .blue.opacity(0.5), radius: 4, x: 0, y: 2)
.offset(x: isAnimating ? containerWidth : -clumpWidth)
.animation(
Animation.linear(duration: 1.0)
.repeatForever(autoreverses: false),
value: isAnimating
)
}
.clipShape(RoundedRectangle(cornerRadius: 16))
.onAppear {
isAnimating = true
setupEmulation()
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if get_current_fps() != 0 {
withAnimation {
isLoading = false
isAnimating = false
}
timer.invalidate()
}
}
}
}
.frame(height: min(screenGeometry.size.height * 0.015, 12))
.frame(width: min(screenGeometry.size.width * 0.35, 350))
}
private func initializeSDL() {
setMoltenVKSettings()
SDL_SetMainReady()
SDL_iPhoneSetEventPump(SDL_TRUE)
SDL_Init(sdlInitFlags)
initialize()
}
private func initControllerObservers() {
NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main
) { notification in
if let controller = notification.object as? GCController {
// print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller)
refreshControllersList()
}
}
NotificationCenter.default.addObserver(
forName: .GCControllerDidDisconnect,
object: nil,
queue: .main
) { notification in
if let controller = notification.object as? GCController {
// print("Controller disconnected: \(controller.productCategory)")
nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil
refreshControllersList()
}
}
}
private func setupEmulation() {
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
DispatchQueue.main.async {
start(displayid: 1)
}
}
private func refreshControllersList() {
controllersList = ryujinx.getConnectedControllers()
if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
self.onscreencontroller = onscreen
}
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])
} else if (controllersList.count - 1) >= 1 {
for controller in controllersList {
if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
currentControllers.append(controller)
}
}
}
}
private func start(displayid: UInt32) {
guard let game else { return }
config.gamepath = game.fileURL.path
config.inputids = Array(Set(currentControllers.map(\.id)))
configureEnvironmentVariables()
if config.inputids.isEmpty {
config.inputids.append("0")
}
do {
try ryujinx.start(with: config)
} catch {
// print("Error: \(error.localizedDescription)")
}
}
private func configureEnvironmentVariables() {
if mVKPreFillBuffer {
mVKPreFillBuffer = false
// setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1)
}
if syncqsubmits {
setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "1", 1)
}
}
private func setMoltenVKSettings() {
settings.forEach { setting in
setenv(setting.string, setting.value, 1)
}
}
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")
}
}
}
private func handleDeepLink(_ url: URL) {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" {
DispatchQueue.main.async {
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 {
game = ryujinx.games.first(where: { $0.titleName == text })
}
}
}
}
}
extension Array {
@inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
for index in self.indices {
try body(&self[index])
}
}
}
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()
}
}
}

View File

@ -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)
}
}

View File

@ -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
}

File diff suppressed because it is too large Load Diff

View File

@ -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,8 +35,10 @@ struct JITPopover: View {
if isJIT {
dismiss()
presentationMode.wrappedValue.dismiss()
onJITEnabled()
Ryujinx.shared.ryuIsJITEnabled()
}
}
}

View File

@ -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<AnyCancellable>()
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()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
// MeloNXUpdateSheet.swift
// MeloNX
//
// Created by Bella on 12/03/2025.
// Created by Stossy11 and Bella on 12/03/2025.
//
import SwiftUI
@ -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!")
@ -58,7 +72,3 @@ struct MeloNXUpdateSheet: View {
}
}
}
#Preview {
MeloNXUpdateSheet(updateInfo: LatestVersionResponse.example1, isPresented: .constant(true))
}

View File

@ -0,0 +1,337 @@
//
// GameDLCManagerSheet.swift
// MeloNX
//
// Created by XITRIX on 16/02/2025.
//
import SwiftUI
import UniformTypeIdentifiers
// MARK: - Models
struct DownloadableContentNca: Codable, Hashable {
var fullPath: String
var titleId: UInt
var enabled: Bool
enum CodingKeys: String, CodingKey {
case fullPath = "path"
case titleId = "title_id"
case enabled = "is_enabled"
}
}
struct DownloadableContentContainer: Codable, Hashable, Identifiable {
var id: String { containerPath }
var containerPath: String
var downloadableContentNcaList: [DownloadableContentNca]
var filename: String {
(containerPath as NSString).lastPathComponent
}
var isEnabled: Bool {
downloadableContentNcaList.first?.enabled == true
}
enum CodingKeys: String, CodingKey {
case containerPath = "path"
case downloadableContentNcaList = "dlc_nca_list"
}
}
// MARK: - View
struct DLCManagerSheet: View {
// MARK: - Properties
@Binding var game: Game!
@State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = []
@Environment(\.presentationMode) var presentationMode
// MARK: - Body
var body: some View {
iOSNav {
List {
if dlcs.isEmpty {
emptyStateView
} else {
ForEach(dlcs) { dlc in
dlcRow(dlc)
}
.onDelete(perform: removeDLCs)
}
}
.navigationTitle("\(game.titleName) DLCs")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isSelectingGameDLC = true
} label: {
Label("Add DLC", systemImage: "plus")
}
}
}
.onAppear {
loadData()
}
}
.fileImporter(
isPresented: $isSelectingGameDLC,
allowedContentTypes: [.item],
allowsMultipleSelection: true,
onCompletion: handleFileImport
)
}
// MARK: - Views
private var emptyStateView: some View {
Group {
if #available(iOS 17, *) {
ContentUnavailableView(
"No DLCs Found",
systemImage: "puzzlepiece.extension",
description: Text("Tap the + button to add game DLCs.")
)
} else {
VStack(spacing: 20) {
Spacer()
Image(systemName: "puzzlepiece.extension")
.font(.system(size: 64))
.foregroundColor(.secondary)
Text("No DLCs Found")
.font(.title2)
.fontWeight(.semibold)
Text("Tap the + button to add game DLCs.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Spacer()
}
.frame(maxWidth: .infinity)
.listRowInsets(EdgeInsets())
}
}
}
private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
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)
}
}
}
}
}
// MARK: - Functions
private func loadData() {
dlcs = Self.loadDlc(game)
}
private func toggleDLC(_ dlc: DownloadableContentContainer) {
guard let index = dlcs.firstIndex(where: { $0.id == dlc.id }) else { return }
let toggle = !dlcs[index].isEnabled
dlcs[index].downloadableContentNcaList = dlcs[index].downloadableContentNcaList.map { nca in
var mutableNca = nca
mutableNca.enabled = toggle
return mutableNca
}
Self.saveDlcs(game, dlc: dlcs)
}
private func removeDLCs(at offsets: IndexSet) {
offsets.forEach { removeDLC(at: IndexSet(integer: $0)) }
}
private func removeDLC(at indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let dlcToRemove = dlcs[index]
let path = URL.documentsDirectory.appendingPathComponent(dlcToRemove.containerPath)
do {
try FileManager.default.removeItem(at: path)
dlcs.remove(at: index)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Failed to remove DLC: \(error)")
}
}
private func handleFileImport(result: Result<[URL], Error>) {
switch result {
case .success(let urls):
for url in urls {
importDLC(from: url)
}
case .failure(let error):
print("File import failed: \(error.localizedDescription)")
}
}
private func importDLC(from url: URL) {
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let dlcDirectory = URL.documentsDirectory.appendingPathComponent("dlc")
let gameDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
try fileManager.createDirectory(at: gameDlcDirectory, withIntermediateDirectories: true)
// Copy the DLC file
let destinationURL = gameDlcDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.removeItem(at: destinationURL)
try fileManager.copyItem(at: url, to: destinationURL)
// Fetch DLC metadata from Ryujinx
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: destinationURL.path)
guard !dlcContent.isEmpty else {
print("No valid DLC content found")
return
}
let newDlcContainer = DownloadableContentContainer(
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
downloadableContentNcaList: dlcContent
)
dlcs.append(newDlcContainer)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Error importing DLC: \(error)")
}
}
}
// MARK: - Helper Methods
private extension DLCManagerSheet {
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
let jsonURL = dlcJsonPath(for: game)
do {
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
guard FileManager.default.fileExists(atPath: jsonURL.path),
let data = try? Data(contentsOf: jsonURL),
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
else { return [] }
result = result.filter { container in
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
return FileManager.default.fileExists(atPath: path.path)
}
return result
} catch {
// print("Error loading DLCs: \(error)")
return []
}
}
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
do {
let data = try JSONEncoder().encode(dlc)
try data.write(to: dlcJsonPath(for: game))
} catch {
print("Error saving DLCs: \(error)")
}
}
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
}
static func dlcJsonPath(for game: Game) -> URL {
URL.documentsDirectory
.appendingPathComponent("games")
.appendingPathComponent(game.titleId)
.appendingPathComponent("dlc.json")
}
}
// MARK: - Array Extension
extension Array where Element: AnyObject {
mutating func mutableForEach(_ body: (inout Element) -> Void) {
for index in indices {
var element = self[index]
body(&element)
self[index] = element
}
}
}
// MARK: - URL Extension
extension URL {
@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
}
}

View File

@ -0,0 +1,335 @@
//
// GameUpdateManagerSheet.swift
// MeloNX
//
// Created by Stossy11 on 16/02/2025.
//
import SwiftUI
import UniformTypeIdentifiers
struct UpdateManagerSheet: View {
// MARK: - Properties
@State private var updates: [UpdateItem] = []
@Binding var game: Game?
@State private var isSelectingGameUpdate = false
@State private var jsonURL: URL? = nil
@Environment(\.presentationMode) var presentationMode
// MARK: - Models
class UpdateItem: Identifiable, ObservableObject {
let id = UUID()
let url: URL
let filename: String
let path: String
@Published var isSelected: Bool = false
init(url: URL, filename: String, path: String, isSelected: Bool = false) {
self.url = url
self.filename = filename
self.path = path
self.isSelected = isSelected
}
}
// MARK: - Body
var body: some View {
iOSNav {
List {
if updates.isEmpty {
emptyStateView
} else {
ForEach(updates) { update in
updateRow(update)
}
.onDelete(perform: removeUpdates)
}
}
.navigationTitle("\(game?.titleName ?? "Game") Updates")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isSelectingGameUpdate = true
} label: {
Label("Add Update", systemImage: "plus")
}
}
}
.onAppear {
loadData()
}
}
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item], onCompletion: handleFileImport)
}
// MARK: - Views
private var emptyStateView: some View {
Group {
if #available(iOS 17, *) {
ContentUnavailableView(
"No Updates Found",
systemImage: "arrow.down.circle",
description: Text("Tap the + button to add game updates.")
)
} else {
VStack(spacing: 20) {
Spacer()
Image(systemName: "arrow.down.circle")
.font(.system(size: 64))
.foregroundColor(.secondary)
Text("No Updates Found")
.font(.title2)
.fontWeight(.semibold)
Text("Tap the + button to add game updates.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Spacer()
}
.frame(maxWidth: .infinity)
.listRowInsets(EdgeInsets())
}
}
}
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)
.foregroundColor(.primary)
Spacer()
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
.foregroundColor(update.isSelected ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
if let index = updates.firstIndex(where: { $0.path == update.path }) {
removeUpdate(at: IndexSet(integer: index))
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
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 }
let documentsDirectory = URL.documentsDirectory
jsonURL = documentsDirectory
.appendingPathComponent("games")
.appendingPathComponent(game.titleId)
.appendingPathComponent("updates.json")
loadJSON()
}
private func loadJSON() {
guard let jsonURL = jsonURL else { return }
do {
if !FileManager.default.fileExists(atPath: jsonURL.path) {
createDefaultJSON()
return
}
let data = try Data(contentsOf: jsonURL)
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let paths = jsonDict["paths"] as? [String],
let selected = jsonDict["selected"] as? String {
let filteredPaths = paths.filter { relativePath in
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
return FileManager.default.fileExists(atPath: path.path)
}
updates = filteredPaths.map { relativePath in
let url = URL.documentsDirectory.appendingPathComponent(relativePath)
return UpdateItem(
url: url,
filename: url.lastPathComponent,
path: relativePath,
isSelected: selected == relativePath
)
}
}
} catch {
print("Failed to read JSON: \(error)")
createDefaultJSON()
}
}
private func createDefaultJSON() {
guard let jsonURL = jsonURL else { return }
do {
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
let defaultData: [String: Any] = ["selected": "", "paths": []]
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
try newData.write(to: jsonURL)
updates = []
} catch {
print("Failed to create default JSON: \(error)")
}
}
private func handleFileImport(result: Result<URL, Error>) {
switch result {
case .success(let selectedURL):
guard let game = game,
selectedURL.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { selectedURL.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let updatesDirectory = URL.documentsDirectory.appendingPathComponent("updates")
let gameUpdatesDirectory = updatesDirectory.appendingPathComponent(game.titleId)
// Create directories if needed
try fileManager.createDirectory(at: gameUpdatesDirectory, withIntermediateDirectories: true)
// Copy the file
let destinationURL = gameUpdatesDirectory.appendingPathComponent(selectedURL.lastPathComponent)
try? fileManager.removeItem(at: destinationURL) // Remove if exists
try fileManager.copyItem(at: selectedURL, to: destinationURL)
// Add to updates
let relativePath = "updates/\(game.titleId)/\(selectedURL.lastPathComponent)"
let newUpdate = UpdateItem(
url: destinationURL,
filename: selectedURL.lastPathComponent,
path: relativePath
)
updates.append(newUpdate)
toggleSelection(newUpdate)
// Reload games
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Error copying update file: \(error)")
}
case .failure(let error):
print("File import failed: \(error.localizedDescription)")
}
}
private func toggleSelection(_ update: UpdateItem) {
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
}
// print(updates)
saveJSON()
}
private func removeUpdates(at offsets: IndexSet) {
offsets.forEach { removeUpdate(at: IndexSet(integer: $0)) }
}
private func removeUpdate(at indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let updateToRemove = updates[index]
do {
// Remove the file
try FileManager.default.removeItem(at: updateToRemove.url)
// Remove from updates array
updates.remove(at: index)
// Save changes
saveJSON()
// Reload games
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Failed to remove update: \(error)")
}
}
private func saveJSON() {
guard let jsonURL = jsonURL else { return }
do {
let paths = updates.map { $0.path }
let selected = updates.first(where: { $0.isSelected })?.path ?? ""
let jsonDict = ["paths": paths, "selected": selected] as [String: Any]
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL)
} catch {
print("Failed to update JSON: \(error)")
}
}
}

View File

@ -1,168 +0,0 @@
//
// GameDLCManagerSheet.swift
// MeloNX
//
// Created by XITRIX on 16/02/2025.
//
import SwiftUI
import UniformTypeIdentifiers
struct DownloadableContentNca: Codable, Hashable {
var fullPath: String
var titleId: UInt
var enabled: Bool
enum CodingKeys: String, CodingKey {
case fullPath = "path"
case titleId = "title_id"
case enabled = "is_enabled"
}
}
struct DownloadableContentContainer: Codable, Hashable {
var containerPath: String
var downloadableContentNcaList: [DownloadableContentNca]
enum CodingKeys: String, CodingKey {
case containerPath = "path"
case downloadableContentNcaList = "dlc_nca_list"
}
}
struct DLCManagerSheet: View {
@Binding var game: Game!
@State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = []
var body: some View {
NavigationView {
let withIndex = dlcs.enumerated().map { $0 }
List(withIndex, id: \.element.containerPath) { index, dlc in
Button(action: {
let toggle = dlcs[index].downloadableContentNcaList.first?.enabled ?? true
dlcs[index].downloadableContentNcaList.mutableForEach { $0.enabled = !toggle }
Self.saveDlcs(game, dlc: dlcs)
}) {
HStack {
Text((dlc.containerPath as NSString).lastPathComponent)
.foregroundStyle(Color(uiColor: .label))
Spacer()
if dlc.downloadableContentNcaList.first?.enabled == true {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
.font(.system(size: 24))
} else {
Image(systemName: "circle")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.font(.system(size: 24))
}
}
}
.contextMenu {
Button {
let path = URL.documentsDirectory.appendingPathComponent(dlc.containerPath)
try? FileManager.default.removeItem(atPath: path.path)
dlcs.remove(at: index)
Self.saveDlcs(game, dlc: dlcs)
} label: {
Text("Remove DLC")
}
}
}
.navigationTitle("\(game.titleName) DLCs")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Add", systemImage: "plus") {
isSelectingGameDLC = true
}
}
}
.onAppear {
dlcs = Self.loadDlc(game)
}
.fileImporter(isPresented: $isSelectingGameDLC, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in
switch result {
case .success(let urls):
for url in urls {
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 dlcDirectory = documentsDirectory.appendingPathComponent("dlc")
let romDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
if !fileManager.fileExists(atPath: dlcDirectory.path) {
try fileManager.createDirectory(at: dlcDirectory, withIntermediateDirectories: true, attributes: nil)
}
if !fileManager.fileExists(atPath: romDlcDirectory.path) {
try fileManager.createDirectory(at: romDlcDirectory, withIntermediateDirectories: true, attributes: nil)
}
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: url.path)
guard !dlcContent.isEmpty else { return }
let destinationURL = romDlcDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.copyItem(at: url, to: destinationURL)
let container = DownloadableContentContainer(
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
downloadableContentNcaList: dlcContent
)
dlcs.append(container)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Error copying game file: \(error)")
}
}
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
}
}
}
}
private extension DLCManagerSheet {
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
let jsonURL = dlcJsonPath(for: game)
guard let data = try? Data(contentsOf: jsonURL),
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
else { return [] }
result = result.filter { container in
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
return FileManager.default.fileExists(atPath: path.path)
}
return result
}
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
guard let data = try? JSONEncoder().encode(dlc) else { return }
try? data.write(to: dlcJsonPath(for: game))
}
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
}
static func dlcJsonPath(for game: Game) -> URL {
URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game.titleId).appendingPathComponent("dlc.json")
}
}
extension URL {
@available(iOS, introduced: 15.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
}
}

View File

@ -1,201 +0,0 @@
//
// GameUpdateManagerSheet.swift
// MeloNX
//
// Created by Stossy11 on 16/02/2025.
//
import SwiftUI
import UniformTypeIdentifiers
struct UpdateManagerSheet: View {
@State private var items: [String] = []
@State private var paths: [URL] = []
@State private var selectedItem: String? = nil
@Binding var game: Game?
@State private var isSelectingGameUpdate = false
@State private var jsonURL: URL? = nil
var body: some View {
NavigationView {
List(paths, id: \..self, selection: $selectedItem) { item in
Button(action: {
selectItem(item.lastPathComponent)
}) {
HStack {
Text(item.lastPathComponent)
.foregroundStyle(Color(uiColor: .label))
Spacer()
if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
.font(.system(size: 24))
} else {
Image(systemName: "circle")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.font(.system(size: 24))
}
}
}
.contextMenu {
Button {
removeUpdate(item)
} label: {
Text("Remove Update")
}
}
}
.onAppear {
print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
loadJSON(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
}
.navigationTitle("\(game!.titleName) Updates")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Add", systemImage: "plus") {
isSelectingGameUpdate = true
}
}
}
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item]) { result in
switch result {
case .success(let url):
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
let gameInfo = game!
do {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let updatedDirectory = documentsDirectory.appendingPathComponent("updates")
let romUpdatedDirectory = updatedDirectory.appendingPathComponent(gameInfo.titleId)
if !fileManager.fileExists(atPath: updatedDirectory.path) {
try fileManager.createDirectory(at: updatedDirectory, withIntermediateDirectories: true, attributes: nil)
}
if !fileManager.fileExists(atPath: romUpdatedDirectory.path) {
try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil)
}
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.copyItem(at: url, to: destinationURL)
items.append("updates/" + gameInfo.titleId + "/" + url.lastPathComponent)
selectItem(url.lastPathComponent)
Ryujinx.shared.games = Ryujinx.shared.loadGames()
loadJSON(jsonURL!)
} catch {
print("Error copying game file: \(error)")
}
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
}
}
}
func removeUpdate(_ game: URL) {
let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)"
paths.removeAll { $0 == game }
items.removeAll { $0 == gameString }
if selectedItem == gameString {
selectedItem = nil
}
do {
try FileManager.default.removeItem(at: game)
} catch {
print(error)
}
saveJSON(selectedItem: selectedItem ?? "")
Ryujinx.shared.games = Ryujinx.shared.loadGames()
}
func saveJSON(selectedItem: String?) {
guard let jsonURL = jsonURL else { return }
do {
let jsonDict = ["paths": items, "selected": selectedItem ?? self.selectedItem ?? ""] as [String: Any]
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL)
} catch {
print("Failed to update JSON: \(error)")
}
}
func loadJSON(_ json: URL) {
self.jsonURL = json
guard let jsonURL else { return }
do {
let data = try Data(contentsOf: jsonURL)
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let list = jsonDict["paths"] as? [String]
{
let filteredList = list.filter { relativePath in
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
return FileManager.default.fileExists(atPath: path.path)
}
let urls: [URL] = filteredList.map { relativePath in
URL.documentsDirectory.appendingPathComponent(relativePath)
}
items = filteredList
paths = urls
selectedItem = jsonDict["selected"] as? String
}
} catch {
print("Failed to read JSON: \(error)")
createDefaultJSON()
}
}
func createDefaultJSON() {
guard let jsonURL = jsonURL else { return }
let defaultData: [String: Any] = ["selected": "", "paths": []]
do {
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
try newData.write(to: jsonURL)
items = []
selectedItem = ""
} catch {
print("Failed to create default JSON: \(error)")
}
}
func selectItem(_ item: String) {
let newSelection = "updates/\(game!.titleId)/\(item)"
guard let jsonURL else { return }
do {
let data = try Data(contentsOf: jsonURL)
var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection {
jsonDict["selected"] = ""
selectedItem = ""
} else {
jsonDict["selected"] = "\(newSelection)"
selectedItem = newSelection
}
jsonDict["paths"] = items
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL)
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Failed to update JSON: \(error)")
}
}
}

View File

@ -8,101 +8,111 @@
import SwiftUI
import UIKit
import CryptoKit
import Alamofire
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 {
@Environment(\.scenePhase) var scenePhase
@State var finished = false
// Variables for the update system :)
@State var showed = false
@Environment(\.scenePhase) var scenePhase
@State var alert: UIAlertController? = nil
@State var showOutOfDateSheet = false
@State var updateInfo: LatestVersionResponse? = nil
@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
var body: some Scene {
WindowGroup {
VStack {
if finishedStorage {
ContentView()
} else {
SetupView(finished: $finished)
.onChange(of: finished) { newValue in
withAnimation {
withAnimation {
finishedStorage = newValue
}
if finishedStorage {
ContentView()
.withFileImporter()
.onAppear {
if checkForUpdate {
checkLatestVersion()
}
}
.sheet(isPresented: Binding(
get: { showOutOfDateSheet && updateInfo != nil },
set: { newValue in
if !newValue {
showOutOfDateSheet = false
updateInfo = nil
}
}
}
}
.onAppear {
checkLatestVersion()
}
// this seems like a weird way to show the sheet but, from my history this is the most reliable way for the content to actually show in the sheet, otherwise its blank
.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)
}
}
} else {
SetupView(finished: $finished)
.onChange(of: finished) { newValue in
withAnimation(.easeOut) {
finishedStorage = newValue
}
}
}
)) {
if let updateInfo = updateInfo {
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
}
}
}
}
// sends a GET request to the MeloNXSite API and compares the version it returns to the current app version
func checkLatestVersion() {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
let strippedAppVersion = appVersion.replacingOccurrences(of: ".", with: "")
#if DEBUG
// no this isnt a public ip address silly viewers (i know damn well someone thought this was my real ip), this is local :PP
let url = "http://192.168.178.116:8000/api/latest_release"
let urlString = "http://192.168.178.116:8000/api/latest_release"
#else
// dont spam this :pray:
let url = "https://melonx.org/api/latest_release"
let urlString = "https://melonx.net/api/latest_release"
#endif
// actually sends the request
AF.request(url).responseDecodable(of: LatestVersionResponse.self) { response in
switch response.result {
case .success(let latestVersionResponse):
guard let url = URL(string: urlString) else {
// 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)")
return
}
guard let data = data else {
// print("No data received")
return
}
do {
let latestVersionResponse = try JSONDecoder().decode(LatestVersionResponse.self, from: data)
let latestAPIVersionStripped = latestVersionResponse.version_number_stripped
if Int(strippedAppVersion) ?? 0 < Int(latestAPIVersionStripped) ?? 0 {
updateInfo = latestVersionResponse
showOutOfDateSheet = true
if Int(strippedAppVersion) ?? 0 > Int(latestAPIVersionStripped) ?? 0 {
DispatchQueue.main.async {
updateInfo = latestVersionResponse
showOutOfDateSheet = true
}
}
case .failure(let error):
print("Error checking for new version: \(error)")
} catch {
// print("Failed to decode response: \(error)")
}
}
}
}
func detectRoms(path string: String) -> String {
let inputData = Data(string.utf8)
let romHash = SHA256.hash(data: inputData)
return romHash.compactMap { String(format: "%02x", $0) }.joined()
}
func addFolders(_ folderPath: String) -> String? {
let fileManager = FileManager.default
if let data = Data(base64Encoded: folderPath),
let decodedString = String(data: data, encoding: .utf8), let fileURL = UIDevice.current.identifierForVendor?.uuidString {
return decodedString + "auth/" + fileURL + "/"
}
return nil
}
extension String {
func print() {
Swift.print(self)
task.resume()
}
}

View File

@ -21,7 +21,7 @@ struct SetupView: View {
var body: some View {
iOSNav {
ZStack {
if UIDevice.current.userInterfaceIdiom == .pad {
if UIDevice.current.systemName.contains("iPadOS") {
iPadSetupView(
finished: $finished,
isImportingKeys: $isImportingKeys,
@ -54,18 +54,25 @@ 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()
finished = false
keysImported = Ryujinx.shared.checkIfKeysImported()
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmImported = (firmware == "" ? "0" : firmware) != "0"
}
}
@ -116,6 +123,9 @@ struct SetupView: View {
.font(.title)
.fontWeight(.bold)
.foregroundColor(.primary)
.onTapGesture(count: 2) {
showSkipAlert = true
}
Text("Set up your Nintendo Switch emulation environment by importing keys and firmware.")
.font(.subheadline)
@ -365,8 +375,8 @@ struct SetupView: View {
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmImported = (firmware == "" ? "0" : firmware) != "0"
alertMessage = "Firmware installed successfully"
showAlert = true
@ -385,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 ""
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -0,0 +1,6 @@
framework module RyujinxHelper {
umbrella header "RyujinxHelper.h"
export *
module * { export * }
}

View File

@ -4,22 +4,22 @@
<dict>
<key>files</key>
<dict>
<key>Headers/RyujinxKeyboard.h</key>
<key>Headers/RyujinxHelper.h</key>
<data>
5P7GN4g050n199pV6/+SpfMBgJc=
</data>
<key>Info.plist</key>
<data>
hYdI/ktAKwjBSfaJpt6Yc8UKLCY=
UOH9NuuEcz5NQiQlrM2LNFaG2pI=
</data>
<key>Modules/module.modulemap</key>
<data>
0kFAMoTn+4Q1J/dM6uMLe3EhbL0=
JDij7psMD6pZZpigUfkSQldib+I=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/RyujinxKeyboard.h</key>
<key>Headers/RyujinxHelper.h</key>
<dict>
<key>hash2</key>
<data>
@ -30,7 +30,7 @@
<dict>
<key>hash2</key>
<data>
K+ZyxKhTI4bMVZuHBIspvd2PFqvCOlVUFYmwF96O5NQ=
5t/lQcpkzC5bwJqFQqIf6h1ldlhHouYzDawRVrnUeyM=
</data>
</dict>
</dict>

View File

@ -1,6 +0,0 @@
framework module RyujinxKeyboard {
umbrella header "RyujinxKeyboard.h"
export *
module * { export * }
}

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>MoltenVK.framework/MoltenVK</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>MoltenVK.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@ -38,8 +38,9 @@
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>location</string>
<string>processing</string>
<string>audio</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>

View File

@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging
TamperMachine,
UI,
Vic,
Memory,
}
}

View File

@ -4,13 +4,14 @@ using Ryujinx.Memory;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
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
@ -21,12 +22,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
private readonly ReservedRegion _region;
private readonly CacheMemoryAllocator _cacheAllocator;
public readonly IJitMemoryAllocator Allocator;
public CacheMemoryAllocator Allocator => _cacheAllocator;
public CacheMemoryAllocator CacheAllocator => _cacheAllocator;
public IntPtr Pointer => _region.Block.Pointer;
public MemoryCache(IJitMemoryAllocator allocator, ulong size)
{
Allocator = allocator;
_region = new(allocator, size);
_cacheAllocator = new((int)size);
}
@ -101,9 +104,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private readonly IStackWalker _stackWalker;
private readonly Translator _translator;
private readonly MemoryCache _sharedCache;
private readonly MemoryCache _localCache;
private readonly PageAlignedRangeList _pendingMap;
private readonly List<MemoryCache> _sharedCaches;
private readonly List<MemoryCache> _localCaches;
private readonly Dictionary<ulong, PageAlignedRangeList> _pendingMaps;
private readonly object _lock;
class ThreadLocalCacheEntry
@ -111,13 +114,15 @@ namespace Ryujinx.Cpu.LightningJit.Cache
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)
public ThreadLocalCacheEntry(int offset, int size, IntPtr funcPtr, int cacheIndex)
{
Offset = offset;
Size = size;
FuncPtr = funcPtr;
CacheIndex = cacheIndex;
_useCount = 0;
}
@ -134,12 +139,87 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
_stackWalker = stackWalker;
_translator = translator;
_sharedCache = new(allocator, SharedCacheSize);
_localCache = new(allocator, LocalCacheSize);
_pendingMap = new(_sharedCache.ReprotectAsRx, RegisterFunction);
_sharedCaches = new List<MemoryCache> { new(allocator, SharedCacheSize) };
_localCaches = new List<MemoryCache> { new(allocator, LocalCacheSize) };
_pendingMaps = new Dictionary<ulong, PageAlignedRangeList>();
_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, SharedCacheSize));
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)
{
}
}
lock (_lock)
{
var allocator = _localCaches[0].Allocator;
_localCaches.Add(new(allocator, LocalCacheSize));
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<byte> code, ulong guestAddress, ulong guestSize)
{
if (TryGetThreadLocalFunction(guestAddress, out IntPtr funcPtr))
@ -149,16 +229,18 @@ namespace Ryujinx.Cpu.LightningJit.Cache
lock (_lock)
{
if (!_pendingMap.Has(guestAddress) && !_translator.Functions.ContainsKey(guestAddress))
if (!HasInAnyPendingMap(guestAddress) && !_translator.Functions.ContainsKey(guestAddress))
{
int funcOffset = _sharedCache.Allocate(code.Length);
funcPtr = _sharedCache.Pointer + funcOffset;
int combinedOffset = AllocateInSharedCache(code.Length);
var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset);
MemoryCache cache = _sharedCaches[cacheIndex];
funcPtr = cache.Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
TranslatedFunction function = new(funcPtr, guestSize);
_pendingMap.Add(funcOffset, code.Length, guestAddress, function);
GetPendingMapForCache(cacheIndex).Add(funcOffset, code.Length, guestAddress, function);
}
ClearThreadLocalCache(framePointer);
@ -171,25 +253,63 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
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<byte>((void*)funcPtr1, code.Length));
_sharedCaches[cacheIndex].ReprotectAsRx(funcOffset, sizeAligned);
return funcPtr1;
}
catch (OutOfMemoryException)
{
// Try next cache
}
}
// All existing caches are full, create a new one
var allocator = _sharedCaches[0].Allocator;
var newCache = new MemoryCache(allocator, SharedCacheSize);
_sharedCaches.Add(newCache);
cacheIndex = _sharedCaches.Count - 1;
var newPendingMap = GetPendingMapForCache(cacheIndex);
// Ensure we will get an aligned offset from the allocator.
_pendingMap.Pad(_sharedCache.Allocator);
int sizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
int funcOffset = _sharedCache.Allocate(sizeAligned);
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 = _sharedCache.Pointer + funcOffset;
IntPtr funcPtr = newCache.Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
_sharedCache.ReprotectAsRx(funcOffset, sizeAligned);
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)
@ -200,7 +320,15 @@ namespace Ryujinx.Cpu.LightningJit.Cache
lock (_lock)
{
_pendingMap.Pad(_sharedCache.Allocator);
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);
}
}
}
}
@ -224,12 +352,36 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return;
}
IEnumerable<ulong> callStack = _stackWalker.GetCallStack(
framePointer,
_localCache.Pointer,
LocalCacheSize,
_sharedCache.Pointer,
SharedCacheSize);
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] = LocalCacheSize;
}
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] = SharedCacheSize;
}
// Iterate over the arrays and pass each element to GetCallStack
IEnumerable<ulong> callStack = null;
for (int i = 0; i < _localCaches.Count; i++)
{
callStack = _stackWalker.GetCallStack(
framePointer,
cachePointers[i], // Passing each individual cachePointer
cacheSizes[i], // Passing each individual cacheSize
sharedPointers[i], // Passing each individual sharedPointer
sharedSizes[i] // Passing each individual sharedSize
);
}
List<(ulong, ThreadLocalCacheEntry)> toDelete = new();
@ -237,7 +389,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
// We only want to delete if the function is already on the shared cache,
// otherwise we will keep translating the same function over and over again.
bool canDelete = !_pendingMap.Has(address);
bool canDelete = !HasInAnyPendingMap(address);
if (!canDelete)
{
continue;
@ -267,12 +419,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
_threadLocalCache.Remove(address);
int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
_localCache.Free(entry.Offset, sizeAligned);
_localCache.ReprotectAsRw(entry.Offset, sizeAligned);
_localCaches[cacheIndex].Free(offset, sizeAligned);
_localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
}
}
public void ClearEntireThreadLocalCache()
{
// Thread is exiting, delete everything.
@ -287,9 +441,10 @@ namespace Ryujinx.Cpu.LightningJit.Cache
foreach ((_, ThreadLocalCacheEntry entry) in _threadLocalCache)
{
int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
_localCache.Free(entry.Offset, sizeAligned);
_localCache.ReprotectAsRw(entry.Offset, sizeAligned);
_localCaches[cacheIndex].Free(offset, sizeAligned);
_localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
}
_threadLocalCache.Clear();
@ -299,16 +454,17 @@ namespace Ryujinx.Cpu.LightningJit.Cache
private unsafe IntPtr AddThreadLocalFunction(ReadOnlySpan<byte> code, ulong guestAddress)
{
int alignedSize = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
int funcOffset = _localCache.Allocate(alignedSize);
int combinedOffset = AllocateInLocalCache(alignedSize);
var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset);
Debug.Assert((funcOffset & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
IntPtr funcPtr = _localCache.Pointer + funcOffset;
IntPtr funcPtr = _localCaches[cacheIndex].Pointer + funcOffset;
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
(_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr));
(_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr, cacheIndex));
_localCache.ReprotectAsRx(funcOffset, alignedSize);
_localCaches[cacheIndex].ReprotectAsRx(funcOffset, alignedSize);
return funcPtr;
}
@ -326,8 +482,18 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
if (disposing)
{
_localCache.Dispose();
_sharedCache.Dispose();
foreach (var cache in _localCaches)
{
cache.Dispose();
}
foreach (var cache in _sharedCaches)
{
cache.Dispose();
}
_localCaches.Clear();
_sharedCaches.Clear();
}
}
@ -337,4 +503,4 @@ namespace Ryujinx.Cpu.LightningJit.Cache
GC.SuppressFinalize(this);
}
}
}
}

View File

@ -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<byte>.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<byte>.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<byte>.Empty)
.CopyTo(new Span<byte>(bitmap, bitmapOffset, chunkSize));
currentY += chunkHeight;
bitmapOffset += chunkSize;
}
return bitmap;
}
public PinnedSpan<byte> GetData()
{
BackgroundResource resources = _gd.BackgroundResources.Get();
@ -738,14 +779,28 @@ namespace Ryujinx.Graphics.Vulkan
return GetDataFromBuffer(result, size, result);
}
private ReadOnlySpan<byte> GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer, int level)
private ReadOnlySpan<byte> 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<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
return GetDataFromBuffer(result, size, result);
if (size <= MaxChunkSize)
{
Span<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
return GetDataFromBuffer(result, size, result);
}
byte[] fullResult = new byte[size];
Span<byte> fullTextureData = flushBuffer.GetTextureData(cbp, this, size, layer, level);
GetDataFromBuffer(fullTextureData, size, fullTextureData).CopyTo(fullResult);
return fullResult;
}
/// <inheritdoc/>
public void SetData(MemoryOwner<byte> data)
{
@ -769,46 +824,167 @@ namespace Ryujinx.Graphics.Vulkan
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null)
{
const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
int bufferDataLength = GetBufferDataLength(data.Length);
using var bufferHolder = _gd.BufferManager.Create(_gd, bufferDataLength);
Auto<DisposableImage> imageAuto = GetImage();
// Load texture data inline if the texture has been used on the current command buffer.
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
if (loadInline)
if (bufferDataLength <= MaxChunkSize)
{
_gd.PipelineInternal.EndRenderPass();
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
return;
}
CopyDataToBuffer(bufferHolder.GetDataStorage(0, bufferDataLength), data);
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
var image = imageAuto.Get(cbs).Value;
if (region.HasValue)
if (!region.HasValue && !singleSlice && layers > 1)
{
CopyFromOrToBuffer(
cbs.CommandBuffer,
buffer,
image,
bufferDataLength,
false,
layer,
level,
region.Value.X,
region.Value.Y,
region.Value.Width,
region.Value.Height);
int layerSize = data.Length / layers;
int offset = 0;
for (int i = 0; i < layers; i++)
{
if (offset >= data.Length)
break;
int currentLayer = layer + i;
int currentLayerSize = Math.Min(layerSize, data.Length - offset);
if (currentLayerSize <= 0)
break;
try
{
var layerData = data.Slice(offset, currentLayerSize);
ProcessChunk(layerData, currentLayer, level, 1, levels, true);
offset += layerSize;
}
catch (ArgumentOutOfRangeException)
{
break;
}
}
}
else if (region.HasValue)
{
var rect = region.Value;
if (rect.Width <= 0 || rect.Height <= 0)
return;
int dataPerPixel = data.Length / (rect.Width * rect.Height);
if (dataPerPixel <= 0)
return;
int rowStride = rect.Width * dataPerPixel;
int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
int originalHeight = rect.Height;
int currentY = rect.Y;
int offset = 0;
while (currentY < rect.Y + originalHeight)
{
int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY);
if (chunkHeight <= 0)
break;
var chunkRegion = new Rectangle<int>(rect.X, currentY, rect.Width, chunkHeight);
int chunkSize = chunkHeight * rowStride;
if (offset >= data.Length)
break;
int safeChunkSize = Math.Min(chunkSize, data.Length - offset);
if (safeChunkSize <= 0)
break;
try
{
var chunkData = data.Slice(offset, safeChunkSize);
ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion);
currentY += chunkHeight;
offset += chunkSize;
}
catch (ArgumentOutOfRangeException)
{
break;
}
}
}
else
{
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, bufferDataLength, false, layer, level, layers, levels, singleSlice);
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
}
}
private void ProcessChunk(ReadOnlySpan<byte> chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle<int>? chunkRegion = null)
{
int chunkBufferLength = GetBufferDataLength(chunkData.Length);
if (chunkBufferLength <= 0)
return;
using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength);
using (var imageAuto = GetImage())
{
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
if (loadInline)
{
_gd.PipelineInternal.EndRenderPass();
}
try
{
CopyDataToBuffer(bufferHolder.GetDataStorage(0, chunkBufferLength), chunkData);
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
var image = imageAuto.Get(cbs).Value;
if (chunkRegion.HasValue)
{
var region = chunkRegion.Value;
if (region.Width <= 0 || region.Height <= 0)
return;
CopyFromOrToBuffer(
cbs.CommandBuffer,
buffer,
image,
chunkBufferLength,
false,
chunkLayer,
chunkLevel,
region.X,
region.Y,
region.Width,
region.Height);
}
else
{
CopyFromOrToBuffer(
cbs.CommandBuffer,
buffer,
image,
chunkBufferLength,
false,
chunkLayer,
chunkLevel,
chunkLayers,
chunkLevels,
chunkSingleSlice);
}
}
catch (Exception e)
{
}
}
}

View File

@ -239,6 +239,7 @@ namespace Ryujinx.HLE.HOS.Applets
StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
InitialText = initialText,
};
_device.UIHandler.DisplayInputDialog(args, inputText =>
{
Console.WriteLine($"User entered: {inputText}");

View File

@ -561,7 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
context.ResponseData.Write((int)_gyroscopeZeroDriftMode);
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode });
// Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode });
return ResultCode.Success;
}

View File

@ -44,7 +44,7 @@ namespace Ryujinx.HLE
MemoryAllocationFlags memoryAllocationFlags = configuration.MemoryManagerMode == MemoryManagerMode.SoftwarePageTable
? MemoryAllocationFlags.Reserve
: MemoryAllocationFlags.Reserve | MemoryAllocationFlags.Mirrorable;
: MemoryAllocationFlags.Reserve; // | MemoryAllocationFlags.Mirrorable;
#pragma warning disable IDE0055 // Disable formatting
AudioDeviceDriver = AddAudioCompatLayers(Configuration.AudioDeviceDriver);

View File

@ -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.")]
@ -196,7 +207,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.")]

View File

@ -251,21 +251,21 @@ namespace Ryujinx.Headless.SDL2
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
public static unsafe int GetFPS()
{
if (_window != null) {
Switch Device = _window.Device;
if (_window == null || _window.Device == null)
{
return 0;
}
int intValue = (int)Device.Statistics.GetGameFrameRate();
Switch Device = _window.Device;
return intValue;
}
return 0;
int intValue = (int)Device.Statistics.GetGameFrameRate();
return intValue;
}
[UnmanagedCallersOnly(EntryPoint = "initialize")]
public static unsafe void Initialize()
{
AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
if (_virtualFileSystem == null)
@ -286,16 +286,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)
@ -402,11 +392,32 @@ namespace Ryujinx.Headless.SDL2
return String.Empty;
}
[UnmanagedCallersOnly(EntryPoint = "pause_emulation")]
public static void PauseEmulation(bool shouldPause)
{
if (_window != 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();
}
}
@ -886,19 +897,9 @@ namespace Ryujinx.Headless.SDL2
{
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;
@ -989,12 +990,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,
@ -1118,39 +1133,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;

View File

@ -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;
@ -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();
@ -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();
@ -486,7 +491,12 @@ namespace Ryujinx.Headless.SDL2
public bool DisplayMessageDialog(string title, string message)
{
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
if (OperatingSystem.IsIOS())
{
AlertHelper.ShowAlert(title, message, false);
} else {
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
}
return true;
}

View File

@ -7,13 +7,16 @@ namespace Ryujinx.Headless.SDL2
{
public static class AlertHelper
{
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
public static extern void showKeyboardAlert(string title, string message, string placeholder);
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
public static extern void showAlert(string title, string message, bool showCancel);
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr getKeyboardInput();
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
private static extern void clearKeyboardInput();
public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered)
@ -38,5 +41,10 @@ namespace Ryujinx.Headless.SDL2
}
});
}
public static void ShowAlert(string title, string message, bool cancel) {
showAlert(title, message, cancel);
}
}
}

View File

@ -9,8 +9,18 @@ namespace Ryujinx.Input.SDL2
{
private readonly Dictionary<int, string> _gamepadsInstanceIdsMapping;
private readonly List<string> _gamepadsIds;
private readonly object _lock = new object();
public ReadOnlySpan<string> GamepadsIds => _gamepadsIds.ToArray();
public ReadOnlySpan<string> 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);
}
}
}
}