Compare commits

...

29 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: #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: #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
66 changed files with 4229 additions and 1972 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 MeloNX License
Copyright (c) MeloNX Team and Contributors Copyright (c) MeloNX Team and Contributors

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

@ -24,7 +24,6 @@
/* End PBXAggregateTarget section */ /* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; }; 4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; };
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; }; 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
@ -129,10 +128,6 @@
"Dependencies/Dynamic Libraries/libavutil.dylib" = ( "Dependencies/Dynamic Libraries/libavutil.dylib" = (
CodeSignOnCopy, CodeSignOnCopy,
); );
Dependencies/XCFrameworks/MoltenVK.xcframework = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
Dependencies/XCFrameworks/SDL2.xcframework = ( Dependencies/XCFrameworks/SDL2.xcframework = (
CodeSignOnCopy, CodeSignOnCopy,
RemoveHeadersOnCopy, RemoveHeadersOnCopy,
@ -186,7 +181,6 @@
Dependencies/XCFrameworks/libswresample.xcframework, Dependencies/XCFrameworks/libswresample.xcframework,
Dependencies/XCFrameworks/libswscale.xcframework, Dependencies/XCFrameworks/libswscale.xcframework,
Dependencies/XCFrameworks/libteakra.xcframework, Dependencies/XCFrameworks/libteakra.xcframework,
Dependencies/XCFrameworks/MoltenVK.xcframework,
Dependencies/XCFrameworks/SDL2.xcframework, Dependencies/XCFrameworks/SDL2.xcframework,
); );
}; };
@ -203,7 +197,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */, CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */, 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */, 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
@ -301,7 +294,6 @@
); );
name = MeloNX; name = MeloNX;
packageProductDependencies = ( packageProductDependencies = (
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
4EA5AE812D16807500AD0B9F /* SwiftSVG */, 4EA5AE812D16807500AD0B9F /* SwiftSVG */,
); );
productName = MeloNX; productName = MeloNX;
@ -393,7 +385,6 @@
mainGroup = 4E80A9842CD6F54500029585; mainGroup = 4E80A9842CD6F54500029585;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = ( packageReferences = (
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */, 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
); );
preferredProjectObjectVersion = 56; preferredProjectObjectVersion = 56;
@ -652,7 +643,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO; ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -721,6 +712,24 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -736,7 +745,7 @@
INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES; INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -863,6 +872,50 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(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)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
@ -955,6 +1008,24 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -970,7 +1041,7 @@
INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES; INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1097,6 +1168,50 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(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)"; MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
@ -1298,14 +1413,6 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.5.0;
};
};
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = { 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mchoe/SwiftSVG"; repositoryURL = "https://github.com/mchoe/SwiftSVG";
@ -1317,11 +1424,6 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
isa = XCSwiftPackageProductDependency;
package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;
productName = SwiftUIJoystick;
};
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = { 4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */; package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */;

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b", "originHash" : "fedf09a893a63378a2e53f631cd833ae83a0c9ee7338eb8d153b04fd34aaf805",
"pins" : [ "pins" : [
{ {
"identity" : "swiftsvg", "identity" : "swiftsvg",
@ -9,15 +9,6 @@
"branch" : "master", "branch" : "master",
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d" "revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
} }
},
{
"identity" : "swiftuijoystick",
"kind" : "remoteSourceControl",
"location" : "https://github.com/michael94ellis/SwiftUIJoystick",
"state" : {
"revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65",
"version" : "1.5.0"
}
} }
], ],
"version" : 3 "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,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1620" LastUpgradeVersion = "1620"
version = "1.7"> version = "2.0">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES"
@ -62,10 +62,11 @@
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugXPCServices = "NO"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1" enableGPUValidationMode = "1"
allowLocationSimulation = "YES" allowLocationSimulation = "YES"
viewDebuggingEnabled = "No" queueDebuggingEnabled = "No"
consoleMode = "0" consoleMode = "0"
structuredConsoleMode = "2"> structuredConsoleMode = "2">
<BuildableProductRunnable <BuildableProductRunnable

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ func enableJITEB() {
func enableJITEBRequest() { func enableJITEBRequest() {
let pid = Int(getpid()) let pid = Int(getpid())
print(pid) // print(pid)
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")! let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
var request = URLRequest(url: address) var request = URLRequest(url: address)
@ -90,7 +90,7 @@ func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping
let task = session.dataTask(with: request) { _, response, error in let task = session.dataTask(with: request) { _, response, error in
if let error = error { if let error = error {
print("Ping failed: \(error.localizedDescription)") // print("Ping failed: \(error.localizedDescription)")
completion(false) completion(false)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
completion(true) completion(true)
@ -140,12 +140,12 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
viewController.present(alert, animated: true) viewController.present(alert, animated: true)
} }
} else { } else {
print("Hopefully JIT is enabled now...") // print("Hopefully JIT is enabled now...")
Ryujinx.shared.ryuIsJITEnabled() Ryujinx.shared.ryuIsJITEnabled()
} }
} catch { } catch {
print(String(data: jsonData, encoding: .utf8)) // print(String(data: jsonData, encoding: .utf8))
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert) let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default)) 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,39 +49,39 @@ class NativeController: Hashable {
// Update joystick state here // Update joystick state here
}, },
SetPlayerIndex: { userdata, playerIndex in SetPlayerIndex: { userdata, playerIndex in
print("Player index set to \(playerIndex)") // print("Player index set to \(playerIndex)")
}, },
Rumble: { userdata, lowFreq, highFreq in Rumble: { userdata, lowFreq, highFreq in
print("Rumble with \(lowFreq), \(highFreq)") // print("Rumble with \(lowFreq), \(highFreq)")
guard let userdata else { return 0 } guard let userdata else { return 0 }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue() let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics) VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics)
return 0 return 0
}, },
RumbleTriggers: { userdata, leftRumble, rightRumble in RumbleTriggers: { userdata, leftRumble, rightRumble in
print("Trigger rumble with \(leftRumble), \(rightRumble)") // print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0 return 0
}, },
SetLED: { userdata, red, green, blue in SetLED: { userdata, red, green, blue in
print("Set LED to RGB(\(red), \(green), \(blue))") // print("Set LED to RGB(\(red), \(green), \(blue))")
return 0 return 0
}, },
SendEffect: { userdata, data, size in SendEffect: { userdata, data, size in
print("Effect sent with size \(size)") // print("Effect sent with size \(size)")
return 0 return 0
} }
) )
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1) instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
if instanceID < 0 { if instanceID < 0 {
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))") // print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return return
} }
controller = SDL_GameControllerOpen(Int32(instanceID)) controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil { if controller == nil {
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))") // print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return return
} }
@ -89,10 +89,10 @@ class NativeController: Hashable {
guard let gamepad = nativeController.extendedGamepad guard let gamepad = nativeController.extendedGamepad
else { return } else { return }
setupButtonChangeListener(gamepad.buttonA, for: .A) setupButtonChangeListener(gamepad.buttonA, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .B : .A)
setupButtonChangeListener(gamepad.buttonB, for: .B) setupButtonChangeListener(gamepad.buttonB, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .A : .B)
setupButtonChangeListener(gamepad.buttonX, for: .X) setupButtonChangeListener(gamepad.buttonX, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .Y : .X)
setupButtonChangeListener(gamepad.buttonY, for: .Y) setupButtonChangeListener(gamepad.buttonY, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .X : .Y)
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp) setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown) setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
@ -139,7 +139,7 @@ class NativeController: Hashable {
func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) { func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) {
button.valueChangedHandler = { [unowned self] _, value, pressed in 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 axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let scaledValue = Sint16(value * 32767.0) let scaledValue = Sint16(value * 32767.0)
updateAxisValue(value: scaledValue, forAxis: axis) updateAxisValue(value: scaledValue, forAxis: axis)
@ -177,7 +177,7 @@ class NativeController: Hashable {
try highFreqPlayer.start(atTime: 0.2) try highFreqPlayer.start(atTime: 0.2)
} catch { } catch {
print("Error creating haptic patterns: \(error)") // print("Error creating haptic patterns: \(error)")
} }
} }
@ -206,7 +206,7 @@ class NativeController: Hashable {
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) { func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return } 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) { if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0 let value: Int = (state == 1) ? 32767 : 0

View File

@ -41,39 +41,39 @@ class VirtualController {
// Update joystick state here // Update joystick state here
}, },
SetPlayerIndex: { userdata, playerIndex in SetPlayerIndex: { userdata, playerIndex in
print("Player index set to \(playerIndex)") // print("Player index set to \(playerIndex)")
}, },
Rumble: { userdata, lowFreq, highFreq in Rumble: { userdata, lowFreq, highFreq in
print("Rumble with \(lowFreq), \(highFreq)") // print("Rumble with \(lowFreq), \(highFreq)")
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone {
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq)) VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
} }
return 0 return 0
}, },
RumbleTriggers: { userdata, leftRumble, rightRumble in RumbleTriggers: { userdata, leftRumble, rightRumble in
print("Trigger rumble with \(leftRumble), \(rightRumble)") // print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0 return 0
}, },
SetLED: { userdata, red, green, blue in SetLED: { userdata, red, green, blue in
print("Set LED to RGB(\(red), \(green), \(blue))") // print("Set LED to RGB(\(red), \(green), \(blue))")
return 0 return 0
}, },
SendEffect: { userdata, data, size in SendEffect: { userdata, data, size in
print("Effect sent with size \(size)") // print("Effect sent with size \(size)")
return 0 return 0
} }
) )
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1) instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
if instanceID < 0 { if instanceID < 0 {
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))") // print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return return
} }
controller = SDL_GameControllerOpen(Int32(instanceID)) controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil { if controller == nil {
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))") // print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return return
} }
} }
@ -107,7 +107,7 @@ class VirtualController {
} }
guard let engine else { 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) let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
@ -117,7 +117,7 @@ class VirtualController {
try highFreqPlayer.start(atTime: 0) try highFreqPlayer.start(atTime: 0)
} catch { } catch {
print("Error creating haptic patterns: \(error)") // print("Error creating haptic patterns: \(error)")
} }
} }
@ -131,10 +131,8 @@ class VirtualController {
} }
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) { func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
let scaleFactor = 32767.0 / 160 let scaledX = Int16(min(32767.0, max(-32768.0, x * 32767.0)))
let scaledY = Int16(min(32767.0, max(-32768.0, y * 32767.0)))
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
if stick == .right { if stick == .right {
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue)) updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
@ -148,7 +146,7 @@ class VirtualController {
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) { func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return } 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) { if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0 let value: Int = (state == 1) ? 32767 : 0

View File

@ -35,8 +35,8 @@ class MemoryUsageMonitor: ObservableObject {
memoryUsage = taskInfo.phys_footprint memoryUsage = taskInfo.phys_footprint
} }
else { else {
print("Error with task_info(): " + // print("Error with task_info(): " +
(String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")) // (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
} }
} }

View File

@ -32,7 +32,7 @@ class MTLHud {
} }
func toggle() { func toggle() {
print(UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED")) // print(UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED"))
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") { if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
enable() enable()
} else { } else {
@ -44,12 +44,12 @@ class MTLHud {
let path = "/usr/lib/libMTLHud.dylib" let path = "/usr/lib/libMTLHud.dylib"
if dlopen(path, RTLD_NOW) != nil { if dlopen(path, RTLD_NOW) != nil {
print("Library loaded from \(path)") // print("Library loaded from \(path)")
canMetalHud = true canMetalHud = true
return true return true
} else { } else {
if let error = String(validatingUTF8: dlerror()) { if let error = String(validatingUTF8: dlerror()) {
print("Error loading library: \(error)") // print("Error loading library: \(error)")
} }
canMetalHud = false canMetalHud = false
return false return false

View File

@ -11,6 +11,93 @@ import GameController
import MetalKit import MetalKit
import Metal 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 { struct Controller: Identifiable, Hashable {
var id: String var id: String
var name: String var name: String
@ -32,12 +119,13 @@ struct iOSNav<Content: View>: View {
class Ryujinx : ObservableObject { class Ryujinx : ObservableObject {
private var isRunning = false @Published var isRunning = false
let virtualController = VirtualController() let virtualController = VirtualController()
@Published var controllerMap: [Controller] = [] @Published var controllerMap: [Controller] = []
@Published var metalLayer: CAMetalLayer? = nil @Published var metalLayer: CAMetalLayer? = nil
@Published var isPortrait = false
@Published var firmwareversion = "0" @Published var firmwareversion = "0"
@Published var emulationUIView: MeloMTKView? = nil @Published var emulationUIView: MeloMTKView? = nil
@Published var config: Ryujinx.Configuration? = nil @Published var config: Ryujinx.Configuration? = nil
@ -45,7 +133,7 @@ class Ryujinx : ObservableObject {
@Published var defMLContentSize: CGFloat? @Published var defMLContentSize: CGFloat?
var thread: Thread! var thread: Thread = Thread { }
@Published var jitenabled = false @Published var jitenabled = false
@ -59,6 +147,22 @@ class Ryujinx : ObservableObject {
self.games = loadGames() 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 { public struct Configuration : Codable, Equatable {
var gamepath: String var gamepath: String
var inputids: [String] var inputids: [String]
@ -149,7 +253,8 @@ class Ryujinx : ObservableObject {
self.config = config self.config = config
thread = Thread { [self] in
runloop { [self] in
isRunning = true isRunning = true
@ -179,13 +284,92 @@ class Ryujinx : ObservableObject {
} }
} catch { } catch {
self.isRunning = false 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")
}
}
}
}
} }
} }
thread.qualityOfService = .background func saveArrayAsTextFile(strings: [String], filePath: String) {
thread.name = "MeloNX" let text = strings.joined(separator: "\n")
thread.start()
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
} }
@ -199,14 +383,9 @@ class Ryujinx : ObservableObject {
self.emulationUIView = nil self.emulationUIView = nil
self.metalLayer = nil self.metalLayer = nil
stop_emulation()
thread.cancel() thread.cancel()
} }
var running: Bool {
return isRunning
}
func loadGames() -> [Game] { func loadGames() -> [Game] {
let fileManager = FileManager.default let fileManager = FileManager.default
@ -218,7 +397,7 @@ class Ryujinx : ObservableObject {
do { do {
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil) try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch { } catch {
print("Failed to create roms directory: \(error)") // print("Failed to create roms directory: \(error)")
} }
} }
var games: [Game] = [] var games: [Game] = []
@ -243,13 +422,13 @@ class Ryujinx : ObservableObject {
games.append(game) games.append(game)
} catch { } catch {
print(error) // print(error)
} }
} }
return games return games
} catch { } catch {
print("Error loading games from roms folder: \(error)") // print("Error loading games from roms folder: \(error)")
return games return games
} }
@ -273,12 +452,31 @@ class Ryujinx : ObservableObject {
// We don't need this. Ryujinx should handle it fine :3 // We don't need this. Ryujinx should handle it fine :3
// this also causes crashes in some games :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-language", config.language.rawValue])
args.append(contentsOf: ["--system-region", config.regioncode.rawValue]) args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue]) args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
if config.nintendoinput { if config.nintendoinput {
args.append("--correct-controller") args.append("--correct-controller")
} }
@ -383,7 +581,7 @@ class Ryujinx : ObservableObject {
func installFirmware(firmwarePath: String) { func installFirmware(firmwarePath: String) {
guard let cString = firmwarePath.cString(using: .utf8) else { guard let cString = firmwarePath.cString(using: .utf8) else {
print("Invalid firmware path") // print("Invalid firmware path")
return return
} }
@ -399,12 +597,12 @@ class Ryujinx : ObservableObject {
guard let titleIdCString = titleId.cString(using: .utf8), guard let titleIdCString = titleId.cString(using: .utf8),
let pathCString = path.cString(using: .utf8) let pathCString = path.cString(using: .utf8)
else { else {
print("Invalid path") // print("Invalid path")
return [] return []
} }
let listPointer = get_dlc_nca_list(titleIdCString, pathCString) 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 [] } guard listPointer.success else { return [] }
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size))) let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
@ -456,7 +654,7 @@ class Ryujinx : ObservableObject {
let guid = generateGamepadId(joystickIndex: i) let guid = generateGamepadId(joystickIndex: i)
let name = String(cString: SDL_GameControllerName(controller)) let name = String(cString: SDL_GameControllerName(controller))
print("Controller \(i): \(name), GUID: \(guid ?? "")") // print("Controller \(i): \(name), GUID: \(guid ?? "")")
guard let guid else { guard let guid else {
SDL_GameControllerClose(controller) SDL_GameControllerClose(controller)
@ -487,33 +685,163 @@ class Ryujinx : ObservableObject {
do { do {
if fileManager.fileExists(atPath: registeredFolder) { if fileManager.fileExists(atPath: registeredFolder) {
try fileManager.removeItem(atPath: registeredFolder) try fileManager.removeItem(atPath: registeredFolder)
print("Folder removed successfully.") // print("Folder removed successfully.")
let version = fetchFirmwareVersion() let version = fetchFirmwareVersion()
if version.isEmpty { if version.isEmpty {
self.firmwareversion = "0" self.firmwareversion = "0"
} else { } else {
print("Firmware eeeeee \(version)") // print("Firmware eeeeee \(version)")
} }
} else { } else {
print("Folder does not exist.") // print("Folder does not exist.")
} }
} catch { } catch {
print("Error removing folder: \(error)") // print("Error removing folder: \(error)")
} }
} }
static func log(_ message: String) { 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() { func ryuIsJITEnabled() {
jitenabled = isJITEnabled() jitenabled = isJITEnabled()
print("JIT \(jitenabled)")
} }
} }
public extension UIDevice {
static let modelName: String = {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity
#if os(iOS)
switch identifier {
case "iPod5,1": return "iPod touch (5th generation)"
case "iPod7,1": return "iPod touch (6th generation)"
case "iPod9,1": return "iPod touch (7th generation)"
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4"
case "iPhone4,1": return "iPhone 4s"
case "iPhone5,1", "iPhone5,2": return "iPhone 5"
case "iPhone5,3", "iPhone5,4": return "iPhone 5c"
case "iPhone6,1", "iPhone6,2": return "iPhone 5s"
case "iPhone7,2": return "iPhone 6"
case "iPhone7,1": return "iPhone 6 Plus"
case "iPhone8,1": return "iPhone 6s"
case "iPhone8,2": return "iPhone 6s Plus"
case "iPhone9,1", "iPhone9,3": return "iPhone 7"
case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus"
case "iPhone10,1", "iPhone10,4": return "iPhone 8"
case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus"
case "iPhone10,3", "iPhone10,6": return "iPhone X"
case "iPhone11,2": return "iPhone XS"
case "iPhone11,4", "iPhone11,6": return "iPhone XS Max"
case "iPhone11,8": return "iPhone XR"
case "iPhone12,1": return "iPhone 11"
case "iPhone12,3": return "iPhone 11 Pro"
case "iPhone12,5": return "iPhone 11 Pro Max"
case "iPhone13,1": return "iPhone 12 mini"
case "iPhone13,2": return "iPhone 12"
case "iPhone13,3": return "iPhone 12 Pro"
case "iPhone13,4": return "iPhone 12 Pro Max"
case "iPhone14,4": return "iPhone 13 mini"
case "iPhone14,5": return "iPhone 13"
case "iPhone14,2": return "iPhone 13 Pro"
case "iPhone14,3": return "iPhone 13 Pro Max"
case "iPhone14,7": return "iPhone 14"
case "iPhone14,8": return "iPhone 14 Plus"
case "iPhone15,2": return "iPhone 14 Pro"
case "iPhone15,3": return "iPhone 14 Pro Max"
case "iPhone15,4": return "iPhone 15"
case "iPhone15,5": return "iPhone 15 Plus"
case "iPhone16,1": return "iPhone 15 Pro"
case "iPhone16,2": return "iPhone 15 Pro Max"
case "iPhone17,3": return "iPhone 16"
case "iPhone17,4": return "iPhone 16 Plus"
case "iPhone17,1": return "iPhone 16 Pro"
case "iPhone17,2": return "iPhone 16 Pro Max"
case "iPhone17,5": return "iPhone 16e"
case "iPhone8,4": return "iPhone SE"
case "iPhone12,8": return "iPhone SE (2nd generation)"
case "iPhone14,6": return "iPhone SE (3rd generation)"
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2"
case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)"
case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)"
case "iPad6,11", "iPad6,12": return "iPad (5th generation)"
case "iPad7,5", "iPad7,6": return "iPad (6th generation)"
case "iPad7,11", "iPad7,12": return "iPad (7th generation)"
case "iPad11,6", "iPad11,7": return "iPad (8th generation)"
case "iPad12,1", "iPad12,2": return "iPad (9th generation)"
case "iPad13,18", "iPad13,19": return "iPad (10th generation)"
case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air"
case "iPad5,3", "iPad5,4": return "iPad Air 2"
case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)"
case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
case "iPad13,16", "iPad13,17": return "iPad Air (5th generation)"
case "iPad14,8", "iPad14,9": return "iPad Air (11-inch) (M2)"
case "iPad14,10", "iPad14,11": return "iPad Air (13-inch) (M2)"
case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini"
case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2"
case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3"
case "iPad5,1", "iPad5,2": return "iPad mini 4"
case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)"
case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)"
case "iPad16,1", "iPad16,2": return "iPad mini (A17 Pro)"
case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)"
case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)"
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)"
case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)"
case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)"
case "iPad14,3", "iPad14,4": return "iPad Pro (11-inch) (4th generation)"
case "iPad16,3", "iPad16,4": return "iPad Pro (11-inch) (M4)"
case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)"
case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)"
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)"
case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)"
case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)"
case "iPad14,5", "iPad14,6": return "iPad Pro (12.9-inch) (6th generation)"
case "iPad16,5", "iPad16,6": return "iPad Pro (13-inch) (M4)"
case "AppleTV5,3": return "Apple TV"
case "AppleTV6,2": return "Apple TV 4K"
case "AudioAccessory1,1": return "HomePod"
case "AudioAccessory5,1": return "HomePod mini"
case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
default: return identifier
}
#elseif os(tvOS)
switch identifier {
case "AppleTV5,3": return "Apple TV 4"
case "AppleTV6,2", "AppleTV11,1", "AppleTV14,1": return "Apple TV 4K"
case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))"
default: return identifier
}
#elseif os(visionOS)
switch identifier {
case "RealityDevice14,1": return "Apple Vision Pro"
default: return identifier
}
#endif
}
return mapToDevice(identifier: identifier)
}()
}

View File

@ -35,7 +35,7 @@ struct LaunchGameIntentDef: AppIntent {
let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName)) let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName))
let urlString = "melonx://game?name=\(name ?? gameName)" let urlString = "melonx://game?name=\(name ?? gameName)"
print(urlString) // print(urlString)
if let url = URL(string: urlString) { if let url = URL(string: urlString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil) UIApplication.shared.open(url, options: [:], completionHandler: nil)
} }

View File

@ -57,7 +57,7 @@ public struct Game: Identifiable, Equatable, Hashable {
gameTemp.icon = UIImage(data: imageData) gameTemp.icon = UIImage(data: imageData)
} else { } else {
print("Invalid image size.") // print("Invalid image size.")
} }
return gameTemp return gameTemp
} }
@ -67,7 +67,7 @@ public struct Game: Identifiable, Equatable, Hashable {
let imageSize = Int(gameInfoValue.ImageSize) let imageSize = Int(gameInfoValue.ImageSize)
guard imageSize > 0, imageSize <= 1024 * 1024 else { guard imageSize > 0, imageSize <= 1024 * 1024 else {
print("Invalid image size.") // print("Invalid image size.")
return nil return nil
} }

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,61 +0,0 @@
//
// JoystickView.swift
// Pomelo
//
// Created by Stossy11 on 30/9/2024.
// Copyright © 2024 Stossy11. All rights reserved.
//
import SwiftUI
import SwiftUIJoystick
public struct Joystick: View {
@State var iscool: Bool? = nil
@Environment(\.colorScheme) var colorScheme
@ObservedObject public var joystickMonitor = JoystickMonitor()
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var dragDiameter: CGFloat {
var selfs = CGFloat(160)
selfs *= controllerScale
if UIDevice.current.systemName.contains("iPadOS") {
return selfs * 1.2
}
return selfs
}
private let shape: JoystickShape = .circle
public var body: some View {
VStack{
JoystickBuilder(
monitor: self.joystickMonitor,
width: self.dragDiameter,
shape: .circle,
background: {
Text("")
.hidden()
},
foreground: {
Circle()
.fill(colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.7))
.background(
Circle()
.fill(colorScheme == .dark ? Color.gray.opacity(0.3) : Color.gray.opacity(0.2))
.frame(width: (dragDiameter / 4) * 1.2, height: (dragDiameter / 4) * 1.2)
)
},
locksInPlace: false)
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
let scaledX = Float(newValue.x)
let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/
print("Joystick Position: (\(scaledX), \(scaledY))")
if iscool != nil {
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
} else {
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: newValue.x, y: newValue.y)
}
}
}
}
}

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) { @objc func didConnect(sender: NSNotification) {
print("AirKit - Connect") // print("AirKit - Connect")
self.connected = true self.connected = true
guard let screen: UIScreen = sender.object as? UIScreen else { return } guard let screen: UIScreen = sender.object as? UIScreen else { return }
add(screen: screen) { success in add(screen: screen) { success in
@ -69,35 +69,35 @@ public class Air {
func add(screen: UIScreen, completion: @escaping (Bool) -> ()) { func add(screen: UIScreen, completion: @escaping (Bool) -> ()) {
print("AirKit - Add Screen") // print("AirKit - Add Screen")
airScreen = screen airScreen = screen
airWindow = UIWindow(frame: airScreen!.bounds) airWindow = UIWindow(frame: airScreen!.bounds)
guard let viewController: UIViewController = hostingController else { guard let viewController: UIViewController = hostingController else {
print("AirKit - Add - Failed: Hosting Controller Not Found") // print("AirKit - Add - Failed: Hosting Controller Not Found")
completion(false) completion(false)
return return
} }
findWindowScene(for: airScreen!) { windowScene in findWindowScene(for: airScreen!) { windowScene in
guard let airWindowScene: UIWindowScene = windowScene else { guard let airWindowScene: UIWindowScene = windowScene else {
print("AirKit - Add - Failed: Window Scene Not Found") // print("AirKit - Add - Failed: Window Scene Not Found")
completion(false) completion(false)
return return
} }
self.airWindow?.rootViewController = viewController self.airWindow?.rootViewController = viewController
self.airWindow?.windowScene = airWindowScene self.airWindow?.windowScene = airWindowScene
self.airWindow?.isHidden = false self.airWindow?.isHidden = false
print("AirKit - Add Screen - Done") // print("AirKit - Add Screen - Done")
completion(true) completion(true)
} }
} }
func findWindowScene(for screen: UIScreen, shouldRecurse: Bool = true, completion: @escaping (UIWindowScene?) -> ()) { 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 var matchingWindowScene: UIWindowScene? = nil
let scenes = UIApplication.shared.connectedScenes let scenes = UIApplication.shared.connectedScenes
for scene in scenes { for scene in scenes {
@ -120,23 +120,23 @@ public class Air {
} }
@objc func didDisconnect() { @objc func didDisconnect() {
print("AirKit - Disconnect") // print("AirKit - Disconnect")
remove() remove()
connected = false connected = false
} }
func remove() { func remove() {
print("AirKit - Remove") // print("AirKit - Remove")
airWindow = nil airWindow = nil
airScreen = nil airScreen = nil
} }
@objc func didBecomeActive() { @objc func didBecomeActive() {
print("AirKit - App Active") // print("AirKit - App Active")
} }
@objc func willResignActive() { @objc func willResignActive() {
print("AirKit - App Inactive") // print("AirKit - App Inactive")
} }

View File

@ -4,7 +4,7 @@ import SwiftUI
public extension View { public extension View {
func airPlay() -> some View { func airPlay() -> some View {
print("AirKit - airPlay") // print("AirKit - airPlay")
Air.play(AnyView(self)) Air.play(AnyView(self))
return self return self
} }

View File

@ -7,7 +7,6 @@
import SwiftUI import SwiftUI
import GameController import GameController
import SwiftUIJoystick
import CoreMotion import CoreMotion
struct ControllerView: View { struct ControllerView: View {
@ -15,6 +14,8 @@ struct ControllerView: View {
@AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0
@AppStorage("stick-button") private var stickButton = false @AppStorage("stick-button") private var stickButton = false
@State private var isPortrait = true @State private var isPortrait = true
@State var hideDpad = false
@State var hideABXY = false
@Environment(\.verticalSizeClass) var verticalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass
@ -45,16 +46,22 @@ struct ControllerView: View {
VStack(spacing: 15) { VStack(spacing: 15) {
ShoulderButtonsViewLeft() ShoulderButtonsViewLeft()
ZStack { ZStack {
Joystick() JoystickController(showBackground: $hideDpad)
if !hideDpad {
DPadView() DPadView()
.animation(.easeInOut(duration: 0.2), value: hideDpad)
}
} }
} }
VStack(spacing: 15) { VStack(spacing: 15) {
ShoulderButtonsViewRight() ShoulderButtonsViewRight()
ZStack { ZStack {
Joystick(iscool: true) JoystickController(iscool: true, showBackground: $hideABXY)
if !hideABXY {
ABXYView() ABXYView()
.animation(.easeInOut(duration: 0.2), value: hideABXY)
}
} }
} }
} }
@ -63,11 +70,11 @@ struct ControllerView: View {
HStack { HStack {
ButtonView(button: .leftStick) ButtonView(button: .leftStick)
.padding() .padding()
ButtonView(button: .start) ButtonView(button: .back)
} }
HStack { HStack {
ButtonView(button: .back) ButtonView(button: .start)
ButtonView(button: .rightStick) ButtonView(button: .rightStick)
.padding() .padding()
} }
@ -81,11 +88,14 @@ struct ControllerView: View {
Spacer() Spacer()
HStack { HStack {
VStack(spacing: 15) { VStack(spacing: 20) {
ShoulderButtonsViewLeft() ShoulderButtonsViewLeft()
ZStack { ZStack {
Joystick() JoystickController(showBackground: $hideDpad)
if !hideDpad {
DPadView() DPadView()
.animation(.easeInOut(duration: 0.2), value: hideDpad)
}
} }
} }
@ -95,11 +105,14 @@ struct ControllerView: View {
Spacer() Spacer()
VStack(spacing: 15) { VStack(spacing: 20) {
ShoulderButtonsViewRight() ShoulderButtonsViewRight()
ZStack { ZStack {
Joystick(iscool: true) JoystickController(iscool: true, showBackground: $hideABXY)
if !hideABXY {
ABXYView() ABXYView()
.animation(.easeInOut(duration: 0.2), value: hideABXY)
}
} }
} }
} }
@ -199,11 +212,11 @@ struct DPadView: View {
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
VStack(spacing: 5) { VStack(spacing: 7) {
ButtonView(button: .dPadUp) ButtonView(button: .dPadUp)
HStack(spacing: 20) { HStack(spacing: 22) {
ButtonView(button: .dPadLeft) ButtonView(button: .dPadLeft)
Spacer(minLength: 20) Spacer(minLength: 22)
ButtonView(button: .dPadRight) ButtonView(button: .dPadRight)
} }
ButtonView(button: .dPadDown) ButtonView(button: .dPadDown)
@ -224,11 +237,11 @@ struct ABXYView: View {
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
VStack(spacing: 5) { VStack(spacing: 7) {
ButtonView(button: .X) ButtonView(button: .X)
HStack(spacing: 20) { HStack(spacing: 22) {
ButtonView(button: .Y) ButtonView(button: .Y)
Spacer(minLength: 20) Spacer(minLength: 22)
ButtonView(button: .A) ButtonView(button: .A)
} }
ButtonView(button: .B) ButtonView(button: .B)
@ -244,129 +257,180 @@ struct ABXYView: View {
} }
} }
struct ButtonView: View { struct ButtonView: View {
var button: VirtualControllerButton var button: VirtualControllerButton
@State private var width: CGFloat = 45
@State private var height: CGFloat = 45
@State private var isPressed = false
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.presentationMode) var presentationMode
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State private var debounceTimer: Timer? @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 { var body: some View {
Image(systemName: buttonText)
.resizable()
.scaledToFit()
.frame(width: width, height: height)
.foregroundColor(true ? Color.white.opacity(0.9) : Color.black.opacity(0.9))
.background(
Group {
if !button.isTrigger {
Circle() Circle()
.fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3)) .foregroundStyle(.clear.opacity(0))
.frame(width: width * 1.25, height: height * 1.25) .overlay {
} else { Image(systemName: buttonConfig.iconName)
Image(systemName: buttonText)
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: width * 1.25, height: height * 1.25) .frame(width: size.width, height: size.height)
.foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3)) .foregroundStyle(.white)
}
}
)
.opacity(isPressed ? 0.6 : 1.0) .opacity(isPressed ? 0.6 : 1.0)
.allowsHitTesting(false)
}
.frame(width: size.width, height: size.height)
.background(
buttonBackground
)
.gesture( .gesture(
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { _ in .onChanged { _ in handleButtonPress() }
handleButtonPress() .onEnded { _ in handleButtonRelease() }
}
.onEnded { _ in
handleButtonRelease()
}
) )
.onAppear { .onAppear {
configureSizeForButton() 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() { private func handleButtonPress() {
if !isPressed { 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 isPressed = true
debounceTimer?.invalidate()
Ryujinx.shared.virtualController.setButtonState(1, for: button) Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.medium) Haptics.shared.play(.medium)
} }
} }
private func handleButtonRelease() { private func handleButtonRelease() {
if isPressed { if istoggle { return }
isPressed = false
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { _ in guard isPressed else { return }
isPressed = false
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.05) {
Ryujinx.shared.virtualController.setButtonState(0, for: button) Ryujinx.shared.virtualController.setButtonState(0, for: button)
} }
} }
}
private func configureSizeForButton() { private func calculateButtonSize() -> CGSize {
let baseWidth: CGFloat
let baseHeight: CGFloat
if button.isTrigger { if button.isTrigger {
width = 70 baseWidth = 70
height = 40 baseHeight = 40
} else if button.isSmall { } else if button.isSmall {
width = 35 baseWidth = 35
height = 35 baseHeight = 35
} else {
baseWidth = 45
baseHeight = 45
} }
// Adjust for iPad let deviceMultiplier = UIDevice.current.userInterfaceIdiom == .pad ? 1.2 : 1.0
if UIDevice.current.systemName.contains("iPadOS") { let scaleMultiplier = CGFloat(controllerScale)
width *= 1.2
height *= 1.2 return CGSize(
width: baseWidth * deviceMultiplier * scaleMultiplier,
height: baseHeight * deviceMultiplier * scaleMultiplier
)
} }
width *= CGFloat(controllerScale) // Centralized button configuration
height *= CGFloat(controllerScale) private var buttonConfig: ButtonConfiguration {
}
private var buttonText: String {
switch button { switch button {
case .A: case .A:
return "a.circle.fill" return ButtonConfiguration(iconName: "a.circle.fill")
case .B: case .B:
return "b.circle.fill" return ButtonConfiguration(iconName: "b.circle.fill")
case .X: case .X:
return "x.circle.fill" return ButtonConfiguration(iconName: "x.circle.fill")
case .Y: case .Y:
return "y.circle.fill" return ButtonConfiguration(iconName: "y.circle.fill")
case .leftStick: case .leftStick:
return "l.joystick.press.down.fill" return ButtonConfiguration(iconName: "l.joystick.press.down.fill")
case .rightStick: case .rightStick:
return "r.joystick.press.down.fill" return ButtonConfiguration(iconName: "r.joystick.press.down.fill")
case .dPadUp: case .dPadUp:
return "arrowtriangle.up.circle.fill" return ButtonConfiguration(iconName: "arrowtriangle.up.circle.fill")
case .dPadDown: case .dPadDown:
return "arrowtriangle.down.circle.fill" return ButtonConfiguration(iconName: "arrowtriangle.down.circle.fill")
case .dPadLeft: case .dPadLeft:
return "arrowtriangle.left.circle.fill" return ButtonConfiguration(iconName: "arrowtriangle.left.circle.fill")
case .dPadRight: case .dPadRight:
return "arrowtriangle.right.circle.fill" return ButtonConfiguration(iconName: "arrowtriangle.right.circle.fill")
case .leftTrigger: case .leftTrigger:
return "zl.rectangle.roundedtop.fill" return ButtonConfiguration(iconName: "zl.rectangle.roundedtop.fill")
case .rightTrigger: case .rightTrigger:
return "zr.rectangle.roundedtop.fill" return ButtonConfiguration(iconName: "zr.rectangle.roundedtop.fill")
case .leftShoulder: case .leftShoulder:
return "l.rectangle.roundedbottom.fill" return ButtonConfiguration(iconName: "l.rectangle.roundedbottom.fill")
case .rightShoulder: case .rightShoulder:
return "r.rectangle.roundedbottom.fill" return ButtonConfiguration(iconName: "r.rectangle.roundedbottom.fill")
case .start: case .start:
return "plus.circle.fill" return ButtonConfiguration(iconName: "plus.circle.fill")
case .back: case .back:
return "minus.circle.fill" return ButtonConfiguration(iconName: "minus.circle.fill")
case .guide: case .guide:
return "house.circle.fill" return ButtonConfiguration(iconName: "house.circle.fill")
} }
} }
struct ButtonConfiguration {
let iconName: String
}
} }

View File

@ -15,7 +15,6 @@ class Haptics {
private init() { } private init() { }
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) { func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
print("haptics")
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred() 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? @Binding var startgame: Game?
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase
@State private var isInBackground = false
@AppStorage("location-enabled") var locationenabled: Bool = false
var body: some View { var body: some View {
ZStack { ZStack {
if isAirplaying { if isAirplaying {
@ -26,7 +29,7 @@ struct EmulationView: View {
.ignoresSafeArea() .ignoresSafeArea()
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
.onAppear { .onAppear {
Air.play(AnyView(MetalView().ignoresSafeArea())) Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
} }
} else { } else {
MetalView() // The Emulation View MetalView() // The Emulation View
@ -62,37 +65,50 @@ struct EmulationView: View {
Spacer() Spacer()
} }
Spacer()
if ssb { if ssb {
HStack { HStack {
Button { Image(systemName: "arrow.left.circle")
if let screenshot = Ryujinx.shared.emulationUIView?.screenshot() { .resizable()
UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil) .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()
} }
} }
Spacer()
} }
} }
} }
.onAppear { .onAppear {
LocationManager.sharedInstance.startUpdatingLocation()
Air.shared.connectionCallbacks.append { cool in Air.shared.connectionCallbacks.append { cool in
DispatchQueue.main.async { DispatchQueue.main.async {
isAirplaying = cool 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,8 +87,11 @@ class MeloMTKView: MTKView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event) super.touchesBegan(touches, with: event)
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9) setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
if !disabled {
for touch in touches { for touch in touches {
let location = touch.location(in: self) let location = touch.location(in: self)
if scaleToTargetResolution(location) == nil { if scaleToTargetResolution(location) == nil {
@ -100,16 +103,20 @@ class MeloMTKView: MTKView {
let index = activeTouches.firstIndex(of: touch)! let index = activeTouches.firstIndex(of: touch)!
let scaledLocation = scaleToTargetResolution(location)! let scaledLocation = scaleToTargetResolution(location)!
print("Touch began at: \(scaledLocation) and \(self.aspectRatio)") // // print("Touch began at: \(scaledLocation) and \(self.aspectRatio)")
touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index)) touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
} }
} }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event) super.touchesEnded(touches, with: event)
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9) setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
if !disabled {
for touch in touches { for touch in touches {
if ignoredTouches.contains(touch) { if ignoredTouches.contains(touch) {
ignoredTouches.remove(touch) ignoredTouches.remove(touch)
@ -119,17 +126,21 @@ class MeloMTKView: MTKView {
if let index = activeTouches.firstIndex(of: touch) { if let index = activeTouches.firstIndex(of: touch) {
activeTouches.remove(at: index) activeTouches.remove(at: index)
print("Touch ended for index \(index)") // // print("Touch ended for index \(index)")
touch_ended(Int32(index)) touch_ended(Int32(index))
} }
} }
} }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event) super.touchesMoved(touches, with: event)
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9) setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
if !disabled {
for touch in touches { for touch in touches {
if ignoredTouches.contains(touch) { if ignoredTouches.contains(touch) {
continue continue
@ -139,16 +150,17 @@ class MeloMTKView: MTKView {
guard let scaledLocation = scaleToTargetResolution(location) else { guard let scaledLocation = scaleToTargetResolution(location) else {
if let index = activeTouches.firstIndex(of: touch) { if let index = activeTouches.firstIndex(of: touch) {
activeTouches.remove(at: index) activeTouches.remove(at: index)
print("Touch left active area, removed index \(index)") // // print("Touch left active area, removed index \(index)")
touch_ended(Int32(index)) touch_ended(Int32(index))
} }
continue continue
} }
if let index = activeTouches.firstIndex(of: touch) { if let index = activeTouches.firstIndex(of: touch) {
print("Touch moved to: \(scaledLocation)") // // print("Touch moved to: \(scaledLocation)")
touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index)) touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
} }
} }
} }
} }
}

View File

@ -1,512 +0,0 @@
//
// GameListView.swift
// MeloNX
//
// Created by Stossy11 on 3/11/2024.
//
import SwiftUI
import UniformTypeIdentifiers
extension UTType {
static let nsp = UTType(exportedAs: "com.nintendo.switch-package")
static let xci = UTType(exportedAs: "com.nintendo.switch-cartridge")
}
struct GameLibraryView: View {
@Binding var startemu: Game?
// @State var importDLCs = false
@State private var searchText = ""
@State private var isSearching = false
@AppStorage("recentGames") private var recentGamesData: Data = Data()
@State private var recentGames: [Game] = []
@Environment(\.colorScheme) var colorScheme
@State var firmwareInstaller = false
@State var firmwareversion = "0"
@State var isImporting: Bool = false
@State var startgame = false
@State var isSelectingGameFile = false
@State var isViewingGameInfo: Bool = false
@State var isSelectingGameUpdate: Bool = false
@State var isSelectingGameDLC: Bool = false
@StateObject var ryujinx = Ryujinx.shared
@State var gameInfo: Game?
var games: Binding<[Game]> {
Binding(
get: { Ryujinx.shared.games },
set: { Ryujinx.shared.games = $0 }
)
}
var filteredGames: [Game] {
if searchText.isEmpty {
return Ryujinx.shared.games.filter { game in
!realRecentGames.contains(where: { $0.fileURL == game.fileURL })
}
}
return Ryujinx.shared.games.filter {
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
$0.developer.localizedCaseInsensitiveContains(searchText)
}
}
var realRecentGames: [Game] {
let games = Ryujinx.shared.games
return recentGames.compactMap { recentGame in
games.first(where: { $0.fileURL == recentGame.fileURL })
}
}
var body: some View {
iOSNav {
List {
if Ryujinx.shared.games.isEmpty {
VStack(spacing: 16) {
Image(systemName: "gamecontroller.fill")
.font(.system(size: 64))
.foregroundColor(.secondary.opacity(0.7))
.padding(.top, 60)
Text("No Games Found")
.font(.title2.bold())
.foregroundColor(.primary)
Text("Add ROM, Keys and Firmware to get started")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else {
if !isSearching && !realRecentGames.isEmpty {
Section {
ForEach(realRecentGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
removeFromRecentGames(game)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
} header: {
Text("Recent")
}
Section {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
}
} header: {
Text("Others")
}
} else {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
}
}
}
}
.navigationTitle("Games")
.navigationBarTitleDisplayMode(.large)
.onAppear {
loadRecentGames()
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmwareversion = (firmware == "" ? "0" : firmware)
}
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
switch result {
case .success(let url):
do {
let fun = url.startAccessingSecurityScopedResource()
let path = url.path
Ryujinx.shared.installFirmware(firmwarePath: path)
firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
if fun {
url.stopAccessingSecurityScopedResource()
}
}
case .failure(let error):
print(error)
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
isSelectingGameFile = true
isImporting = true
} label: {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .topBarLeading) {
Menu {
Text("Firmware Version: \(firmwareversion)")
.tint(.white)
if firmwareversion == "0" {
Button {
DispatchQueue.main.async {
firmwareInstaller.toggle()
}
} label: {
Text("Install Firmware")
}
} else {
Menu("Firmware") {
Button {
Ryujinx.shared.removeFirmware()
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmwareversion = (firmware == "" ? "0" : firmware)
} label: {
Text("Remove Firmware")
}
Button {
let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
self.startemu = game
} label: {
Text("Mii Maker")
}
}
}
Button {
isSelectingGameFile = false
isImporting = true
} label: {
Text("Open Game")
}
Button {
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
var sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
if ProcessInfo.processInfo.isiOSAppOnMac {
sharedurl = documentsUrl.absoluteString
}
print(sharedurl)
let furl = URL(string: sharedurl)!
if UIApplication.shared.canOpenURL(furl) {
UIApplication.shared.open(furl, options: [:])
}
} label: {
Text("Show MeloNX Folder")
}
} label: {
Image(systemName: "ellipsis.circle")
.foregroundColor(.blue)
}
}
ToolbarItem(placement: .topBarLeading) {
if ryujinx.jitenabled {
Image(systemName: "checkmark")
.foregroundStyle(.green)
}
}
}
.onChange(of: startemu) { game in
guard let game else { return }
addToRecentGames(game)
}
}
.searchable(text: $searchText)
.animation(.easeInOut, value: searchText)
.onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty
}
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.folder, .nsp, .xci, .zip, .item]) { result in
if isSelectingGameFile {
switch result {
case .success(let url):
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
if !fileManager.fileExists(atPath: romsDirectory.path) {
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
}
let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
try fileManager.copyItem(at: url, to: destinationURL)
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Error copying game file: \(error)")
}
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
}
} else {
switch result {
case .success(let url):
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
do {
let handle = try FileHandle(forReadingFrom: url)
let fileExtension = (url.pathExtension as NSString).utf8String
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
DispatchQueue.main.async {
startemu = game
}
} catch {
print(error)
}
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
}
}
}
.sheet(isPresented: $isSelectingGameUpdate) {
UpdateManagerSheet(game: $gameInfo)
}
.sheet(isPresented: $isSelectingGameDLC) {
DLCManagerSheet(game: $gameInfo)
}
.sheet(isPresented: Binding(
get: { isViewingGameInfo && gameInfo != nil },
set: { newValue in
if !newValue {
isViewingGameInfo = false
gameInfo = nil
}
}
)) {
if let game = gameInfo {
GameInfoSheet(game: game)
}
}
}
private func addToRecentGames(_ game: Game) {
recentGames.removeAll { $0.titleId == game.titleId }
recentGames.insert(game, at: 0)
if recentGames.count > 5 {
recentGames = Array(recentGames.prefix(5))
}
saveRecentGames()
}
private func removeFromRecentGames(_ game: Game) {
recentGames.removeAll { $0.titleId == game.titleId }
saveRecentGames()
}
private func saveRecentGames() {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(recentGames)
recentGamesData = data
} catch {
print("Error saving recent games: \(error)")
}
}
private func loadRecentGames() {
do {
let decoder = JSONDecoder()
recentGames = try decoder.decode([Game].self, from: recentGamesData)
} catch {
print("Error loading recent games: \(error)")
recentGames = []
}
}
// MARK: - Delete Game Function
func deleteGame(game: Game) {
let fileManager = FileManager.default
do {
try fileManager.removeItem(at: game.fileURL)
Ryujinx.shared.games.removeAll { $0.id == game.id }
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Error deleting game: \(error)")
}
}
}
// MARK: - Game Model
extension Game: Codable {
enum CodingKeys: String, CodingKey {
case titleName, titleId, developer, version, fileURL
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
titleName = try container.decode(String.self, forKey: .titleName)
titleId = try container.decode(String.self, forKey: .titleId)
developer = try container.decode(String.self, forKey: .developer)
version = try container.decode(String.self, forKey: .version)
fileURL = try container.decode(URL.self, forKey: .fileURL)
// Initialize other properties
self.containerFolder = fileURL.deletingLastPathComponent()
self.fileType = .item
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(titleName, forKey: .titleName)
try container.encode(titleId, forKey: .titleId)
try container.encode(developer, forKey: .developer)
try container.encode(version, forKey: .version)
try container.encode(fileURL, forKey: .fileURL)
}
}
// MARK: - Game List Item
struct GameListRow: View {
let game: Game
@Binding var startemu: Game?
@Binding var games: [Game] // Add this binding
@Binding var isViewingGameInfo: Bool
@Binding var isSelectingGameUpdate: Bool
@Binding var isSelectingGameDLC: Bool
@Binding var gameInfo: Game?
@State var gametoDelete: Game?
@State var showGameDeleteConfirmation: Bool = false
@Environment(\.colorScheme) var colorScheme
@AppStorage("portal") var gamepo = false
var body: some View {
Button(action: {
startemu = game
}) {
HStack(spacing: 16) {
// Game Icon
if let icon = game.icon {
Image(uiImage: icon)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 45, height: 45)
.cornerRadius(8)
} else {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(colorScheme == .dark ?
Color(.systemGray5) : Color(.systemGray6))
.frame(width: 45, height: 45)
Image(systemName: "gamecontroller.fill")
.font(.system(size: 20))
.foregroundColor(.gray)
}
}
// Game Info
VStack(alignment: .leading, spacing: 2) {
Text(game.titleName)
.font(.body)
.foregroundColor(.primary)
Text(game.developer)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "play.circle.fill")
.font(.title2)
.foregroundColor(.accentColor)
.opacity(0.8)
}
}
.contextMenu {
Section {
Button {
startemu = game
} label: {
Label("Play Now", systemImage: "play.fill")
}
Button {
gameInfo = game
isViewingGameInfo.toggle()
if game.titleName.lowercased() == "portal" {
gamepo = true
} else if game.titleName.lowercased() == "portal 2" {
gamepo = true
}
} label: {
Label("Game Info", systemImage: "info.circle")
}
}
Section {
Button {
gameInfo = game
isSelectingGameUpdate.toggle()
} label: {
Label("Game Update Manager", systemImage: "chevron.up.circle")
}
Button {
gameInfo = game
isSelectingGameDLC.toggle()
} label: {
Label("Game DLC Manager", systemImage: "plus.viewfinder")
}
}
Section {
Button(role: .destructive) {
gametoDelete = game
showGameDeleteConfirmation.toggle()
} label: {
Label("Delete", systemImage: "trash")
}
}
}
.confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
Button("Delete", role: .destructive) {
if let game = gametoDelete {
deleteGame(game: game)
}
}
Button("Cancel", role: .cancel) {}
} message: {
Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?")
}
}
private func deleteGame(game: Game) {
let fileManager = FileManager.default
do {
try fileManager.removeItem(at: game.fileURL)
games.removeAll { $0.id == game.id }
} catch {
print("Error deleting game: \(error)")
}
}
}

View File

@ -1,118 +0,0 @@
//
// LogEntry.swift
// MeloNX
//
// Created by Stossy11 on 09/02/2025.
//
import SwiftUI
struct LogFileView: View {
@State private var logs: [String] = []
@State private var showingLogs = false
public var isfps: Bool
private let fileManager = FileManager.default
private let maxDisplayLines = 10
private var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
return formatter
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
ForEach(logs.suffix(maxDisplayLines), id: \.self) { log in
Text(log)
.font(.caption)
.foregroundColor(.white)
.padding(4)
.background(Color.black.opacity(0.7))
.cornerRadius(4)
.transition(.opacity)
}
}
.onAppear {
startLogFileWatching()
}
.onChange(of: logs) { newLogs in
print("Logs updated: \(newLogs.count) entries")
}
}
private func getLatestLogFile() -> URL? {
let logsDirectory = URL.documentsDirectory.appendingPathComponent("Logs")
let currentDate = Date()
do {
try fileManager.createDirectory(at: logsDirectory, withIntermediateDirectories: true)
let logFiles = try fileManager.contentsOfDirectory(at: logsDirectory, includingPropertiesForKeys: [.creationDateKey])
.filter {
let filename = $0.lastPathComponent
guard filename.hasPrefix("MeloNX_") && filename.hasSuffix(".log") else {
return false
}
let dateString = filename.replacingOccurrences(of: "MeloNX_", with: "").replacingOccurrences(of: ".log", with: "")
guard let logDate = dateFormatter.date(from: dateString) else {
return false
}
return Calendar.current.isDate(logDate, inSameDayAs: currentDate)
}
let sortedLogFiles = logFiles.sorted {
$0.lastPathComponent > $1.lastPathComponent
}
return sortedLogFiles.first
} catch {
print("Error finding log files: \(error)")
return nil
}
}
private func readLatestLogFile() {
guard let logFileURL = getLatestLogFile() else {
print("no logs?")
return
}
print(logFileURL)
do {
let logContents = try String(contentsOf: logFileURL)
let allLines = logContents.components(separatedBy: .newlines)
DispatchQueue.global(qos: .userInteractive).async {
self.logs = Array(allLines)
}
} catch {
print("Error reading log file: \(error)")
}
}
private func startLogFileWatching() {
showingLogs = true
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
if showingLogs {
self.readLatestLogFile()
}
if isfps {
sleep(1)
if get_current_fps() != 0 {
stopLogFileWatching()
timer.invalidate()
}
}
}
}
private func stopLogFileWatching() {
showingLogs = false
}
}

View File

@ -1,799 +0,0 @@
//
// SettingsView.swift
// MeloNX
//
// Created by Stossy11 on 25/11/2024.
//
import SwiftUI
import SwiftSVG
struct SettingsView: View {
@Binding var config: Ryujinx.Configuration
@Binding var MoltenVKSettings: [MoltenVKSettings]
@Binding var controllersList: [Controller]
@Binding var currentControllers: [Controller]
@Binding var onscreencontroller: Controller
@AppStorage("useTrollStore") var useTrollStore: Bool = false
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
var memoryManagerModes = [
("HostMapped", "Host (fast)"),
("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
("SoftwarePageTable", "Software (slow)"),
]
@AppStorage("RyuDemoControls") var ryuDemo: Bool = false
@AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false
@AppStorage("showScreenShotButton") var ssb: Bool = false
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = false
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
@AppStorage("performacehud") var performacehud: Bool = false
@AppStorage("oldWindowCode") var windowCode: Bool = false
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
@AppStorage("showlogsloading") var showlogsloading: Bool = true
@AppStorage("showlogsgame") var showlogsgame: Bool = false
@AppStorage("stick-button") var stickButton = false
@AppStorage("waitForVPN") var waitForVPN = false
@State private var showResolutionInfo = false
@State private var showAnisotropicInfo = false
@State private var showControllerInfo = false
@State private var searchText = ""
@AppStorage("portal") var gamepo = false
@StateObject var ryujinx = Ryujinx.shared
var filteredMemoryModes: [(String, String)] {
guard !searchText.isEmpty else { return memoryManagerModes }
return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
}
var body: some View {
iOSNav {
List {
// Graphics & Performance
Section {
Picker(selection: $config.aspectRatio) {
ForEach(AspectRatio.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
}
.tint(.blue)
Toggle(isOn: $config.disableShaderCache) {
labelWithIcon("Shader Cache", iconName: "memorychip")
}
.tint(.blue)
Toggle(isOn: $config.disablevsync) {
labelWithIcon("Disable VSync", iconName: "arrow.triangle.2.circlepath")
}
.tint(.blue)
Toggle(isOn: $config.enableTextureRecompression) {
labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical")
}
.tint(.blue)
Toggle(isOn: $config.disableDockedMode) {
labelWithIcon("Docked Mode", iconName: "dock.rectangle")
}
.tint(.blue)
Toggle(isOn: $config.macroHLE) {
labelWithIcon("Macro HLE", iconName: "gearshape")
}.tint(.blue)
VStack(alignment: .leading, spacing: 10) {
HStack {
labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
.font(.headline)
Spacer()
Button {
showResolutionInfo.toggle()
} label: {
Image(systemName: "info.circle")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help("Learn more about Resolution Scale")
.alert(isPresented: $showResolutionInfo) {
Alert(
title: Text("Resolution Scale"),
message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
dismissButton: .default(Text("OK"))
)
}
}
Slider(value: $config.resscale, in: 0.1...3.0, step: 0.05) {
Text("Resolution Scale")
} minimumValueLabel: {
Text("0.1x")
.font(.footnote)
.foregroundColor(.secondary)
} maximumValueLabel: {
Text("3.0x")
.font(.footnote)
.foregroundColor(.secondary)
}
Text("\(config.resscale, specifier: "%.2f")x")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.vertical, 8)
VStack(alignment: .leading, spacing: 10) {
HStack {
labelWithIcon("Max Anisotropic Scale", iconName: "magnifyingglass")
.font(.headline)
Spacer()
Button {
showAnisotropicInfo.toggle()
} label: {
Image(systemName: "info.circle")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help("Learn more about Max Anisotropic Scale")
.alert(isPresented: $showAnisotropicInfo) {
Alert(
title: Text("Max Anisotripic Scale"),
message: Text("Adjust the internal Anisotropic resolution. Higher values improve visuals but may reduce performance. Default at 0 lets game decide."),
dismissButton: .default(Text("OK"))
)
}
}
Slider(value: $config.maxAnisotropy, in: 0...16.0, step: 0.1) {
Text("Resolution Scale")
} minimumValueLabel: {
Text("0x")
.font(.footnote)
.foregroundColor(.secondary)
} maximumValueLabel: {
Text("16.0x")
.font(.footnote)
.foregroundColor(.secondary)
}
Text("\(config.maxAnisotropy, specifier: "%.2f")x")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.vertical, 8)
Toggle(isOn: $performacehud) {
labelWithIcon("Custom Performance Overlay", iconName: "speedometer")
}
.tint(.blue)
} header: {
Text("Graphics & Performance")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Fine-tune graphics and performance to suit your device and preferences.")
}
// Input Selector
Section {
if !controllersList.filter({ !currentControllers.contains($0) }).isEmpty {
DisclosureGroup("Unselected Controllers") {
ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in
var customBinding: Binding<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: $stickButton) {
labelWithIcon("Show Stick Buttons", iconName: "l.joystick.press.down")
}.tint(.blue)
Toggle(isOn: $ryuDemo) {
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
}
.tint(.blue)
.disabled(true)
VStack(alignment: .leading, spacing: 10) {
HStack {
labelWithIcon("On-Screen Controller Scale", iconName: "magnifyingglass")
.font(.headline)
Spacer()
Button {
showControllerInfo.toggle()
} label: {
Image(systemName: "info.circle")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help("Learn more about On-Screen Controller Scale")
.alert(isPresented: $showControllerInfo) {
Alert(
title: Text("On-Screen Controller Scale"),
message: Text("Adjust the On-Screen Controller size."),
dismissButton: .default(Text("OK"))
)
}
}
Slider(value: $controllerScale, in: 0.1...3.0, step: 0.05) {
Text("Resolution Scale")
} minimumValueLabel: {
Text("0.1x")
.font(.footnote)
.foregroundColor(.secondary)
} maximumValueLabel: {
Text("3.0x")
.font(.footnote)
.foregroundColor(.secondary)
}
Text("\(controllerScale, specifier: "%.2f")x")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.vertical, 8)
} header: {
Text("Input Settings")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Configure input devices and on-screen controls for easier navigation and play.")
}
// Language and Region Settings
Section {
Picker(selection: $config.language) {
ForEach(SystemLanguage.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Language", iconName: "character.bubble")
}
Picker(selection: $config.regioncode) {
ForEach(SystemRegionCode.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Region", iconName: "globe")
}
// globe
} header: {
Text("Language and Region Settings")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Configure the System Language and the Region.")
}
// CPU Mode
Section {
if filteredMemoryModes.isEmpty {
Text("No matches for \"\(searchText)\"")
.foregroundColor(.secondary)
} else {
Picker(selection: $config.memoryManagerMode) {
ForEach(filteredMemoryModes, id: \.0) { key, displayName in
Text(displayName).tag(key)
}
} label: {
labelWithIcon("Memory Manager Mode", iconName: "gearshape")
}
}
Toggle(isOn: $config.disablePTC) {
labelWithIcon("Disable PTC", iconName: "cpu")
}.tint(.blue)
if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
if #available (iOS 16.4, *) {
Toggle(isOn: .constant(false)) {
labelWithIcon("Hypervisor", iconName: "bolt")
}
.tint(.blue)
.disabled(true)
.onAppear() {
print("CPU Info: \(gpuInfo)")
}
} else if checkAppEntitlement("com.apple.private.hypervisor") {
Toggle(isOn: $config.hypervisor) {
labelWithIcon("Hypervisor", iconName: "bolt")
}
.tint(.blue)
.onAppear() {
print("CPU Info: \(gpuInfo)")
}
}
}
} header: {
Text("CPU")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Select how memory is managed. 'Host (fast)' is best for most users.")
}
Section {
Toggle(isOn: $config.expandRam) {
labelWithIcon("Expand Guest Ram (6GB)", iconName: "exclamationmark.bubble")
}
.tint(.red)
Toggle(isOn: $config.ignoreMissingServices) {
labelWithIcon("Ignore Missing Services", iconName: "waveform.path")
}
.tint(.red)
} header: {
Text("Hacks")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
}
// Other Settings
Section {
Toggle(isOn: $ssb) {
labelWithIcon("Screenshot Button", iconName: "square.and.arrow.up")
}
.tint(.blue)
if #available(iOS 17.0.1, *) {
Toggle(isOn: $jitStreamerEB) {
labelWithIcon("JitStreamer EB", iconName: "bolt.heart")
}
.tint(.blue)
.contextMenu {
Button {
waitForVPN.toggle()
} label: {
Label {
Text("Wait for VPN")
} icon: {
if waitForVPN {
Image(systemName: "checkmark")
}
}
}
Button {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let mainWindow = windowScene.windows.last {
let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best, most kind, helpful and nice developers of all time jkcoxson <3", preferredStyle: .alert)
let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
}
alertController.addAction(learnMoreButton)
let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil)
alertController.addAction(doneButton)
mainWindow.rootViewController?.present(alertController, animated: true)
}
} label: {
Text("About")
}
}
} else {
Toggle(isOn: $useTrollStore) {
labelWithIcon("TrollStore JIT", iconName: "troll.svg")
}
.tint(.blue)
}
Toggle(isOn: $syncqsubmits) {
labelWithIcon("MVK: Synchronous Queue Submits", iconName: "line.diagonal")
}.tint(.blue)
.contextMenu() {
Button {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let mainWindow = windowScene.windows.last {
let alertController = UIAlertController(title: "About MVK: Synchronous Queue Submits", message: "Enable this option if Mario Kart 8 is crashing at Grand Prix mode.", preferredStyle: .alert)
let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alertController.addAction(doneButton)
mainWindow.rootViewController?.present(alertController, animated: true)
}
} label: {
Text("About")
}
}
DisclosureGroup {
Toggle(isOn: $showlogsloading) {
labelWithIcon("Show logs while loading", iconName: "text.alignleft")
}.tint(.blue)
Toggle(isOn: $showlogsgame) {
labelWithIcon("Show logs in-game", iconName: "text.line.magnify")
}.tint(.blue)
Toggle(isOn: $config.debuglogs) {
labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble")
}
.tint(.blue)
Toggle(isOn: $config.tracelogs) {
labelWithIcon("Trace Logs", iconName: "waveform.path")
}
.tint(.blue)
} label: {
Text("Logs")
}
} header: {
Text("Miscellaneous Options")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Enable trace and debug logs for advanced troubleshooting (Note: This degrades performance),\nEnable Screenshot Button for better screenshots\nand Enable TrollStore for automatic TrollStore JIT.")
}
// Info
Section {
let totalMemory = ProcessInfo.processInfo.physicalMemory
let model = getDeviceModel()
let deviceType = model.hasPrefix("iPad") ? "iPadOS" :
model.hasPrefix("iPhone") ? "iOS" :
"macOS"
let iconName = model.hasPrefix("iPad") ? "ipad.landscape" :
model.hasPrefix("iPhone") ? "iphone" :
"macwindow"
labelWithIcon("JIT Acquisition: \(ryujinx.jitenabled ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
.onAppear() {
print("JIY ;(((((")
ryujinx.ryuIsJITEnabled()
}
labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
labelWithIcon("Device: \(getDeviceModel())", iconName: iconName)
if ProcessInfo.processInfo.isiOSAppOnMac {
labelWithIcon("Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill")
} else {
labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill")
}
labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo")
} header: {
Text("Information")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Shows info about Memory, Entitlement and JIT.")
}
// Advanced
Section {
DisclosureGroup {
Toggle(isOn: $config.dfsIntegrityChecks) {
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
}.tint(.blue)
HStack {
labelWithIcon("Page Size", iconName: "textformat.size")
Spacer()
Text("\(String(Int(getpagesize())))")
.foregroundColor(.secondary)
}
if MTLHud.shared.canMetalHud {
Toggle(isOn: $metalHUDEnabled) {
labelWithIcon("Metal Performance HUD", iconName: "speedometer")
}
.tint(.blue)
.onChange(of: metalHUDEnabled) { newValue in
MTLHud.shared.toggle()
}
}
Toggle(isOn: $ignoreJIT) {
labelWithIcon("Ignore JIT Popup", iconName: "cpu")
}.tint(.blue)
TextField("Additional Arguments", text: Binding(
get: {
config.additionalArgs.joined(separator: " ")
},
set: { newValue in
config.additionalArgs = newValue
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
}
))
.textInputAutocapitalization(.none)
.disableAutocorrection(true)
Button {
finishedStorage = false
} label: {
Text("Show Setup")
.font(.body)
}
} label: {
Text("Advanced Options")
}
} header: {
Text("Advanced")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("For advanced users. See page size or add custom arguments for experimental features, \"Metal Performance HUD\" is not needed if you have it enabled in settings. \n \n\(gamepo ? "the cake is a lie" : "")")
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.listStyle(.insetGrouped)
.onAppear {
mVKPreFillBuffer = false
if let configs = loadSettings() {
self.config = configs
} else {
saveSettings()
}
}
.onChange(of: config) { _ in
saveSettings()
}
}
.navigationViewStyle(.stack)
}
private func toggleController(_ controller: Controller) {
if currentControllers.contains(where: { $0.id == controller.id }) {
currentControllers.removeAll(where: { $0.id == controller.id })
} else {
currentControllers.append(controller)
}
}
func saveSettings() {
MeloNX.saveSettings(config: config)
}
func getDeviceModel() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
return identifier
}
func getGPUInfo() -> String? {
let device = MTLCreateSystemDefaultDevice()
let gpu = device?.name
print("GPU: " + (gpu ?? ""))
return gpu
}
@ViewBuilder
private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
HStack(spacing: 8) {
if iconName.hasSuffix(".svg"){
if let flipimage, flipimage {
SVGView(svgName: iconName, color: .blue)
.symbolRenderingMode(.hierarchical)
.frame(width: 20, height: 20)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
} else {
SVGView(svgName: iconName, color: .blue)
.symbolRenderingMode(.hierarchical)
.frame(width: 20, height: 20)
}
} else if !iconName.isEmpty {
Image(systemName: iconName)
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)
}
Text(text)
}
.font(.body)
}
}
struct SVGView: UIViewRepresentable {
var svgName: String
var color: Color = Color.black
func makeUIView(context: Context) -> UIView {
var svgName = svgName
let hammock = UIView()
if svgName.hasSuffix(".svg") {
svgName.removeLast(4)
}
_ = UIView(svgNamed: svgName) { svgLayer in
svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color
svgLayer.resizeToFit(hammock.frame)
hammock.layer.addSublayer(svgLayer)
}
return hammock
}
func updateUIView(_ uiView: UIView, context: Context) {
// Update the SVG view's fill color when the color changes
if let svgLayer = uiView.layer.sublayers?.first as? CAShapeLayer {
svgLayer.fillColor = UIColor(color).cgColor
}
}
}
func saveSettings(config: Ryujinx.Configuration) {
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(config)
let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
try data.write(to: fileURL)
print("Settings saved to: \(fileURL.path)")
} catch {
print("Failed to save settings: \(error)")
}
}
func loadSettings() -> Ryujinx.Configuration? {
do {
let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
guard FileManager.default.fileExists(atPath: fileURL.path) else {
print("Config file does not exist at: \(fileURL.path)")
return nil
}
let data = try Data(contentsOf: fileURL)
let decoder = JSONDecoder()
let configs = try decoder.decode(Ryujinx.Configuration.self, from: data)
return configs
} catch {
print("Failed to load settings: \(error)")
return nil
}
}

View File

@ -10,6 +10,7 @@ import GameController
import Darwin import Darwin
import UIKit import UIKit
import MetalKit import MetalKit
import CoreLocation
struct MoltenVKSettings: Codable, Hashable { struct MoltenVKSettings: Codable, Hashable {
let string: String let string: String
@ -37,13 +38,14 @@ struct ContentView: View {
// JIT // JIT
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
@AppStorage("stikJIT") var stikJIT: Bool = false
// Other Configuration // Other Configuration
@State var isMK8: Bool = false @State var isMK8: Bool = false
@AppStorage("quit") var quit: Bool = false @AppStorage("quit") var quit: Bool = false
@State var quits: Bool = false @State var quits: Bool = false
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true @AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true @AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false @AppStorage("ignoreJIT") var ignoreJIT: Bool = false
// Loading Animation // Loading Animation
@ -79,7 +81,7 @@ struct ContentView: View {
_settings = State(initialValue: defaultSettings) _settings = State(initialValue: defaultSettings)
print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue) // print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
initializeSDL() initializeSDL()
} }
@ -119,7 +121,7 @@ struct ContentView: View {
private var jitErrorView: some View { private var jitErrorView: some View {
Text("") Text("")
.sheet(isPresented:Binding( .fullScreenCover(isPresented:Binding(
get: { !ryujinx.jitenabled }, get: { !ryujinx.jitenabled },
set: { newValue in set: { newValue in
ryujinx.jitenabled = newValue ryujinx.jitenabled = newValue
@ -130,7 +132,7 @@ struct ContentView: View {
JITPopover() { JITPopover() {
ryujinx.jitenabled = false ryujinx.jitenabled = false
} }
.interactiveDismissDisabled() // .interactiveDismissDisabled()
} }
} }
@ -153,21 +155,12 @@ struct ContentView: View {
} }
print(MTLHud.shared.isEnabled) // print(MTLHud.shared.isEnabled)
initControllerObservers() initControllerObservers()
Air.play(AnyView( Air.play(AnyView(
VStack { ControllerListView(game: $game)
Image(systemName: "gamecontroller")
.font(.system(size: 300))
.foregroundColor(.gray)
.padding(.bottom, 10)
Text("Select Game")
.font(.system(size: 150))
.bold()
}
)) ))
checkJitStatus() checkJitStatus()
@ -288,7 +281,7 @@ struct ContentView: View {
queue: .main queue: .main
) { notification in ) { notification in
if let controller = notification.object as? GCController { if let controller = notification.object as? GCController {
print("Controller connected: \(controller.productCategory)") // print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller) nativeControllers[controller] = .init(controller)
refreshControllersList() refreshControllersList()
} }
@ -300,7 +293,7 @@ struct ContentView: View {
queue: .main queue: .main
) { notification in ) { notification in
if let controller = notification.object as? GCController { if let controller = notification.object as? GCController {
print("Controller disconnected: \(controller.productCategory)") // print("Controller disconnected: \(controller.productCategory)")
nativeControllers[controller]?.cleanup() nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil nativeControllers[controller] = nil
refreshControllersList() refreshControllersList()
@ -354,7 +347,7 @@ struct ContentView: View {
do { do {
try ryujinx.start(with: config) try ryujinx.start(with: config)
} catch { } catch {
print("Error: \(error.localizedDescription)") // print("Error: \(error.localizedDescription)")
} }
} }
@ -365,7 +358,7 @@ struct ContentView: View {
} }
if syncqsubmits { if syncqsubmits {
setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "2", 1) setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "1", 1)
} }
} }
@ -377,13 +370,18 @@ struct ContentView: View {
private func checkJitStatus() { private func checkJitStatus() {
ryujinx.ryuIsJITEnabled() ryujinx.ryuIsJITEnabled()
if jitStreamerEB {
jitStreamerEB = false // byee jitstreamer eb
}
if !ryujinx.jitenabled { if !ryujinx.jitenabled {
if useTrollStore { if useTrollStore {
askForJIT() askForJIT()
} else if stikJIT {
enableJITStik()
} else if jitStreamerEB { } else if jitStreamerEB {
enableJITEB() enableJITEB()
} else { } else {
print("no JIT") // print("no JIT")
} }
} }
} }
@ -391,6 +389,8 @@ struct ContentView: View {
private func handleDeepLink(_ url: URL) { private func handleDeepLink(_ url: URL) {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" { components.host == "game" {
DispatchQueue.main.async {
refreshControllersList()
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value { if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = ryujinx.games.first(where: { $0.titleId == text }) game = ryujinx.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value { } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
@ -399,6 +399,7 @@ struct ContentView: View {
} }
} }
} }
}
extension Array { extension Array {
@inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows { @inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
@ -407,3 +408,136 @@ extension Array {
} }
} }
} }
class LocationManager: NSObject, CLLocationManagerDelegate {
private var locationManager: CLLocationManager
static let sharedInstance = LocationManager()
private override init() {
locationManager = CLLocationManager()
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.pausesLocationUpdatesAutomatically = false
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// print("wow")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location manager failed with: \(error)")
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if manager.authorizationStatus == .denied {
print("Location services are disabled in settings.")
} else {
startUpdatingLocation()
}
}
func stop() {
if UserDefaults.standard.bool(forKey: "location-enabled") {
locationManager.stopUpdatingLocation()
}
}
func startUpdatingLocation() {
if UserDefaults.standard.bool(forKey: "location-enabled") {
locationManager.requestAlwaysAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
}
}
}
struct ControllerListView: View {
@State private var selectedIndex = 0
@Binding var game: Game?
@ObservedObject private var ryujinx = Ryujinx.shared
var body: some View {
List(ryujinx.games.indices, id: \.self) { index in
let game = ryujinx.games[index]
HStack(spacing: 16) {
// Game Icon
Group {
if let icon = game.icon {
Image(uiImage: icon)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
ZStack {
RoundedRectangle(cornerRadius: 10)
Image(systemName: "gamecontroller.fill")
.font(.system(size: 24))
.foregroundColor(.gray)
}
}
}
.frame(width: 55, height: 55)
.cornerRadius(10)
// Game Info
VStack(alignment: .leading, spacing: 4) {
Text(game.titleName)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.primary)
HStack(spacing: 4) {
Text(game.developer)
if !game.version.isEmpty && game.version != "0" {
Text("")
Text("v\(game.version)")
}
}
.font(.system(size: 14))
.foregroundColor(.secondary)
}
Spacer()
}
.background(selectedIndex == index ? Color.blue.opacity(0.3) : .clear)
}
.onAppear(perform: setupControllerObservers)
}
private func setupControllerObservers() {
let dpadHandler: GCControllerDirectionPadValueChangedHandler = { _, _, yValue in
if yValue == 1.0 {
selectedIndex = max(0, selectedIndex - 1)
} else if yValue == -1.0 {
selectedIndex = min(ryujinx.games.count - 1, selectedIndex + 1)
}
}
for controller in GCController.controllers() {
print("Controller connected: \(controller.vendorName ?? "Unknown")")
controller.playerIndex = .index1
controller.microGamepad?.dpad.valueChangedHandler = dpadHandler
controller.extendedGamepad?.dpad.valueChangedHandler = dpadHandler
controller.extendedGamepad?.buttonA.pressedChangedHandler = { _, _, pressed in
if pressed {
print("A button pressed")
game = ryujinx.games[selectedIndex]
}
}
}
NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main
) { _ in
setupControllerObservers()
}
}
}

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 { struct GameInfoSheet: View {
let game: Game let game: Game
@Environment(\.dismiss) var dismiss @Environment(\.presentationMode) var presentationMode
var body: some View { var body: some View {
iOSNav { iOSNav {
@ -44,7 +44,7 @@ struct GameInfoSheet: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text(game.developer) Text(game.developer)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundColor(.secondary)
} }
.padding(.vertical, 3) .padding(.vertical, 3)
} }
@ -56,7 +56,7 @@ struct GameInfoSheet: View {
Text("**Version**") Text("**Version**")
Spacer() Spacer()
Text(game.version) Text(game.version)
.foregroundStyle(Color.secondary) .foregroundColor(Color.secondary)
} }
HStack { HStack {
Text("**Title ID**") Text("**Title ID**")
@ -69,36 +69,36 @@ struct GameInfoSheet: View {
} }
Spacer() Spacer()
Text(game.titleId) Text(game.titleId)
.foregroundStyle(Color.secondary) .foregroundColor(Color.secondary)
} }
HStack { HStack {
Text("**Game Size**") Text("**Game Size**")
Spacer() Spacer()
Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes") Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes")
.foregroundStyle(Color.secondary) .foregroundColor(Color.secondary)
} }
HStack { HStack {
Text("**File Type**") Text("**File Type**")
Spacer() Spacer()
Text(getFileType(game.fileURL)) Text(getFileType(game.fileURL))
.foregroundStyle(Color.secondary) .foregroundColor(Color.secondary)
} }
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("**Game URL**") Text("**Game URL**")
Text(trimGameURL(game.fileURL)) Text(trimGameURL(game.fileURL))
.foregroundStyle(Color.secondary) .foregroundColor(Color.secondary)
} }
} header: { } header: {
Text("Information") Text("Information")
} }
.headerProminence(.increased) // .headerProminence(.increased)
} }
.navigationTitle(game.titleName) .navigationTitle(game.titleName)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .cancellationAction) {
Button("Done") { Button("Dismiss") {
dismiss() presentationMode.wrappedValue.dismiss()
} }
} }
} }
@ -113,7 +113,7 @@ struct GameInfoSheet: View {
return size return size
} }
} catch { } catch {
print("Error getting file size: \(error)") // print("Error getting file size: \(error)")
} }
return nil return nil
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ import SwiftUI
struct JITPopover: View { struct JITPopover: View {
var onJITEnabled: () -> Void var onJITEnabled: () -> Void
@Environment(\.dismiss) var dismiss @Environment(\.presentationMode) var presentationMode
@State var isJIT: Bool = false @State var isJIT: Bool = false
var body: some View { var body: some View {
@ -35,7 +35,7 @@ struct JITPopover: View {
if isJIT { if isJIT {
dismiss() presentationMode.wrappedValue.dismiss()
onJITEnabled() onJITEnabled()
Ryujinx.shared.ryuIsJITEnabled() 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

@ -33,6 +33,7 @@ struct MeloNXUpdateSheet: View {
Spacer() Spacer()
if #available(iOS 15.0, *) {
Button(action: { Button(action: {
if let url = URL(string: updateInfo.download_link) { if let url = URL(string: updateInfo.download_link) {
UIApplication.shared.open(url) UIApplication.shared.open(url)
@ -45,6 +46,19 @@ struct MeloNXUpdateSheet: View {
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.frame(alignment: .bottom) .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)
}
} }
.padding(.horizontal) .padding(.horizontal)
.navigationTitle("Version \(updateInfo.version_number) Available!") .navigationTitle("Version \(updateInfo.version_number) Available!")

View File

@ -46,7 +46,7 @@ struct DLCManagerSheet: View {
@Binding var game: Game! @Binding var game: Game!
@State private var isSelectingGameDLC = false @State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = [] @State private var dlcs: [DownloadableContentContainer] = []
@Environment(\.dismiss) private var dismiss @Environment(\.presentationMode) var presentationMode
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
@ -66,7 +66,7 @@ struct DLCManagerSheet: View {
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Done") { Button("Done") {
dismiss() presentationMode.wrappedValue.dismiss()
} }
} }
@ -127,15 +127,17 @@ struct DLCManagerSheet: View {
private func dlcRow(_ dlc: DownloadableContentContainer) -> some View { private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
Group {
if #available(iOS 15.0, *) {
Button { Button {
toggleDLC(dlc) toggleDLC(dlc)
} label: { } label: {
HStack { HStack {
Text(dlc.filename) Text(dlc.filename)
.foregroundStyle(.primary) .foregroundColor(.primary)
Spacer() Spacer()
Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle") Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
.foregroundStyle(dlc.isEnabled ? .primary : .secondary) .foregroundColor(dlc.isEnabled ? .primary : .secondary)
.imageScale(.large) .imageScale(.large)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
@ -150,6 +152,33 @@ struct DLCManagerSheet: View {
Label("Delete", systemImage: "trash") 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 // MARK: - Functions
@ -261,7 +290,7 @@ private extension DLCManagerSheet {
return result return result
} catch { } catch {
print("Error loading DLCs: \(error)") // print("Error loading DLCs: \(error)")
return [] return []
} }
} }
@ -300,7 +329,7 @@ extension Array where Element: AnyObject {
// MARK: - URL Extension // MARK: - URL Extension
extension URL { extension URL {
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above") @available(iOS, introduced: 14.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
static var documentsDirectory: URL { static var documentsDirectory: URL {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return documentDirectory return documentDirectory

View File

@ -14,7 +14,7 @@ struct UpdateManagerSheet: View {
@Binding var game: Game? @Binding var game: Game?
@State private var isSelectingGameUpdate = false @State private var isSelectingGameUpdate = false
@State private var jsonURL: URL? = nil @State private var jsonURL: URL? = nil
@Environment(\.dismiss) private var dismiss @Environment(\.presentationMode) var presentationMode
// MARK: - Models // MARK: - Models
class UpdateItem: Identifiable, ObservableObject { class UpdateItem: Identifiable, ObservableObject {
@ -51,7 +51,7 @@ struct UpdateManagerSheet: View {
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Done") { Button("Done") {
dismiss() presentationMode.wrappedValue.dismiss()
} }
} }
@ -106,15 +106,26 @@ struct UpdateManagerSheet: View {
} }
private func updateRow(_ update: UpdateItem) -> some View { private func updateRow(_ update: UpdateItem) -> some View {
Group {
if #available(iOS 15, *) {
updateRowNew(update)
} else {
updateRowOld(update)
}
}
}
@available(iOS 15, *)
private func updateRowNew(_ update: UpdateItem) -> some View {
Button { Button {
toggleSelection(update) toggleSelection(update)
} label: { } label: {
HStack { HStack {
Text(update.filename) Text(update.filename)
.foregroundStyle(.primary) .foregroundColor(.primary)
Spacer() Spacer()
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle") Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
.foregroundStyle(update.isSelected ? .primary : .secondary) .foregroundColor(update.isSelected ? .primary : .secondary)
.imageScale(.large) .imageScale(.large)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
@ -131,6 +142,31 @@ struct UpdateManagerSheet: View {
} }
} }
private func updateRowOld(_ update: UpdateItem) -> some View {
Button {
toggleSelection(update)
} label: {
HStack {
Text(update.filename)
.foregroundColor(.primary)
Spacer()
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
.foregroundColor(update.isSelected ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
}
.contextMenu {
Button {
if let index = updates.firstIndex(where: { $0.path == update.path }) {
removeUpdate(at: IndexSet(integer: index))
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
// MARK: - Functions // MARK: - Functions
private func loadData() { private func loadData() {
guard let game = game else { return } guard let game = game else { return }
@ -246,12 +282,12 @@ struct UpdateManagerSheet: View {
updates = updates.map { item in updates = updates.map { item in
var mutableItem = item var mutableItem = item
mutableItem.isSelected = item.path == update.path && !update.isSelected mutableItem.isSelected = item.path == update.path && !update.isSelected
print(mutableItem.isSelected) // print(mutableItem.isSelected)
print(update.isSelected) // print(update.isSelected)
return mutableItem return mutableItem
} }
print(updates) // print(updates)
saveJSON() saveJSON()
} }

View File

@ -8,8 +8,15 @@
import SwiftUI import SwiftUI
import UIKit import UIKit
import CryptoKit import CryptoKit
import UniformTypeIdentifiers
import AVFoundation
extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
}
}
@main @main
struct MeloNXApp: App { struct MeloNXApp: App {
@ -24,13 +31,23 @@ struct MeloNXApp: App {
@State var finished = false @State var finished = false
@AppStorage("hasbeenfinished") var finishedStorage: Bool = 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 { var body: some Scene {
WindowGroup { WindowGroup {
if finishedStorage { if finishedStorage {
ContentView() ContentView()
.withFileImporter()
.onAppear { .onAppear {
if checkForUpdate {
checkLatestVersion() checkLatestVersion()
} }
}
.sheet(isPresented: Binding( .sheet(isPresented: Binding(
get: { showOutOfDateSheet && updateInfo != nil }, get: { showOutOfDateSheet && updateInfo != nil },
set: { newValue in set: { newValue in
@ -47,15 +64,13 @@ struct MeloNXApp: App {
} else { } else {
SetupView(finished: $finished) SetupView(finished: $finished)
.onChange(of: finished) { newValue in .onChange(of: finished) { newValue in
withAnimation { withAnimation(.easeOut) {
withAnimation {
finishedStorage = newValue finishedStorage = newValue
} }
} }
} }
} }
} }
}
func checkLatestVersion() { func checkLatestVersion() {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
@ -64,22 +79,22 @@ struct MeloNXApp: App {
#if DEBUG #if DEBUG
let urlString = "http://192.168.178.116:8000/api/latest_release" let urlString = "http://192.168.178.116:8000/api/latest_release"
#else #else
let urlString = "https://melonx.org/api/latest_release" let urlString = "https://melonx.net/api/latest_release"
#endif #endif
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
print("Invalid URL") // print("Invalid URL")
return return
} }
let task = URLSession.shared.dataTask(with: url) { data, response, error in let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { if let error = error {
print("Error checking for new version: \(error)") // print("Error checking for new version: \(error)")
return return
} }
guard let data = data else { guard let data = data else {
print("No data received") // print("No data received")
return return
} }
@ -94,7 +109,7 @@ struct MeloNXApp: App {
} }
} }
} catch { } catch {
print("Failed to decode response: \(error)") // print("Failed to decode response: \(error)")
} }
} }

View File

@ -54,12 +54,17 @@ struct SetupView: View {
) { result in ) { result in
handleFirmwareImport(result: result) handleFirmwareImport(result: result)
} }
.alert(alertMessage, isPresented: $showAlert) { .alert(isPresented: $showAlert) {
Button("OK", role: .cancel) {} Alert(title: Text(alertMessage), dismissButton: .default(Text("OK")))
} }
.alert("Skip Setup?", isPresented: $showSkipAlert) { .alert(isPresented: $showSkipAlert) {
Button("Skip", role: .destructive) { finished = true } Alert(
Button("Cancel", role: .cancel) {} title: Text("Skip Setup?"),
primaryButton: .destructive(Text("Skip")) {
finished = true
},
secondaryButton: .cancel()
)
} }
.onAppear { .onAppear {
initialize() initialize()
@ -390,7 +395,7 @@ struct SetupView: View {
let iconFileName = iconFiles.last else { let iconFileName = iconFiles.last else {
print("Could not find icons in bundle") // print("Could not find icons in bundle")
return "" return ""
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 100 KiB

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> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>audio</string> <string>location</string>
<string>processing</string> <string>processing</string>
<string>audio</string>
</array> </array>
<key>UIFileSharingEnabled</key> <key>UIFileSharingEnabled</key>
<true/> <true/>

View File

@ -11,7 +11,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
class NoWxCache : IDisposable class NoWxCache : IDisposable
{ {
private const int CodeAlignment = 4; // Bytes. 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; 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 // How many calls to the same function we allow until we pad the shared cache to force the function to become available there

View File

@ -609,23 +609,64 @@ namespace Ryujinx.Graphics.Vulkan
public byte[] GetData(int x, int y, int width, int height) public byte[] GetData(int x, int y, int width, int height)
{ {
int size = width * height * Info.BytesPerPixel; const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
using var bufferHolder = _gd.BufferManager.Create(_gd, size);
int size = width * height * Info.BytesPerPixel;
byte[] bitmap = new byte[size];
if (size <= MaxChunkSize)
{
using var bufferHolder = _gd.BufferManager.Create(_gd, size);
using (var cbs = _gd.CommandBufferPool.Rent()) using (var cbs = _gd.CommandBufferPool.Rent())
{ {
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
var image = GetImage().Get(cbs).Value; var image = GetImage().Get(cbs).Value;
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height); CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height);
} }
bufferHolder.WaitForFences(); bufferHolder.WaitForFences();
byte[] bitmap = new byte[size];
GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span<byte>.Empty).CopyTo(bitmap); GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span<byte>.Empty).CopyTo(bitmap);
return 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() public PinnedSpan<byte> GetData()
{ {
BackgroundResource resources = _gd.BackgroundResources.Get(); BackgroundResource resources = _gd.BackgroundResources.Get();
@ -738,14 +779,28 @@ namespace Ryujinx.Graphics.Vulkan
return GetDataFromBuffer(result, size, result); 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)); int size = GetBufferDataLength(Info.GetMipSize(level));
if (size <= MaxChunkSize)
{
Span<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level); Span<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
return GetDataFromBuffer(result, size, result); 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/> /// <inheritdoc/>
public void SetData(MemoryOwner<byte> data) public void SetData(MemoryOwner<byte> data)
{ {
@ -769,7 +824,7 @@ namespace Ryujinx.Graphics.Vulkan
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null) 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; const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
int bufferDataLength = GetBufferDataLength(data.Length); int bufferDataLength = GetBufferDataLength(data.Length);

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.")] [Option("exclusive-fullscreen-height", Required = false, Default = 1080, HelpText = "Set vertical resolution for exclusive fullscreen mode.")]
public int ExclusiveFullscreenHeight { get; set; } 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 // Input
[Option("correct-controller", Required = false, Default = false, HelpText = "Makes the on-screen controller (iOS) buttons correspond to what they show.")] [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.")] [Option("aspect-ratio", Required = false, Default = AspectRatio.Fixed16x9, HelpText = "Aspect Ratio applied to the renderer window.")]
public AspectRatio AspectRatio { get; set; } 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; } 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.")] [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

@ -266,7 +266,6 @@ namespace Ryujinx.Headless.SDL2
[UnmanagedCallersOnly(EntryPoint = "initialize")] [UnmanagedCallersOnly(EntryPoint = "initialize")]
public static unsafe void Initialize() public static unsafe void Initialize()
{ {
AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)); AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
if (_virtualFileSystem == null) if (_virtualFileSystem == null)
@ -287,16 +286,6 @@ namespace Ryujinx.Headless.SDL2
{ {
_contentManager = new ContentManager(_virtualFileSystem); _contentManager = new ContentManager(_virtualFileSystem);
} }
if (_accountManager == null)
{
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, "");
}
if (_userChannelPersistence == null)
{
_userChannelPersistence = new UserChannelPersistence();
}
} }
static void Main(string[] args) static void Main(string[] args)
@ -403,11 +392,32 @@ namespace Ryujinx.Headless.SDL2
return String.Empty; 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")] [UnmanagedCallersOnly(EntryPoint = "stop_emulation")]
public static void StopEmulation() public static void StopEmulation()
{ {
if (_window != null) if (_window != null)
{ {
_window.Exit();
} }
} }
@ -886,21 +896,11 @@ namespace Ryujinx.Headless.SDL2
private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option) private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option)
{ {
if (inputId == null) if (inputId == null)
{
if (index == PlayerIndex.Player1)
{
Logger.Info?.Print(LogClass.Application, $"{index} not configured, defaulting to default keyboard.");
// Default to keyboard
inputId = "0";
}
else
{ {
Logger.Info?.Print(LogClass.Application, $"{index} not configured"); Logger.Info?.Print(LogClass.Application, $"{index} not configured");
return null; return null;
} }
}
IGamepad gamepad; IGamepad gamepad;
@ -990,12 +990,26 @@ namespace Ryujinx.Headless.SDL2
{ {
bool isNintendoStyle = true; // gamepadName.Contains("Nintendo") || gamepadName.Contains("Joycons"); 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 config = new StandardControllerInputConfig
{ {
Version = InputConfig.CurrentVersion, Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL2, Backend = InputBackendType.GamepadSDL2,
Id = null, Id = null,
ControllerType = ControllerType.JoyconPair, ControllerType = currentController,
DeadzoneLeft = 0.1f, DeadzoneLeft = 0.1f,
DeadzoneRight = 0.1f, DeadzoneRight = 0.1f,
RangeLeft = 1.0f, RangeLeft = 1.0f,
@ -1118,40 +1132,25 @@ namespace Ryujinx.Headless.SDL2
} }
static void Load(Options option) static void Load(Options option)
{
if (_virtualFileSystem == null)
{
_virtualFileSystem = VirtualFileSystem.CreateInstance();
}
if (_libHacHorizonManager == null)
{ {
_libHacHorizonManager = new LibHacHorizonManager(); _libHacHorizonManager = new LibHacHorizonManager();
_libHacHorizonManager.InitializeFsServer(_virtualFileSystem); _libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
_libHacHorizonManager.InitializeArpServer(); _libHacHorizonManager.InitializeArpServer();
_libHacHorizonManager.InitializeBcatServer(); _libHacHorizonManager.InitializeBcatServer();
_libHacHorizonManager.InitializeSystemClients(); _libHacHorizonManager.InitializeSystemClients();
}
if (_contentManager == null)
{
_contentManager = new ContentManager(_virtualFileSystem); _contentManager = new ContentManager(_virtualFileSystem);
}
if (_accountManager == null)
{
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile); _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
}
if (_userChannelPersistence == null)
{
_userChannelPersistence = new UserChannelPersistence(); _userChannelPersistence = new UserChannelPersistence();
}
if (_inputManager == null)
{
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver()); _inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
if (OperatingSystem.IsIOS())
{
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; GraphicsConfig.EnableShaderCache = true;

View File

@ -44,6 +44,9 @@ namespace Ryujinx.Headless.SDL2
_mainThreadActions.Enqueue(action); _mainThreadActions.Enqueue(action);
} }
public bool _isPaused;
public ManualResetEvent _pauseEvent;
public NpadManager NpadManager; public NpadManager NpadManager;
public TouchScreenManager TouchScreenManager; public TouchScreenManager TouchScreenManager;
public Switch Device; public Switch Device;
@ -104,6 +107,7 @@ namespace Ryujinx.Headless.SDL2
_gpuCancellationTokenSource = new CancellationTokenSource(); _gpuCancellationTokenSource = new CancellationTokenSource();
_exitEvent = new ManualResetEvent(false); _exitEvent = new ManualResetEvent(false);
_gpuDoneEvent = new ManualResetEvent(false); _gpuDoneEvent = new ManualResetEvent(false);
_pauseEvent = new ManualResetEvent(true);
_aspectRatio = aspectRatio; _aspectRatio = aspectRatio;
_enableMouse = enableMouse; _enableMouse = enableMouse;
HostUITheme = new HeadlessHostUiTheme(); HostUITheme = new HeadlessHostUiTheme();
@ -298,6 +302,8 @@ namespace Ryujinx.Headless.SDL2
return; return;
} }
_pauseEvent.WaitOne();
_ticks += _chrono.ElapsedTicks; _ticks += _chrono.ElapsedTicks;
_chrono.Restart(); _chrono.Restart();
@ -378,7 +384,6 @@ namespace Ryujinx.Headless.SDL2
{ {
while (_isActive) while (_isActive)
{ {
UpdateFrame(); UpdateFrame();
SDL_PumpEvents(); SDL_PumpEvents();

View File

@ -9,8 +9,18 @@ namespace Ryujinx.Input.SDL2
{ {
private readonly Dictionary<int, string> _gamepadsInstanceIdsMapping; private readonly Dictionary<int, string> _gamepadsInstanceIdsMapping;
private readonly List<string> _gamepadsIds; 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"; 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); Guid guid = SDL_JoystickGetDeviceGUID(joystickIndex);
@ -44,14 +54,16 @@ namespace Ryujinx.Input.SDL2
return null; return null;
} }
// Include joystickIndex at the start of the ID to maintain compatibility with GetJoystickIndexByGamepadId
return joystickIndex + "-" + guid; return joystickIndex + "-" + guid;
} }
private static int GetJoystickIndexByGamepadId(string id) private int GetJoystickIndexByGamepadId(string id)
{ {
string[] data = id.Split("-"); 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; return -1;
} }
@ -64,7 +76,11 @@ namespace Ryujinx.Input.SDL2
if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id)) if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id))
{ {
_gamepadsInstanceIdsMapping.Remove(joystickInstanceId); _gamepadsInstanceIdsMapping.Remove(joystickInstanceId);
lock (_lock)
{
_gamepadsIds.Remove(id); _gamepadsIds.Remove(id);
}
OnGamepadDisconnected?.Invoke(id); OnGamepadDisconnected?.Invoke(id);
} }
@ -74,6 +90,13 @@ namespace Ryujinx.Input.SDL2
{ {
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE) 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); string id = GenerateGamepadId(joystickDeviceId);
if (id == null) if (id == null)
@ -81,16 +104,21 @@ namespace Ryujinx.Input.SDL2
return; return;
} }
// Sometimes a JoyStick connected event fires after the app starts even though it was connected before // Check if we already have this gamepad ID in our list
// so it is rejected to avoid doubling the entries. lock (_lock)
{
if (_gamepadsIds.Contains(id)) if (_gamepadsIds.Contains(id))
{ {
return; return;
} }
}
if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id)) if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id))
{
lock (_lock)
{ {
_gamepadsIds.Add(id); _gamepadsIds.Add(id);
}
OnGamepadConnected?.Invoke(id); OnGamepadConnected?.Invoke(id);
} }
@ -104,12 +132,16 @@ namespace Ryujinx.Input.SDL2
SDL2Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected; SDL2Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected;
SDL2Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected; SDL2Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected;
// Simulate a full disconnect when disposing
foreach (string id in _gamepadsIds) foreach (string id in _gamepadsIds)
{ {
OnGamepadDisconnected?.Invoke(id); OnGamepadDisconnected?.Invoke(id);
} }
lock (_lock)
{
_gamepadsIds.Clear(); _gamepadsIds.Clear();
}
SDL2Driver.Instance.Dispose(); SDL2Driver.Instance.Dispose();
} }
@ -130,11 +162,6 @@ namespace Ryujinx.Input.SDL2
return null; return null;
} }
if (id != GenerateGamepadId(joystickIndex))
{
return null;
}
IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex); IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex);
if (gamepadHandle == IntPtr.Zero) if (gamepadHandle == IntPtr.Zero)