Compare commits

...

54 commits

Author SHA1 Message Date
Stossy11
71056542c4 Update Rumble and Motion to use the iPhones sensors when using Backbone controllers 2025-06-18 16:21:17 +10:00
Stossy11
2348f5f4b1 fix virtual controller and update constants 2025-06-18 07:15:20 +10:00
Stossy11
bfa3e25d9e Improved Performance, Added support for exiting Mii Maker, Updated Touch code and more 2025-06-15 19:14:34 +10:00
Stossy11
0fe067ccf6 Fix version number and fix timezone 2025-06-13 12:51:39 +10:00
Stossy11
2e77bb0345 Update UpdateAndBindTexturesWithoutTemplate to have the proper UE changes 2025-06-13 11:40:24 +10:00
Stossy11
83bf8aecae Revert button config 2025-06-13 10:23:16 +10:00
Stossy11
e2e26eed8b remove BackendMultithreading 2025-06-13 08:35:48 +10:00
Stossy11
c21dd01a58 App Icon Switcher, Per-App Settings Page, Update Virtual Controller and more 2025-06-13 07:03:22 +10:00
Stossy11
fad5915c2e Fix Joy-Con Detection 2025-05-31 14:26:42 +10:00
Stossy11
43fb7e2e7f Fix Haptics on iPhones and make JoyCons use Device Motion. 2025-05-31 14:24:21 +10:00
stossy11
056bcb6736 Merge pull request 'Rumble Implementation' (#34) from MediaMoots/MeloNX:rumbleDev into XC-ios-ht
Reviewed-on: https://git.743378673.xyz/MeloNX/MeloNX/pulls/34
2025-05-31 04:14:42 +00:00
MediaMoots
33fdecf0ad Update VirtualController.swift 2025-05-31 11:20:37 +08:00
MediaMoots
e9b862bfce Update NativeController.swift 2025-05-31 11:03:40 +08:00
MediaMoots
54ef5018e0 Merge branch 'XC-ios-ht' into rumbleDev 2025-05-31 11:01:24 +08:00
stossy11
f2cf4edb75 Merge pull request 'Motion Control Implementation' (#33) from MediaMoots/MeloNX:motionDev into XC-ios-ht
Reviewed-on: https://git.743378673.xyz/MeloNX/MeloNX/pulls/33
2025-05-31 02:54:14 +00:00
MediaMoots
6ece04918d Merge branch 'XC-ios-ht' into rumbleDev 2025-05-31 10:54:14 +08:00
MediaMoots
ea5287d9c7 Merge branch 'XC-ios-ht' into motionDev 2025-05-31 10:47:46 +08:00
Stossy11
22de4fd6c4 Remove Swift Module from StosJIT and add StikDebug to readme 2025-05-31 12:46:18 +10:00
MediaMoots
30b9d3cf1c Merge branch 'XC-ios-ht' into motionDev 2025-05-31 10:30:30 +08:00
Stossy11
bffc5356a8 Better Performance, Updated Compilation. Updated Settings UI, iOS 18.4db1 JIT, etc 2025-05-31 11:50:33 +10:00
MediaMoots
d7dad1b848 rumble implementation 2025-05-24 15:56:38 +08:00
MediaMoots
ad67d8d7df fix unicode 2025-05-22 16:43:17 +08:00
MediaMoots
6bc21f13ea Update BaseController.swift 2025-05-22 16:39:08 +08:00
MediaMoots
19b055de2d Update project.pbxproj 2025-05-22 16:38:06 +08:00
MediaMoots
29997c46e4 motion implementation 2025-05-22 16:38:00 +08:00
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
Bella
c5c79c26ea
Refactor settings to include update check option and adjust API URLs 2025-04-18 15:25:23 +12:00
stossy11
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: https://git.743378673.xyz/MeloNX/MeloNX/pulls/27
2025-04-13 09:41:56 +00:00
Bella!!!
6b045f3e6f fix bundle id again
(i suck)
2025-04-13 09:38:52 +00:00
Bella!!!
f33e8ed879 fix team 2025-04-13 09:37:58 +00:00
Bella!!!
c32873a734 fix bundle id 2025-04-13 09:36:51 +00:00
Bella
c6415d7e32
Refactor navigation button labels and enhance SettingsView with app version display and keyboard dismissal functionality 2025-04-13 21:31:10 +12:00
Bella
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
june
37020a5026 Update LICENSE.txt 2025-03-24 08:49:09 +00:00
june
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
june
8917ebf708 revert d326f5a00b
im dumb

revert Fix typos

i should git blame it 😭
2025-03-23 02:25:23 +00:00
june
d326f5a00b Fix typos
i should git blame it 😭
2025-03-23 02:22:18 +00:00
stossy11
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: https://git.743378673.xyz/MeloNX/MeloNX/pulls/23

melonx
2025-03-22 01:08:51 +00:00
sudo rm -rf --no-preserve-root /
667d54ed2d okay NOW we're in business🤑 2025-03-20 15:11:56 +00:00
sudo rm -rf --no-preserve-root /
1b70bfea8b its not resized properly. shit. brb. 2025-03-20 15:07:43 +00:00
sudo rm -rf --no-preserve-root /
33b8571414 Replace app icon with the new one 🤑 2025-03-20 15:06:16 +00:00
sudo rm -rf --no-preserve-root /
33af004d85 Delete src/MeloNX/MeloNX/Assets/Assets.xcassets/AppIcon.appiconset/nxgradientpng.png 2025-03-20 15:02:30 +00:00
128 changed files with 13673 additions and 2760 deletions

View file

@ -0,0 +1,49 @@
name: Update apps.json on new release
on:
release:
types: [published]
jobs:
update:
runs-on: debian-trixie
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get install -y jq
- name: Extract release data
id: release
run: |
echo "VERSION=${GITEA_REF_NAME}" >> $GITHUB_OUTPUT
echo "DESCRIPTION=$(echo '${GITEA_EVENT_RELEASE_BODY}' | jq -Rs .)" >> $GITHUB_OUTPUT
echo "DATE=$(date '+%Y-%m-%d')" >> $GITHUB_OUTPUT
IPA_URL=$(echo '${GITEA_EVENT_RELEASE_ASSETS}' | jq -r '.[0].browser_download_url')
echo "DOWNLOAD_URL=$IPA_URL" >> $GITHUB_OUTPUT
- name: Update apps.json
run: |
jq --arg version "${{ steps.release.outputs.VERSION }}" \
--arg buildVersion "1" \
--arg date "${{ steps.release.outputs.DATE }}" \
--arg localizedDescription "${{ steps.release.outputs.DESCRIPTION }}" \
--arg downloadURL "${{ steps.release.outputs.DOWNLOAD_URL }}" \
'.apps[0].versions |= [{"version": $version, "buildVersion": $buildVersion, "date": $date, "localizedDescription": $localizedDescription, "downloadURL": $downloadURL, "minOSVersion": "15.0"}]' \
apps.json > tmp.json && mv tmp.json apps.json
- name: Commit and push
run: |
git config user.name "gitea-actions"
git config user.email "gitea-actions@localhost"
git add apps.json
git commit -m "Update apps.json for release ${{ steps.release.outputs.VERSION }}"
git push
env:
GIT_AUTHOR_NAME: gitea-actions
GIT_AUTHOR_EMAIL: gitea-actions@localhost
GIT_COMMITTER_NAME: gitea-actions
GIT_COMMITTER_EMAIL: gitea-actions@localhost

View file

@ -1,3 +1,12 @@
Currently licensed under the GNU AFFERO GENERAL PUBLIC LICENSE version 3, or any later version, at your choice.
You may obtain a copy of the license at <https://gnu.org/>
Copyright (c) Rhajune Park and contributors, 2025
For copyright infringement claims, please contact abuse@pythonplayer123.dev for expedited processing
Previously licensed under the MeloNX License.
MeloNX License
Copyright (c) MeloNX Team and Contributors

View file

@ -28,6 +28,10 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
- Recommended Device: iPhone 15 Pro or newer.
- Low-End Recommended Device: iPhone 13 Pro.
## Discord Server
We have a discord server!
- https://discord.gg/melonx
## How to install
@ -48,7 +52,7 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
4. **Enable JIT**
- Use your preferred method to enable Just-In-Time (JIT) compilation.
- We reccomend using [JitStreamer](https://jkcoxson.com/jitstreamer)
- We reccomend using [StikDebug](https://apps.apple.com/us/app/stikdebug/id6744045754)
5. **Add Necessary Files**
@ -90,7 +94,7 @@ If having Issues installing firmware (Make sure your keys are installed first)
9. **Enable JIT**
- Use your preferred method to enable Just-In-Time (JIT) compilation.
- We recommend using [JitStreamer](https://jkcoxson.com/jitstreamer)
- We recommend using [StikDebug](https://apps.apple.com/us/app/stikdebug/id6744045754)
### TrollStore
@ -141,12 +145,12 @@ If having Issues installing firmware (Make sure your keys are installed first)
- **GPU**
The GPU emulator emulates the Switch's Maxwell GPU using Metal (via MoltenVK) APIs through a custom build of OpenTK or Silk.NET respectively.
The GPU emulator emulates the Switch's Maxwell GPU using Metal (via MoltenVK) APIs through a custom build of Silk.NET.
- **Input**
We currently have support for keyboard, touch input, JoyCon input support, and nearly all controllers.
Motion controls are natively supported in most cases.
We currently have support for keyboard, touch input, JoyCon input support, and nearly all MFI controllers.
Motion controls are natively supported in most cases, however JoyCons do not have motion support doe to an iOS limitation.
- **DLC & Modifications**
@ -157,14 +161,13 @@ If having Issues installing firmware (Make sure your keys are installed first)
The emulator has settings for enabling or disabling some logging, remapping controllers, and more.
## License
# License
This software is licensed under the terms of the [MeloNX license (Based on MIT License)](LICENSE.txt).
This software is licensed under the terms of the [MeloNX license](LICENSE.txt).
This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3.
See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY.md) for more details.
## Credits
# Credits
- [Ryujinx](https://github.com/ryujinx-mirror/ryujinx) is used for the base of this emulator. (link is to ryujinx-mirror since they were supportive)
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.

View file

@ -3,11 +3,13 @@
# Define the destination directory (hardcoded)
DESTINATION_DIR="src/MeloNX/Dependencies/Dynamic\ Libraries/Ryujinx.Headless.SDL2.dylib"
dotnet clean
# Restore the project
dotnet restore
# Build the project with the specified version
dotnet build -c Release
# dotnet build -c Release
# Publish the project with the specified runtime and settings
dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true

View file

@ -1,8 +1,7 @@
#!/bin/bash
XCCONFIG_FILE="${SRCROOT}/MeloNX.xcconfig"
# XCCONFIG_FILE="${SRCROOT}/MeloNX.xcconfig"
# Define the common paths to search for dotnet, including user-specific directories
SEARCH_PATHS=(
"/usr/local/share/dotnet"
"/usr/local/bin"
@ -14,10 +13,10 @@ SEARCH_PATHS=(
"$HOME/Developer"
)
# Initialize DOTNET_PATH as empty
DOTNET_PATH=""
# Search in the defined paths
for path in "${SEARCH_PATHS[@]}"; do
if [ -d "$path" ]; then
DOTNET_PATH=$(find "$path" -name dotnet -type f -print -quit 2>/dev/null)
@ -27,20 +26,8 @@ for path in "${SEARCH_PATHS[@]}"; do
fi
done
# Check if the path was found
if [ -z "$DOTNET_PATH" ]; then
echo "Error: dotnet path not found."
exit 1
fi
echo "dotnet path: $DOTNET_PATH"
# Escape the path for sed
ESCAPED_PATH=$(echo "$DOTNET_PATH" | sed 's/\//\\\//g')
# Update the xcconfig file
sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE"
$DOTNET_PATH clean
echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH"
echo "$DOTNET_PATH"

21
distribution/ios/xc-compile.sh Executable file
View file

@ -0,0 +1,21 @@
dotnet_output=$(./distribution/ios/get_dotnet.sh)
exit_code=$?
if [ $exit_code -eq 0 ]; then
dotnet="$dotnet_output"
else
echo "error: .NET not found, Please follow the compilation instructions on the gitea." >&2
exit 1
fi
$dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
if [ $? -ne 0 ]; then
echo "warning: Compiling MeloNX failed! Running dotnet clean + restore then Retrying..."
$dotnet clean
$dotnet restore
$dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
fi

49
source.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "MeloNX",
"subtitle": "A source for the MeloNX Application",
"description": "Welcome to the MeloNX source! The latest download for MeloNX.",
"iconURL": "https://git.743378673.xyz/CycloKid/assets/media/branch/main/Melo/AppIcons/MeloNX.png",
"headerURL": "https://cdn.discordapp.com/attachments/1320760161836466257/1331670540447912090/melon-x-not-melo-nx-amiright-guys.png?ex=67f556d6&is=67f40556&hm=71be8f109a14f1c47d8f4965aa017bccb5617962b7a9f5cdfb936a5a8135dad7&",
"website": "https://MeloNX.org",
"tintColor": "#AE34EB",
"featuredApps": [
"com.stossy11.MeloNX"
],
"apps": [
{
"name": "MeloNX",
"bundleIdentifier": "com.stossy11.MeloNX",
"developerName": "Stossy11",
"subtitle": "An NX Emulator.",
"localizedDescription": "MeloNX is an iOS Nintendo Switch emulator based on Ryujinx, written primarily in C#. Designed to bring accurate performance and a user-friendly interface to iOS, MeloNX makes Switch games accessible on Apple devices. Developed from the ground up, MeloNX is open-source and available on a custom Gitea server under the MeloNX license (Based on MIT) (requires increased memory limit)",
"iconURL": "https://git.743378673.xyz/CycloKid/assets/media/branch/main/Melo/AppIcons/MeloNX.png",
"tintColor": "#AE34EB",
"category": "games",
"screenshots": [
"https://git.743378673.xyz/stossy11/screenshots/raw/branch/main/IMG_0380.PNG",
"https://git.743378673.xyz/stossy11/screenshots/raw/branch/main/IMG_0381.PNG"
],
"versions": [
{
"version": "1.7.0",
"buildVersion": "1",
"date": "2025-04-08",
"localizedDescription": "First AltStore release!",
"downloadURL": "https://git.743378673.xyz/MeloNX/MeloNX/releases/download/1.7.0/MeloNX.ipa",
"size": 79821,
"minOSVersion": "15.0"
}
],
"appPermissions": {
"entitlements": [
"get-task-allow",
"com.apple.developer.kernel.increased-memory-limit"
],
"privacy": {
"NSPhotoLibraryAddUsageDescription": "MeloNX needs access to your Photo Library in order to save screenshots."
}
}
}
],
"news": []
}

View file

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

View file

@ -24,7 +24,7 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */ = {isa = PBXBuildFile; productRef = 4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */; };
4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; };
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
@ -32,13 +32,6 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
};
4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@ -53,6 +46,13 @@
remoteGlobalIDString = 4E80A98C2CD6F54500029585;
remoteInfo = MeloNX;
};
4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
};
BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@ -120,6 +120,10 @@
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
"Dependencies/Dynamic Libraries/StosJIT.framework" = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
"Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (
CodeSignOnCopy,
);
@ -129,10 +133,6 @@
"Dependencies/Dynamic Libraries/libavutil.dylib" = (
CodeSignOnCopy,
);
Dependencies/XCFrameworks/MoltenVK.xcframework = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
Dependencies/XCFrameworks/SDL2.xcframework = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
@ -178,6 +178,7 @@
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
"Dependencies/Dynamic Libraries/RyujinxHelper.framework",
"Dependencies/Dynamic Libraries/StosJIT.framework",
Dependencies/XCFrameworks/libavcodec.xcframework,
Dependencies/XCFrameworks/libavfilter.xcframework,
Dependencies/XCFrameworks/libavformat.xcframework,
@ -186,7 +187,6 @@
Dependencies/XCFrameworks/libswresample.xcframework,
Dependencies/XCFrameworks/libswscale.xcframework,
Dependencies/XCFrameworks/libteakra.xcframework,
Dependencies/XCFrameworks/MoltenVK.xcframework,
Dependencies/XCFrameworks/SDL2.xcframework,
);
};
@ -203,8 +203,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
);
@ -264,12 +264,12 @@
/* Begin PBXLegacyTarget section */
BD43C61D2D1B23AB003BBC42 /* Ryujinx */ = {
isa = PBXLegacyTarget;
buildArgumentsString = "publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true";
buildArgumentsString = "./distribution/ios/xc-compile.sh";
buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */;
buildPhases = (
);
buildToolPath = "$(DOTNET)";
buildWorkingDirectory = "$(SRCROOT)/../..";
buildToolPath = /bin/sh;
buildWorkingDirectory = "$(SRCROOT)/../../";
dependencies = (
);
name = Ryujinx;
@ -294,15 +294,15 @@
buildRules = (
);
dependencies = (
4E2953AC2D803BC9000497CD /* PBXTargetDependency */,
4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
4E80A98F2CD6F54500029585 /* MeloNX */,
);
name = MeloNX;
packageProductDependencies = (
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */,
);
productName = MeloNX;
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
@ -393,8 +393,8 @@
mainGroup = 4E80A9842CD6F54500029585;
minimizedProjectReferenceProxies = 1;
packageReferences = (
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */,
);
preferredProjectObjectVersion = 56;
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
@ -482,12 +482,6 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */;
};
4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
@ -498,6 +492,11 @@
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
};
4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
targetProxy = 4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */;
};
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
@ -571,6 +570,7 @@
ONLY_ACTIVE_ARCH = NO;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
@ -637,6 +637,7 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_DISABLE_SAFETY_CHECKS = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
VALIDATE_PRODUCT = YES;
};
@ -646,13 +647,16 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -721,6 +725,72 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
@ -733,10 +803,10 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UIRequiresFullScreen = NO;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -863,6 +933,119 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
@ -880,13 +1063,16 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
@ -955,6 +1141,72 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
@ -967,10 +1219,10 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UIRequiresFullScreen = NO;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1097,6 +1349,119 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
@ -1298,12 +1663,12 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
repositoryURL = "https://github.com/robbiehanson/CocoaAsyncSocket";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.5.0;
minimumVersion = 7.6.5;
};
};
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
@ -1317,10 +1682,10 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */ = {
isa = XCSwiftPackageProductDependency;
package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;
productName = SwiftUIJoystick;
package = 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */;
productName = CocoaAsyncSocket;
};
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
isa = XCSwiftPackageProductDependency;

View file

@ -1,6 +1,15 @@
{
"originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b",
"originHash" : "b4a593815773c4e9eedb98cabe88f41620776314bffb6c39d5a41cb743e4d390",
"pins" : [
{
"identity" : "cocoaasyncsocket",
"kind" : "remoteSourceControl",
"location" : "https://github.com/robbiehanson/CocoaAsyncSocket",
"state" : {
"revision" : "dbdc00669c1ced63b27c3c5f052ee4d28f10150c",
"version" : "7.6.5"
}
},
{
"identity" : "swiftsvg",
"kind" : "remoteSourceControl",
@ -9,15 +18,6 @@
"branch" : "master",
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
}
},
{
"identity" : "swiftuijoystick",
"kind" : "remoteSourceControl",
"location" : "https://github.com/michael94ellis/SwiftUIJoystick",
"state" : {
"revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65",
"version" : "1.5.0"
}
}
],
"version" : 3

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"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
@ -58,16 +58,19 @@
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
disableMainThreadChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugXPCServices = "NO"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES"
viewDebuggingEnabled = "No"
queueDebuggingEnabled = "No"
consoleMode = "0"
structuredConsoleMode = "2">
structuredConsoleMode = "2"
disablePerformanceAntipatternChecker = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference

View file

@ -17,11 +17,20 @@ func SecTaskCopyValueForEntitlement(
_ error: NSErrorPointer
) -> CFTypeRef?
@_silgen_name("SecTaskCopyTeamIdentifier")
func SecTaskCopyTeamIdentifier(
_ task: SecTaskRef,
_ error: NSErrorPointer
) -> NSString?
@_silgen_name("SecTaskCreateFromSelf")
func SecTaskCreateFromSelf(
_ allocator: CFAllocator?
) -> SecTaskRef?
@_silgen_name("CFRelease")
func CFRelease(_ cf: CFTypeRef)
@_silgen_name("SecTaskCopyValuesForEntitlements")
func SecTaskCopyValuesForEntitlements(
_ task: SecTaskRef,
@ -29,30 +38,43 @@ func SecTaskCopyValuesForEntitlements(
_ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
) -> CFDictionary?
func releaseSecTask(_ task: SecTaskRef) {
let cf = unsafeBitCast(task, to: CFTypeRef.self)
CFRelease(cf)
}
func checkAppEntitlements(_ ents: [String]) -> [String: Any] {
guard let task = SecTaskCreateFromSelf(nil) else {
print("Failed to create SecTask")
return [:]
}
defer {
releaseSecTask(task)
}
guard let entitlements = SecTaskCopyValuesForEntitlements(task, ents as CFArray, nil) else {
print("Failed to get entitlements")
return [:]
}
return (entitlements as? [String: Any]) ?? [:]
return (entitlements as NSDictionary) as? [String: Any] ?? [:]
}
func checkAppEntitlement(_ ent: String) -> Bool {
guard let task = SecTaskCreateFromSelf(nil) else {
print("Failed to create SecTask")
return false
}
guard let entitlements = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
print("Failed to get entitlements")
defer {
releaseSecTask(task)
}
guard let entitlement = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
return false
}
return entitlements.boolValue != nil && entitlements.boolValue
if let number = entitlement as? NSNumber {
return number.boolValue
} else if let bool = entitlement as? Bool {
return bool
}
return false
}

View file

@ -14,11 +14,14 @@
#include <SDL2/SDL.h>
#include <SDL2/SDL_syswm.h>
#include <StosJIT/StosJIT-Swift.h>
#ifdef __cplusplus
extern "C" {
#endif
struct GameInfo {
long FileSize;
char TitleName[512];
@ -40,6 +43,10 @@ struct DlcNcaList {
struct DlcNcaListItem* items;
};
typedef void (^SwiftCallback)(NSString *result);
void RegisterCallback(NSString *identifier, SwiftCallback callback);
extern struct GameInfo get_game_info(int, char*);
extern struct DlcNcaList get_dlc_nca_list(const char* titleIdPtr, const char* pathPtr);
@ -50,12 +57,16 @@ char* installed_firmware_version();
void set_native_window(void *layerPtr);
void pause_emulation(bool shouldPause);
void stop_emulation();
void initialize();
int main_ryujinx_sdl(int argc, char **argv);
int update_settings_external(int argc, char **argv);
int get_current_fps();
void touch_began(float x, float y, int index);

View file

@ -20,6 +20,14 @@ func isJITEnabled() -> Bool {
return csops(pid: getpid(), ops: 0, useraddr: &flags, usersize: Int32(MemoryLayout.size(ofValue: flags))) == 0 && (flags & Int(CS_DEBUGGED)) != 0 ? allocateTest() : false
}
func checkDebugged() -> Bool {
var flags: Int = 0
if checkAppEntitlement("dynamic-codesigning") {
return true
}
return csops(pid: getpid(), ops: 0, useraddr: &flags, usersize: Int32(MemoryLayout.size(ofValue: flags))) == 0 && (flags & Int(CS_DEBUGGED)) != 0
}
func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
var region: vm_address_t = vm_address_t(UInt(bitPattern: address))
var regionSize: vm_size_t = 0
@ -34,7 +42,7 @@ func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
}
if result != KERN_SUCCESS {
print("Failed to reach \(address)")
// print("Failed to reach \(address)")
return false
}

View file

@ -23,7 +23,7 @@ func enableJITEB() {
func enableJITEBRequest() {
let pid = Int(getpid())
print(pid)
// print(pid)
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
var request = URLRequest(url: address)
@ -90,7 +90,7 @@ func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping
let task = session.dataTask(with: request) { _, response, error in
if let error = error {
print("Ping failed: \(error.localizedDescription)")
// print("Ping failed: \(error.localizedDescription)")
completion(false)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
completion(true)
@ -118,6 +118,7 @@ func presentAlert(title: String, message: String, completion: (() -> Void)? = ni
}
}
struct LaunchApp: Codable {
let success: Bool
let message: String
@ -140,12 +141,12 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
viewController.present(alert, animated: true)
}
} else {
print("Hopefully JIT is enabled now...")
// print("Hopefully JIT is enabled now...")
Ryujinx.shared.ryuIsJITEnabled()
}
} catch {
print(String(data: jsonData, encoding: .utf8))
// print(String(data: jsonData, encoding: .utf8))
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))

View file

@ -0,0 +1,74 @@
//
// EnableJIT.swift
// MeloNX
//
// Created by Stossy11 on 10/02/2025.
//
import Foundation
import Network
import UIKit
func stikJITorStikDebug() -> Int {
let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil)
if checkifappinstalled("com.stik.sj") {
return 1 // StikDebug
}
if checkifappinstalled("com.stik.sj.\(String(teamid ?? ""))") {
return 2 // StikJIT
}
return 0 // Not Found
}
func checkforOld() -> Bool {
let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil)
if checkifappinstalled(changeAppUI("Y29tLnN0b3NzeTExLlBvbWVsbw==") ?? "") {
return true
}
if checkifappinstalled(changeAppUI("Y29tLnN0b3NzeTExLlBvbWVsbw==") ?? "" + ".\(String(teamid ?? ""))") {
return true
}
if checkifappinstalled((Bundle.main.bundleIdentifier ?? "").replacingOccurrences(of: "MeloNX", with: changeAppUI("UG9tZWxv") ?? "")) {
return true
}
return false
}
func checkifappinstalled(_ id: String) -> Bool {
guard let handle = dlopen("/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices", RTLD_LAZY) else {
return false
}
typealias SBSLaunchApplicationWithIdentifierFunc = @convention(c) (CFString, Bool) -> Int32
guard let sym = dlsym(handle, "SBSLaunchApplicationWithIdentifier") else {
if let error = dlerror() {
print(String(cString: error))
}
dlclose(handle)
return false
}
let bundleID: CFString = id as CFString
let suspended: Bool = false
let SBSLaunchApplicationWithIdentifier = unsafeBitCast(sym, to: SBSLaunchApplicationWithIdentifierFunc.self)
let result = SBSLaunchApplicationWithIdentifier(bundleID, suspended)
return result == 9
}
func enableJITStik() {
let urlScheme = "stikjit://enable-jit?bundle-id=\(Bundle.main.bundleIdentifier ?? "wow")"
if let launchURL = URL(string: urlScheme), !isJITEnabled() {
UIApplication.shared.open(launchURL, options: [:], completionHandler: nil)
}
}

View file

@ -0,0 +1,14 @@
//
// BaseController.swift
// MeloNX
//
// Created by MediaMoots on 5/17/2025.
//
// MARK:- Base Controller Protocol
/// Base Controller with motion related functions
protocol BaseController: AnyObject {
func tryRegisterMotion(slot: UInt8)
func tryGetMotionProvider() -> DSUMotionProvider?
}

View file

@ -0,0 +1,178 @@
//
// DSUMotionProviders.swift
//
// Multi-source Cemuhook-compatible DSU server.
// Created by MediaMoots on 5/17/2025.
//
//
import CoreMotion
import GameController // GCController
// MARK:- Providers
/// iPhone / iPad IMU
final class DeviceMotionProvider: DSUMotionProvider {
// DSUMotionProvider conformance
let slot: UInt8
let mac: [UInt8] = [0xAB,0x12,0xCD,0x34,0xEF,0x56]
let connectionType: UInt8 = 2
let batteryLevel: UInt8 = 5
let motionRate: Double = 60.0 // 60 Hz
// Internals
private let mm = CMMotionManager()
// Thread Safety
private let dataLock = NSLock()
private var _latest: CMDeviceMotion?
private var latest: CMDeviceMotion? {
get { dataLock.lock(); defer { dataLock.unlock() }; return _latest }
set { dataLock.lock(); _latest = newValue; dataLock.unlock() }
}
private var orientation: UIDeviceOrientation =
UIDevice.current.orientation == .unknown ? .landscapeLeft : UIDevice.current.orientation
init(slot: UInt8) {
precondition(slot < 8, "DSU only supports slots 0…7")
self.slot = slot
// start Core Motion
mm.deviceMotionUpdateInterval = 1.0 / motionRate
mm.startDeviceMotionUpdates(to: .main) { [weak self] m, _ in
guard let self = self, let m = m else { return }
self.latest = m
if let sample = self.nextSample() {
DSUServer.shared.pushSample(sample, from: self)
}
}
// track orientation changes (ignore flat)
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
NotificationCenter.default.addObserver(
self,
selector: #selector(orientationDidChange),
name: UIDevice.orientationDidChangeNotification,
object: nil
)
}
@objc private func orientationDidChange() {
let o = UIDevice.current.orientation
if o.isFlat { return } // ignore face-up / face-down
orientation = o
}
func nextSample() -> DSUMotionSample? {
guard let m = latest else { return nil }
// Raw values
let gx = Float(m.rotationRate.x)
let gy = Float(m.rotationRate.y)
let gz = Float(m.rotationRate.z)
let ax = Float(m.gravity.x + m.userAcceleration.x)
let ay = Float(m.gravity.y + m.userAcceleration.y)
let az = Float(m.gravity.z + m.userAcceleration.z)
// Rotate axes to match Cemuhook's "landscape-left as neutral" convention
let a: SIMD3<Float>
let g: SIMD3<Float>
switch orientation {
case .portrait:
a = SIMD3( ax, az, -ay)
g = SIMD3( gx, -gz, gy)
case .landscapeRight:
a = SIMD3( ay, az, ax)
g = SIMD3( gy, -gz, -gx)
case .portraitUpsideDown:
a = SIMD3( -ax, az, ay)
g = SIMD3( -gx, -gz, -gy)
case .landscapeLeft, .unknown, .faceUp, .faceDown:
a = SIMD3( -ay, az, -ax)
g = SIMD3( -gy, -gz, gx)
@unknown default:
return nil
}
// Convert gyro rad/s °/s here so the server doesn't have to.
let gDeg = g * (180 / .pi)
return DSUMotionSample(timestampUS: currentUS(),
accel: a,
gyroDeg: gDeg)
}
}
// Any Switch Pro / DualSense controller that exposes `GCMotion`
final class ControllerMotionProvider: DSUMotionProvider {
// DSUMotionProvider
let slot: UInt8
let mac: [UInt8] = [0xAB,0x12,0xCD,0x34,0xEF,0x56]
let connectionType: UInt8 = 2
var batteryLevel: UInt8 {
UInt8((pad.battery?.batteryLevel ?? 0.3) * 5).clamped(to: 0...5)
}
private let pad: GCController
// Thread Safety
private let dataLock = NSLock()
private var _latest: GCMotion?
private var latest: GCMotion? {
get { dataLock.lock(); defer { dataLock.unlock() }; return _latest }
set { dataLock.lock(); _latest = newValue; dataLock.unlock() }
}
init(controller: GCController, slot: UInt8) {
self.pad = controller
self.slot = slot
pad.motion?.sensorsActive = true
pad.motion?.valueChangedHandler = { [weak self] motion in
guard let self = self else { return }
self.latest = motion
if let sample = self.nextSample() {
DSUServer.shared.pushSample(sample, from: self)
}
}
}
func nextSample() -> DSUMotionSample? {
guard let m = latest else { return nil }
// Extract and convert acceleration to SIMD3<Float>
let a = SIMD3<Float>(
Float(m.acceleration.x),
Float(m.acceleration.z),
-Float(m.acceleration.y)
)
// Extract, transform, and convert rotation rate to SIMD3<Float> (in radians/s)
let g = SIMD3<Float>(
Float(m.rotationRate.x),
-Float(m.rotationRate.z),
Float(m.rotationRate.y)
)
// Convert gyro rotation rate from rad/s to degrees/s
let gDeg = g * (180 / .pi)
return DSUMotionSample(
timestampUS: currentUS(),
accel: a,
gyroDeg: gDeg
)
}
}
// MARK:- Helper funcs / ext
private func uint64US(_ time: TimeInterval) -> UInt64 { UInt64(time * 1_000_000) }
private func currentUS() -> UInt64 { uint64US(CACurrentMediaTime()) }
private extension Comparable {
func clamped(to r: ClosedRange<Self>) -> Self { min(max(self, r.lowerBound), r.upperBound) }
}

View file

@ -0,0 +1,217 @@
//
// DSUServer.swift
//
// Multi-source Cemuhook-compatible DSU server.
// Created by MediaMoots on 5/17/2025.
//
//
import Foundation
import CocoaAsyncSocket // GCDAsyncUdpSocket
import zlib // CRC-32
// MARK:- DSU Motion protocol
/// One motion source == one DSU *slot* (0-7).
protocol DSUMotionProvider: AnyObject {
var slot: UInt8 { get } // unique, 0-7
var mac: [UInt8] { get } // 6-byte ID
var connectionType: UInt8 { get } // 0 = USB, 2 = BT
var batteryLevel: UInt8 { get } // 0-5 (Cemuhook)
func nextSample() -> DSUMotionSample?
}
/// Raw motion payload returned by providers.
struct DSUMotionSample {
var timestampUS: UInt64 // µs
var accel: SIMD3<Float> // G's
var gyroDeg: SIMD3<Float> // °/s
}
// MARK:- Server constants
private enum C {
static let port: UInt16 = 26_760
static let protocolVersion: UInt16 = 1_001
static let headerMagic = "DSUS"
}
// MARK:- Server core
final class DSUServer: NSObject {
// Singleton for convenience
static let shared = DSUServer()
private override init() {
serverID = UInt32.random(in: .min ... .max)
super.init()
configureSocket()
}
// MARK: Public API
func register(_ provider: DSUMotionProvider) { providers[provider.slot] = provider }
func unregister(slot: UInt8) { providers.removeValue(forKey: slot) }
/// 🔸 providers push fresh samples here.
func pushSample(_ sample: DSUMotionSample, from provider: DSUMotionProvider) {
guard let addr = lastClientAddress else { return } // no subscriber drop
sendPadData(sample: sample, from: provider, to: addr)
}
// MARK: Private
private let serverID: UInt32
private var socket: GCDAsyncUdpSocket?
private var lastClientAddress: Data?
private var providers = [UInt8 : DSUMotionProvider]() // slotprovider
private var packetNumber = [UInt8 : UInt32]() // per-slot counter
// UDP setup
private func configureSocket() {
socket = GCDAsyncUdpSocket(delegate: self, delegateQueue: .main)
do {
try socket?.bind(toPort: C.port)
try socket?.beginReceiving()
//print("🟢 DSU server listening on UDP \(C.port)")
} catch {
//print(" DSU socket error:", error)
}
}
}
// MARK:- UDP delegate
extension DSUServer: GCDAsyncUdpSocketDelegate {
func udpSocket(_ sock: GCDAsyncUdpSocket,
didReceive data: Data,
fromAddress addr: Data,
withFilterContext ctx: Any?) {
lastClientAddress = addr
// Light validation
guard data.count >= 20,
String(decoding: data[0..<4], as: UTF8.self) == C.headerMagic,
data.readUInt16LE(at: 4) == C.protocolVersion
else { return }
let type = data.readUInt32LE(at: 16)
switch type {
case 0x100001: sendPortInfo(to: addr) // client asks for port list
case 0x100002: break // subscription acknowledged
default: break
}
}
func udpSocketDidClose(_ sock: GCDAsyncUdpSocket, withError err: Error?) {
//print("UDP closed:", err?.localizedDescription ?? "nil")
lastClientAddress = nil
}
}
// MARK:- Packet helpers
private extension DSUServer {
// Header (16 bytes)
func appendHeader(into d: inout Data, payloadSize: UInt16) {
d.append(C.headerMagic.data(using: .utf8)!) // "DSUS"
d.append(C.protocolVersion.leData) // Protocol Version
d.append(payloadSize.leData) // Payload Size
d.append(Data(repeating: 0, count: 4)) // CRC-stub
d.append(serverID.leData) // Server ID
}
func patchCRC32(of packet: inout Data) {
let crc = packet.withUnsafeBytes { ptr in
crc32(0, ptr.baseAddress, uInt(packet.count))
}.littleEndian
let crcLE = UInt32(crc).littleEndian
let crcData = withUnsafeBytes(of: crcLE) { Data($0) }
packet.replaceSubrange(8..<12, with: crcData)
}
// 0x100001 DSUSPortInfo
func sendPortInfo(to addr: Data) {
for p in providers.values {
var pkt = Data()
appendHeader(into: &pkt, payloadSize: 12)
pkt.append(UInt32(0x100001).leData)
pkt.append(p.slot)
pkt.append(UInt8(2)) // connected
pkt.append(UInt8(2)) // full gyro
pkt.append(p.connectionType)
pkt.append(p.mac, count: 6)
pkt.append(p.batteryLevel)
pkt.append(UInt8(0)) // padding
patchCRC32(of: &pkt)
socket?.send(pkt, toAddress: addr, withTimeout: -1, tag: 0)
}
}
// 0x100002 DSUSPadDataRsp
func sendPadData(sample s: DSUMotionSample,
from p: DSUMotionProvider,
to addr: Data) {
var pkt = Data()
appendHeader(into: &pkt, payloadSize: 84)
pkt.append(UInt32(0x100002).leData)
pkt.append(p.slot)
pkt.append(UInt8(2)) // connected
pkt.append(UInt8(2)) // full gyro
pkt.append(p.connectionType)
pkt.append(p.mac, count: 6)
pkt.append(p.batteryLevel)
pkt.append(UInt8(1)) // is connected
let num = packetNumber[p.slot, default: 0]
pkt.append(num.leData)
packetNumber[p.slot] = num &+ 1
pkt.append(UInt16(0).leData) // buttons
pkt.append(contentsOf: [0,0]) // HOME / Touch
pkt.append(contentsOf: [128,128,128,128]) // sticks
pkt.append(Data(repeating: 0, count: 12)) // d-pad / face / trig
pkt.append(Data(repeating: 0, count: 12)) // touch 1 & 2
pkt.append(s.timestampUS.leData)
pkt.append(s.accel.x.leData)
pkt.append(s.accel.y.leData)
pkt.append(s.accel.z.leData)
pkt.append(s.gyroDeg.x.leData)
pkt.append(s.gyroDeg.y.leData)
pkt.append(s.gyroDeg.z.leData)
patchCRC32(of: &pkt)
socket?.send(pkt, toAddress: addr, withTimeout: -1, tag: 0)
}
}
// MARK:- Helper funcs / ext
private extension FixedWidthInteger {
var leData: Data {
var v = self.littleEndian
return Data(bytes: &v, count: MemoryLayout<Self>.size)
}
}
private extension Float {
var leData: Data {
var v = self
return Data(bytes: &v, count: MemoryLayout<Self>.size)
}
}
private extension Data {
func readUInt16LE(at offset: Int) -> UInt16 {
self[offset..<offset+2].withUnsafeBytes { $0.load(as: UInt16.self) }.littleEndian
}
func readUInt32LE(at offset: Int) -> UInt32 {
self[offset..<offset+4].withUnsafeBytes { $0.load(as: UInt32.self) }.littleEndian
}
}

View file

@ -8,24 +8,57 @@
import CoreHaptics
import GameController
class NativeController: Hashable {
class NativeController: Hashable, BaseController {
private var instanceID: SDL_JoystickID = -1
private var controller: OpaquePointer?
private var nativeController: GCController
private var controllerMotionProvider: DSUMotionProvider?
private let controllerHaptics: CHHapticEngine?
private let rumbleController: RumbleController?
public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" }
init(_ controller: GCController) {
nativeController = controller
controllerHaptics = nativeController.haptics?.createEngine(withLocality: .default)
try? controllerHaptics?.start()
var ncontrollerHaptics = nativeController.haptics?.createEngine(withLocality: .default)
let vendorName = nativeController.vendorName ?? "Unknown"
var usesdeviceHaptics = (ncontrollerHaptics == nil || vendorName.lowercased().hasSuffix("backbone") || vendorName.lowercased() == "backbone one")
controllerHaptics = usesdeviceHaptics ? ncontrollerHaptics : try? CHHapticEngine()
// Make sure the haptic engine exists before attempting to start it or initialize the controller.
if let hapticsEngine = controllerHaptics {
do {
try hapticsEngine.start()
rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: 1.2)
} catch {
rumbleController = nil
}
} else {
rumbleController = nil
}
setupHandheldController()
}
deinit {
cleanup()
}
internal func tryRegisterMotion(slot: UInt8) {
// Setup Motion
let dsuServer = DSUServer.shared
let vendorName = nativeController.vendorName ?? "Unknown"
var usesdevicemotion = (vendorName.lowercased() == "Joy-Con (l/R)".lowercased() || vendorName.lowercased().hasSuffix("backbone") || vendorName.lowercased() == "backbone one")
controllerMotionProvider = usesdevicemotion ? DeviceMotionProvider(slot: slot) : ControllerMotionProvider(controller: nativeController, slot: slot)
if let provider = controllerMotionProvider {
dsuServer.register(provider)
}
}
internal func tryGetMotionProvider() -> DSUMotionProvider? { return controllerMotionProvider }
private func setupHandheldController() {
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
@ -49,50 +82,51 @@ class NativeController: Hashable {
// Update joystick state here
},
SetPlayerIndex: { userdata, playerIndex in
print("Player index set to \(playerIndex)")
// print("Player index set to \(playerIndex)")
guard let userdata, let player = GCControllerPlayerIndex(rawValue: Int(playerIndex)) else { return }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
_self.nativeController.playerIndex = player
},
Rumble: { userdata, lowFreq, highFreq in
print("Rumble with \(lowFreq), \(highFreq)")
guard let userdata else { return 0 }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics)
_self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
return 0
},
RumbleTriggers: { userdata, leftRumble, rightRumble in
print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0
},
SetLED: { userdata, red, green, blue in
print("Set LED to RGB(\(red), \(green), \(blue))")
guard let userdata else { return 0 }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
guard let light = _self.nativeController.light else { return 0 }
light.color = .init(red: Float(red), green: Float(green), blue: Float(blue))
return 0
},
SendEffect: { userdata, data, size in
print("Effect sent with size \(size)")
return 0
}
)
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)
if instanceID < 0 {
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return
}
controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil {
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return
}
if #available(iOS 16, *) {
guard let gamepad = nativeController.extendedGamepad
else { return }
setupButtonChangeListener(gamepad.buttonA, for: .A)
setupButtonChangeListener(gamepad.buttonB, for: .B)
setupButtonChangeListener(gamepad.buttonX, for: .X)
setupButtonChangeListener(gamepad.buttonY, for: .Y)
setupButtonChangeListener(gamepad.buttonA, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .B : .A)
setupButtonChangeListener(gamepad.buttonB, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .A : .B)
setupButtonChangeListener(gamepad.buttonX, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .Y : .X)
setupButtonChangeListener(gamepad.buttonY, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .X : .Y)
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
@ -139,49 +173,13 @@ class NativeController: Hashable {
func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) {
button.valueChangedHandler = { [unowned self] _, value, pressed in
// print("Value: \(value), Is pressed: \(pressed)")
// // print("Value: \(value), Is pressed: \(pressed)")
let axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let scaledValue = Sint16(value * 32767.0)
updateAxisValue(value: scaledValue, forAxis: axis)
}
}
static func rumble(lowFreq: Float, highFreq: Float) {
do {
// Low-frequency haptic pattern
let lowFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
], relativeTime: 0, duration: 0.2)
], parameters: [])
// High-frequency haptic pattern
let highFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
], relativeTime: 0.2, duration: 0.2)
], parameters: [])
// Create and start the haptic engine
let engine = try CHHapticEngine()
try engine.start()
// Create and play the low-frequency player
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
try lowFreqPlayer.start(atTime: 0)
// Create and play the high-frequency player after a short delay
let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
try highFreqPlayer.start(atTime: 0.2)
} catch {
print("Error creating haptic patterns: \(error)")
}
}
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID)
@ -206,7 +204,6 @@ class NativeController: Hashable {
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return }
// print("Button: \(button.rawValue) {state: \(state)}")
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0

View file

@ -0,0 +1,132 @@
//
// RumbleController.swift
// MeloNX
//
// Created by MediaMoots on 2025/5/24.
//
import CoreHaptics
import Foundation
class RumbleController {
private var engine: CHHapticEngine?
private var lowHapticPlayer: CHHapticPatternPlayer?
private var highHapticPlayer: CHHapticPatternPlayer?
private var rumbleMultiplier: Float = 1.0
// The duration of each continuous haptic event.
// We'll restart the players before this duration expires.
private let hapticEventDuration: TimeInterval = 20
// Timer to schedule player restarts
private var playerRestartTimer: Timer?
// Interval before the haptic event duration runs out to restart
private let restartGracePeriod: TimeInterval = 1.0
init (engine: CHHapticEngine?, rumbleMultiplier: Float) {
self.engine = engine
self.rumbleMultiplier = rumbleMultiplier
createPlayers()
setupPlayerRestartTimer()
}
// Deinitializer to clean up the timer and stop players when the controller is deallocated
deinit {
playerRestartTimer?.invalidate() // Stop the timer
playerRestartTimer = nil
// Optionally stop the haptic players immediately
try? lowHapticPlayer?.stop(atTime: CHHapticTimeImmediate)
try? highHapticPlayer?.stop(atTime: CHHapticTimeImmediate)
// print("RumbleController deinitialized.")
}
// MARK: - Private Methods for Player Management
private func createPlayers() {
// Ensure the engine is available before proceeding
guard let engine = self.engine else {
// print("CHHapticEngine is nil. Cannot initialize RumbleController.")
return
}
do {
let baseIntensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
let lowSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.0)
let highSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1)
// Create continuous haptic events with the defined duration
let lowContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, lowSharpness], relativeTime: 0, duration: hapticEventDuration)
let highContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, highSharpness], relativeTime: 0, duration: hapticEventDuration)
// Create patterns from the continuous haptic events.
let lowPattern = try CHHapticPattern(events: [lowContinuousEvent], parameters: [])
let highPattern = try CHHapticPattern(events: [highContinuousEvent], parameters: [])
// Make players from the patterns
lowHapticPlayer = try engine.makePlayer(with: lowPattern)
highHapticPlayer = try engine.makePlayer(with: highPattern)
rumble(lowFreq: 0, highFreq: 0)
// Start players initially
try lowHapticPlayer?.start(atTime: 0)
try highHapticPlayer?.start(atTime: 0)
} catch {
// print("Error initializing RumbleController or setting up haptic player: \(error.localizedDescription)")
// Clean up if setup fails
lowHapticPlayer = nil
highHapticPlayer = nil
playerRestartTimer?.invalidate()
playerRestartTimer = nil
}
}
private func setupPlayerRestartTimer() {
// Invalidate any existing timer to prevent multiple timers if init is called multiple times
playerRestartTimer?.invalidate()
// Calculate the interval for restarting: 1 second before the haptic event duration ends
let restartInterval = hapticEventDuration - restartGracePeriod
guard restartInterval > 0 else {
// print("Warning: hapticEventDuration (\(hapticEventDuration)s) is too short for scheduled restart with grace period (\(restartGracePeriod)s). Timer will not be set.")
return
}
// Schedule a repeating timer that calls restartPlayers()
playerRestartTimer = Timer.scheduledTimer(withTimeInterval: restartInterval, repeats: true) { [weak self] _ in
self?.createPlayers()
}
// Ensure the timer is added to the current run loop in its default mode
RunLoop.current.add(playerRestartTimer!, forMode: .default)
// print("Haptic Players restart timer scheduled to fire every \(restartInterval) seconds.")
}
// MARK: - Public Rumble Control
public func rumble(lowFreq: Float, highFreq: Float) {
// Normalize SDL values (0-65535) to CoreHaptics range (0.0-1.0)
let normalizedLow = min(1.0, max(0.0, lowFreq * rumbleMultiplier / 65535.0))
let normalizedHigh = min(1.0, max(0.0, highFreq * rumbleMultiplier / 65535.0))
// Create dynamic parameters to control intensity
let lowIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedLow, relativeTime: 0)
let highIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedHigh, relativeTime: 0)
// Send parameters to the players
do {
try lowHapticPlayer?.sendParameters([lowIntensityParameter], atTime: 0)
try highHapticPlayer?.sendParameters([highIntensityParameter], atTime: 0)
} catch {
// print("Error sending haptic parameters: \(error.localizedDescription)")
}
}
}

View file

@ -9,16 +9,49 @@ import Foundation
import CoreHaptics
import UIKit
class VirtualController {
class VirtualController : BaseController {
private var instanceID: SDL_JoystickID = -1
private var controller: OpaquePointer?
private let hapticEngine: CHHapticEngine?
private let rumbleController: RumbleController?
private var deviceMotionProvider: DeviceMotionProvider?
public let controllername = "MeloNX Touch Controller"
init() {
// Setup Haptics
hapticEngine = try? CHHapticEngine()
if let hapticsEngine = hapticEngine {
do {
try hapticsEngine.start()
rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: 2.0)
// print("CHHapticEngine started and RumbleController initialized.")
} catch {
// print("Error starting CHHapticEngine: \(error.localizedDescription)")
rumbleController = nil
}
} else {
// print("CHHapticEngine is nil. Cannot initialize RumbleController.")
rumbleController = nil
}
setupVirtualController()
}
internal func tryRegisterMotion(slot: UInt8) {
// Setup Motion
let dsuServer = DSUServer.shared
deviceMotionProvider = DeviceMotionProvider(slot: slot)
if let provider = deviceMotionProvider {
dsuServer.register(provider)
}
}
internal func tryGetMotionProvider() -> DSUMotionProvider? {
return deviceMotionProvider
}
private func setupVirtualController() {
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
@ -36,94 +69,50 @@ class VirtualController {
button_mask: 0,
axis_mask: 0,
name: controllername.withCString { $0 },
userdata: nil,
userdata: Unmanaged.passUnretained(self).toOpaque(),
Update: { userdata in
// Update joystick state here
},
SetPlayerIndex: { userdata, playerIndex in
print("Player index set to \(playerIndex)")
// print("Player index set to \(playerIndex)")
},
Rumble: { userdata, lowFreq, highFreq in
print("Rumble with \(lowFreq), \(highFreq)")
// print("Rumble with \(lowFreq), \(highFreq)")
if UIDevice.current.userInterfaceIdiom == .phone {
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
guard let userdata else { return 0 }
let _self = Unmanaged<VirtualController>.fromOpaque(userdata).takeUnretainedValue()
_self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
}
return 0
},
RumbleTriggers: { userdata, leftRumble, rightRumble in
print("Trigger rumble with \(leftRumble), \(rightRumble)")
// print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0
},
SetLED: { userdata, red, green, blue in
print("Set LED to RGB(\(red), \(green), \(blue))")
// print("Set LED to RGB(\(red), \(green), \(blue))")
return 0
},
SendEffect: { userdata, data, size in
print("Effect sent with size \(size)")
// print("Effect sent with size \(size)")
return 0
}
)
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
if instanceID < 0 {
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
// print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return
}
controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil {
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
// print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return
}
}
static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
do {
let lowFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
], relativeTime: 0, duration: 0.2)
], parameters: [])
let highFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
], relativeTime: 0.2, duration: 0.2)
], parameters: [])
var engine = engine
if engine == nil {
if hapticEngine == nil {
hapticEngine = try CHHapticEngine()
try hapticEngine?.start()
}
engine = hapticEngine
}
guard let engine else {
return print("Error creating haptic patterns: hapticEngine is nil")
}
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
try lowFreqPlayer.start(atTime: 0)
let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
try highFreqPlayer.start(atTime: 0)
} catch {
print("Error creating haptic patterns: \(error)")
}
}
private static var hapticEngine: CHHapticEngine?
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID)
@ -131,10 +120,8 @@ class VirtualController {
}
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
let scaleFactor = 32767.0 / 160
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
let scaledX = Int16(min(32767.0, max(-32768.0, x * 32767.0)))
let scaledY = Int16(min(32767.0, max(-32768.0, y * 32767.0)))
if stick == .right {
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
@ -148,7 +135,7 @@ class VirtualController {
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return }
print("Button: \(button.rawValue) {state: \(state)}")
// // print("Button: \(button.rawValue) {state: \(state)}")
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0

View file

@ -13,7 +13,7 @@ class MemoryUsageMonitor: ObservableObject {
private var timer: Timer?
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] _ in
self?.updateMemoryUsage()
}
}
@ -32,11 +32,12 @@ class MemoryUsageMonitor: ObservableObject {
}
if result == KERN_SUCCESS {
memoryUsage = 0
memoryUsage = taskInfo.phys_footprint
}
else {
print("Error with task_info(): " +
(String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
// print("Error with task_info(): " +
// (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
}
}
@ -46,7 +47,6 @@ class MemoryUsageMonitor: ObservableObject {
formatter.countStyle = .memory
return formatter.string(fromByteCount: Int64(bytes))
}
}

View file

@ -1,22 +0,0 @@
//
// Untitled.swift
// MeloNX
//
// Created by Stossy11 on 21/12/2024.
//
import SwiftUI
struct PerformanceOverlayView: View {
@StateObject private var memorymonitor = MemoryUsageMonitor()
@StateObject private var fpsmonitor = FPSMonitor()
var body: some View {
VStack {
Text("\(fpsmonitor.formatFPS())")
Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage))
}
}
}

View file

@ -6,34 +6,28 @@
//
import Foundation
import SwiftUI
class MTLHud {
class MTLHud: ObservableObject {
@Published var canMetalHud: Bool = false
var isEnabled: Bool {
if let getenv = getenv("MTL_HUD_ENABLED") {
return String(cString: getenv).contains("1")
@AppStorage("MTL_HUD_ENABLED") var metalHudEnabled: Bool = false {
didSet {
if metalHudEnabled {
enable()
} else {
disable()
}
}
return false
}
static let shared = MTLHud()
private init() {
let _ = openMetalDylib() // i'm fixing the warnings just because you said i suck at coding Autumn (propenchiefer,
https://youtu.be/tc65SNOTMz4 7:23)
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
enable()
} else {
disable()
}
}
func toggle() {
print(UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED"))
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
canMetalHud = openMetalDylib() // i'm fixing the warnings just because you said i suck at coding Autumn (propenchiefer, https://youtu.be/tc65SNOTMz4 7:23)
if metalHudEnabled {
enable()
} else {
disable()
@ -44,14 +38,8 @@ class MTLHud {
let path = "/usr/lib/libMTLHud.dylib"
if dlopen(path, RTLD_NOW) != nil {
print("Library loaded from \(path)")
canMetalHud = true
return true
} else {
if let error = String(validatingUTF8: dlerror()) {
print("Error loading library: \(error)")
}
canMetalHud = false
return false
}
}

View file

@ -10,6 +10,94 @@ import SwiftUI
import GameController
import MetalKit
import Metal
import Darwin
class LogCapture {
static let shared = LogCapture()
private var stdoutPipe: Pipe?
private var stderrPipe: Pipe?
private let originalStdout: Int32
private let originalStderr: Int32
var capturedLogs: [String] = [] {
didSet {
DispatchQueue.main.async {
NotificationCenter.default.post(name: .newLogCaptured, object: nil)
}
}
}
private init() {
originalStdout = dup(STDOUT_FILENO)
originalStderr = dup(STDERR_FILENO)
startCapturing()
}
func startCapturing() {
stdoutPipe = Pipe()
stderrPipe = Pipe()
redirectOutput(to: stdoutPipe!, fileDescriptor: STDOUT_FILENO)
redirectOutput(to: stderrPipe!, fileDescriptor: STDERR_FILENO)
setupReadabilityHandler(for: stdoutPipe!, isStdout: true)
setupReadabilityHandler(for: stderrPipe!, isStdout: false)
}
func stopCapturing() {
dup2(originalStdout, STDOUT_FILENO)
dup2(originalStderr, STDERR_FILENO)
stdoutPipe?.fileHandleForReading.readabilityHandler = nil
stderrPipe?.fileHandleForReading.readabilityHandler = nil
}
private func redirectOutput(to pipe: Pipe, fileDescriptor: Int32) {
dup2(pipe.fileHandleForWriting.fileDescriptor, fileDescriptor)
}
private func setupReadabilityHandler(for pipe: Pipe, isStdout: Bool) {
pipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
let data = fileHandle.availableData
let originalFD = isStdout ? self?.originalStdout : self?.originalStderr
write(originalFD ?? STDOUT_FILENO, (data as NSData).bytes, data.count)
if let logString = String(data: data, encoding: .utf8),
let cleanedLog = self?.cleanLog(logString), !cleanedLog.isEmpty {
self?.capturedLogs.append(cleanedLog)
}
}
}
private func cleanLog(_ raw: String) -> String? {
let lines = raw.split(separator: "\n")
let filteredLines = lines.filter { line in
!line.contains("SwiftUI") &&
!line.contains("ForEach") &&
!line.contains("VStack") &&
!line.contains("Invalid frame dimension (negative or non-finite).")
}
let cleaned = filteredLines.map { line -> String in
if let tabRange = line.range(of: "\t") {
return line[tabRange.upperBound...].trimmingCharacters(in: .whitespacesAndNewlines)
}
return line.trimmingCharacters(in: .whitespacesAndNewlines)
}.joined(separator: "\n")
return cleaned.isEmpty ? nil : cleaned.replacingOccurrences(of: "\n\n", with: "\n")
}
deinit {
stopCapturing()
}
}
extension Notification.Name {
static let newLogCaptured = Notification.Name("newLogCaptured")
}
struct Controller: Identifiable, Hashable {
var id: String
@ -30,22 +118,28 @@ struct iOSNav<Content: View>: View {
}
}
func threadEntry(_ arg: () -> Void) -> UnsafeMutableRawPointer? {
arg()
return nil
}
class Ryujinx : ObservableObject {
private var isRunning = false
@Published var isRunning = false
let virtualController = VirtualController()
@Published var controllerMap: [Controller] = []
@Published var metalLayer: CAMetalLayer? = nil
@Published var isPortrait = false
@Published var firmwareversion = "0"
@Published var emulationUIView: MeloMTKView? = nil
@Published var config: Ryujinx.Configuration? = nil
@Published var config: Ryujinx.Arguments? = nil
@Published var games: [Game] = []
@Published var defMLContentSize: CGFloat?
var thread: Thread!
var thread: pthread_t? = nil
@Published var jitenabled = false
@ -54,41 +148,76 @@ class Ryujinx : ObservableObject {
}
static let shared = Ryujinx()
private init() {
func addGames() {
self.games = loadGames()
}
public struct Configuration : Codable, Equatable {
func runloop(_ cool: @escaping () -> Void) {
if UserDefaults.standard.bool(forKey: "runOnMainThread") {
RunLoop.main.perform {
cool()
}
} else {
// Box the closure
let boxed = Unmanaged.passRetained(ClosureBox(cool)).toOpaque()
var thread: pthread_t?
let result = pthread_create(&thread, nil, { arg in
let unmanaged = Unmanaged<ClosureBox>.fromOpaque(arg)
let box = unmanaged.takeRetainedValue()
box.closure()
return nil
}, boxed)
if result == 0 {
pthread_detach(thread!)
} else {
print("Failed to create thread: \(result)")
Unmanaged<ClosureBox>.fromOpaque(boxed).release()
}
}
}
private class ClosureBox {
let closure: () -> Void
init(_ closure: @escaping () -> Void) {
self.closure = closure
}
}
public class Arguments : Observable, Codable, Equatable {
var gamepath: String
var inputids: [String]
var resscale: Float
var debuglogs: Bool
var tracelogs: Bool
var nintendoinput: Bool
var enableInternet: Bool
var listinputids: Bool
var aspectRatio: AspectRatio
var memoryManagerMode: String
var disableShaderCache: Bool
var hypervisor: Bool
var disableDockedMode: Bool
var enableTextureRecompression: Bool
var additionalArgs: [String]
var maxAnisotropy: Float
var macroHLE: Bool
var ignoreMissingServices: Bool
var expandRam: Bool
var dfsIntegrityChecks: Bool
var disablePTC: Bool
var disablevsync: Bool
var language: SystemLanguage
var regioncode: SystemRegionCode
var handHeldController: Bool
var inputDSUServers: [String]
var resscale: Float = 1.0
var debuglogs: Bool = false
var tracelogs: Bool = false
var nintendoinput: Bool = true
var enableInternet: Bool = false
var listinputids: Bool = false
var aspectRatio: AspectRatio = .fixed16x9
var memoryManagerMode: String = "HostMappedUnsafe"
var disableShaderCache: Bool = false
var hypervisor: Bool = false
var disableDockedMode: Bool = false
var enableTextureRecompression: Bool = true
var additionalArgs: [String] = []
var maxAnisotropy: Float = 1.0
var macroHLE: Bool = true
var ignoreMissingServices: Bool = false
var expandRam: Bool = false
var dfsIntegrityChecks: Bool = false
var disablePTC: Bool = false
var disablevsync: Bool = false
var language: SystemLanguage = .americanEnglish
var regioncode: SystemRegionCode = .usa
var handHeldController: Bool = true
init(gamepath: String,
init(gamepath: String = "",
inputids: [String] = [],
inputDSUServers: [String] = [],
debuglogs: Bool = false,
tracelogs: Bool = false,
listinputids: Bool = false,
@ -111,10 +240,11 @@ class Ryujinx : ObservableObject {
disablevsync: Bool = false,
language: SystemLanguage = .americanEnglish,
regioncode: SystemRegionCode = .usa,
handHeldController: Bool = false
handHeldController: Bool = false,
) {
self.gamepath = gamepath
self.inputids = inputids
self.inputDSUServers = inputDSUServers
self.debuglogs = debuglogs
self.tracelogs = tracelogs
self.listinputids = listinputids
@ -139,17 +269,71 @@ class Ryujinx : ObservableObject {
self.regioncode = regioncode
self.handHeldController = handHeldController
}
static func == (lhs: Arguments, rhs: Arguments) -> Bool {
return lhs.resscale == rhs.resscale &&
lhs.debuglogs == rhs.debuglogs &&
lhs.tracelogs == rhs.tracelogs &&
lhs.nintendoinput == rhs.nintendoinput &&
lhs.enableInternet == rhs.enableInternet &&
lhs.listinputids == rhs.listinputids &&
lhs.aspectRatio == rhs.aspectRatio &&
lhs.memoryManagerMode == rhs.memoryManagerMode &&
lhs.disableShaderCache == rhs.disableShaderCache &&
lhs.hypervisor == rhs.hypervisor &&
lhs.disableDockedMode == rhs.disableDockedMode &&
lhs.enableTextureRecompression == rhs.enableTextureRecompression &&
lhs.additionalArgs == rhs.additionalArgs &&
lhs.maxAnisotropy == rhs.maxAnisotropy &&
lhs.macroHLE == rhs.macroHLE &&
lhs.ignoreMissingServices == rhs.ignoreMissingServices &&
lhs.expandRam == rhs.expandRam &&
lhs.dfsIntegrityChecks == rhs.dfsIntegrityChecks &&
lhs.disablePTC == rhs.disablePTC &&
lhs.disablevsync == rhs.disablevsync &&
lhs.language == rhs.language &&
lhs.regioncode == rhs.regioncode &&
lhs.handHeldController == rhs.handHeldController
}
}
func start(with config: Configuration) throws {
func start(with config: Arguments) throws {
guard !isRunning else {
throw RyujinxError.alreadyRunning
}
self.config = config
thread = Thread { [self] in
if UserDefaults.standard.bool(forKey: "lockInApp") {
let cool = Thread {
while true {
if UserDefaults.standard.bool(forKey: "lockInApp") {
if let workspaceClass = NSClassFromString("LSApplicationWorkspace") as? NSObject.Type,
let workspace = workspaceClass.perform(NSSelectorFromString("defaultWorkspace"))?.takeUnretainedValue() {
let selector = NSSelectorFromString("openApplicationWithBundleID:")
if workspace.responds(to: selector) {
workspace.perform(selector, with: Bundle.main.bundleIdentifier ?? "")
} else {
print("Selector not found or not responding.")
}
} else {
print("Could not get LSApplicationWorkspace class.")
}
}
}
}
cool.qualityOfService = .userInteractive
cool.start()
}
runloop { [self] in
isRunning = true
@ -169,7 +353,9 @@ class Ryujinx : ObservableObject {
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
if result != 0 {
self.isRunning = false
DispatchQueue.main.async {
self.isRunning = false
}
if let accessing, accessing {
url!.stopAccessingSecurityScopedResource()
}
@ -178,17 +364,103 @@ class Ryujinx : ObservableObject {
}
}
} catch {
self.isRunning = false
Self.log("Emulation failed to start: \(error)")
DispatchQueue.main.async {
self.isRunning = false
}
Thread.sleep(forTimeInterval: 0.3)
let logs = LogCapture.shared.capturedLogs
let parsedLogs = extractExceptionInfo(logs)
if let parsedLogs {
DispatchQueue.main.async {
let result = Array(logs.suffix(from: parsedLogs.lineIndex))
LogCapture.shared.capturedLogs = Array(LogCapture.shared.capturedLogs.prefix(upTo: parsedLogs.lineIndex))
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
let currentDate = Date()
let dateString = dateFormatter.string(from: currentDate)
let path = URL.documentsDirectory.appendingPathComponent("StackTrace").appendingPathComponent("StackTrace-\(dateString).txt").path
self.saveArrayAsTextFile(strings: result, filePath: path)
presentAlert(title: "MeloNX Crashed!", message: parsedLogs.exceptionType + ": " + parsedLogs.message) {
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
exit(0)
}
}
}
} else {
DispatchQueue.main.async {
presentAlert(title: "MeloNX Crashed!", message: "Unknown Error") {
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
exit(0)
}
}
}
}
}
}
}
func saveArrayAsTextFile(strings: [String], filePath: String) {
let text = strings.joined(separator: "\n")
let path = URL.documentsDirectory.appendingPathComponent("StackTrace").path
do {
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false)
} catch {
}
do {
try text.write(to: URL(fileURLWithPath: filePath), atomically: true, encoding: .utf8)
print("File saved successfully.")
} catch {
print("Error saving file: \(error)")
}
}
struct ExceptionInfo {
let exceptionType: String
let message: String
let lineIndex: Int
}
func extractExceptionInfo(_ logs: [String]) -> ExceptionInfo? {
for i in (0..<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)
}
}
thread.qualityOfService = .background
thread.name = "MeloNX"
thread.start()
return nil
}
func stop() throws {
guard isRunning else {
throw RyujinxError.notRunning
@ -196,17 +468,14 @@ class Ryujinx : ObservableObject {
isRunning = false
UserDefaults.standard.set(false, forKey: "lockInApp")
self.emulationUIView = nil
self.metalLayer = nil
stop_emulation()
thread.cancel()
}
var running: Bool {
return isRunning
}
func loadGames() -> [Game] {
let fileManager = FileManager.default
@ -218,7 +487,7 @@ class Ryujinx : ObservableObject {
do {
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Failed to create roms directory: \(error)")
// print("Failed to create roms directory: \(error)")
}
}
var games: [Game] = []
@ -243,19 +512,18 @@ class Ryujinx : ObservableObject {
games.append(game)
} catch {
print(error)
// print(error)
}
}
return games
} catch {
print("Error loading games from roms folder: \(error)")
// print("Error loading games from roms folder: \(error)")
return games
}
}
private func buildCommandLineArgs(from config: Configuration) -> [String] {
func buildCommandLineArgs(from config: Arguments) -> [String] {
var args: [String] = []
// Add the game path
@ -267,18 +535,41 @@ class Ryujinx : ObservableObject {
args.append(contentsOf: ["--memory-manager-mode", config.memoryManagerMode])
// args.append(contentsOf: ["--exclusive-fullscreen", String(true)])
// args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"])
// args.append(contentsOf: ["--exclusive-fullscreen-height", "\(Int(UIScreen.main.bounds.height))"])
args.append(contentsOf: ["--exclusive-fullscreen", String(true)])
args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"])
args.append(contentsOf: ["--exclusive-fullscreen-height", "\(Int(UIScreen.main.bounds.height))"])
// We don't need this. Ryujinx should handle it fine :3
// this also causes crashes in some games :3
var model = ""
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
model = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
args.append(contentsOf: ["--device-model", model])
args.append(contentsOf: ["--device-display-name", UIDevice.modelName])
if checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") {
args.append("--has-memory-entitlement")
}
args.append(contentsOf: ["--system-language", config.language.rawValue])
args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
args.append(contentsOf: ["--system-timezone", TimeZone.current.identifier])
// args.append(contentsOf: ["--system-time-offset", String(TimeZone.current.secondsFromGMT())])
if config.nintendoinput {
args.append("--correct-controller")
}
@ -356,6 +647,16 @@ class Ryujinx : ObservableObject {
}
}
// Append the input dsu servers (limit to 8 (used to be 4) just in case)
if !config.inputDSUServers.isEmpty {
config.inputDSUServers.prefix(8).enumerated().forEach { index, inputDSUServer in
if index == 0 {
args.append(contentsOf: ["--input-dsu-server-handheld", inputDSUServer])
}
args.append(contentsOf: ["--input-dsu-server-\(index + 1)", inputDSUServer])
}
}
args.append(contentsOf: config.additionalArgs)
return args
@ -383,7 +684,7 @@ class Ryujinx : ObservableObject {
func installFirmware(firmwarePath: String) {
guard let cString = firmwarePath.cString(using: .utf8) else {
print("Invalid firmware path")
// print("Invalid firmware path")
return
}
@ -399,12 +700,12 @@ class Ryujinx : ObservableObject {
guard let titleIdCString = titleId.cString(using: .utf8),
let pathCString = path.cString(using: .utf8)
else {
print("Invalid path")
// print("Invalid path")
return []
}
let listPointer = get_dlc_nca_list(titleIdCString, pathCString)
print("DLC parcing success: \(listPointer.success)")
// print("DLC parcing success: \(listPointer.success)")
guard listPointer.success else { return [] }
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
@ -456,7 +757,7 @@ class Ryujinx : ObservableObject {
let guid = generateGamepadId(joystickIndex: i)
let name = String(cString: SDL_GameControllerName(controller))
print("Controller \(i): \(name), GUID: \(guid ?? "")")
// print("Controller \(i): \(name), GUID: \(guid ?? "")")
guard let guid else {
SDL_GameControllerClose(controller)
@ -487,33 +788,163 @@ class Ryujinx : ObservableObject {
do {
if fileManager.fileExists(atPath: registeredFolder) {
try fileManager.removeItem(atPath: registeredFolder)
print("Folder removed successfully.")
// print("Folder removed successfully.")
let version = fetchFirmwareVersion()
if version.isEmpty {
self.firmwareversion = "0"
} else {
print("Firmware eeeeee \(version)")
// print("Firmware eeeeee \(version)")
}
} else {
print("Folder does not exist.")
// print("Folder does not exist.")
}
} catch {
print("Error removing folder: \(error)")
// print("Error removing folder: \(error)")
}
}
static func log(_ message: String) {
print("[Ryujinx] \(message)")
// print("[Ryujinx] \(message)")
}
public func updateOrientation() -> Bool {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
return (window.bounds.size.height > window.bounds.size.width)
}
return false
}
func ryuIsJITEnabled() {
jitenabled = isJITEnabled()
print("JIT \(jitenabled)")
}
}
public extension UIDevice {
static let modelName: String = {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity
#if os(iOS)
switch identifier {
case "iPod5,1": return "iPod touch (5th generation)"
case "iPod7,1": return "iPod touch (6th generation)"
case "iPod9,1": return "iPod touch (7th generation)"
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4"
case "iPhone4,1": return "iPhone 4s"
case "iPhone5,1", "iPhone5,2": return "iPhone 5"
case "iPhone5,3", "iPhone5,4": return "iPhone 5c"
case "iPhone6,1", "iPhone6,2": return "iPhone 5s"
case "iPhone7,2": return "iPhone 6"
case "iPhone7,1": return "iPhone 6 Plus"
case "iPhone8,1": return "iPhone 6s"
case "iPhone8,2": return "iPhone 6s Plus"
case "iPhone9,1", "iPhone9,3": return "iPhone 7"
case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus"
case "iPhone10,1", "iPhone10,4": return "iPhone 8"
case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus"
case "iPhone10,3", "iPhone10,6": return "iPhone X"
case "iPhone11,2": return "iPhone XS"
case "iPhone11,4", "iPhone11,6": return "iPhone XS Max"
case "iPhone11,8": return "iPhone XR"
case "iPhone12,1": return "iPhone 11"
case "iPhone12,3": return "iPhone 11 Pro"
case "iPhone12,5": return "iPhone 11 Pro Max"
case "iPhone13,1": return "iPhone 12 mini"
case "iPhone13,2": return "iPhone 12"
case "iPhone13,3": return "iPhone 12 Pro"
case "iPhone13,4": return "iPhone 12 Pro Max"
case "iPhone14,4": return "iPhone 13 mini"
case "iPhone14,5": return "iPhone 13"
case "iPhone14,2": return "iPhone 13 Pro"
case "iPhone14,3": return "iPhone 13 Pro Max"
case "iPhone14,7": return "iPhone 14"
case "iPhone14,8": return "iPhone 14 Plus"
case "iPhone15,2": return "iPhone 14 Pro"
case "iPhone15,3": return "iPhone 14 Pro Max"
case "iPhone15,4": return "iPhone 15"
case "iPhone15,5": return "iPhone 15 Plus"
case "iPhone16,1": return "iPhone 15 Pro"
case "iPhone16,2": return "iPhone 15 Pro Max"
case "iPhone17,3": return "iPhone 16"
case "iPhone17,4": return "iPhone 16 Plus"
case "iPhone17,1": return "iPhone 16 Pro"
case "iPhone17,2": return "iPhone 16 Pro Max"
case "iPhone17,5": return "iPhone 16e"
case "iPhone8,4": return "iPhone SE"
case "iPhone12,8": return "iPhone SE (2nd generation)"
case "iPhone14,6": return "iPhone SE (3rd generation)"
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2"
case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)"
case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)"
case "iPad6,11", "iPad6,12": return "iPad (5th generation)"
case "iPad7,5", "iPad7,6": return "iPad (6th generation)"
case "iPad7,11", "iPad7,12": return "iPad (7th generation)"
case "iPad11,6", "iPad11,7": return "iPad (8th generation)"
case "iPad12,1", "iPad12,2": return "iPad (9th generation)"
case "iPad13,18", "iPad13,19": return "iPad (10th generation)"
case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air"
case "iPad5,3", "iPad5,4": return "iPad Air 2"
case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)"
case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
case "iPad13,16", "iPad13,17": return "iPad Air (5th generation)"
case "iPad14,8", "iPad14,9": return "iPad Air (11-inch) (M2)"
case "iPad14,10", "iPad14,11": return "iPad Air (13-inch) (M2)"
case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini"
case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2"
case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3"
case "iPad5,1", "iPad5,2": return "iPad mini 4"
case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)"
case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)"
case "iPad16,1", "iPad16,2": return "iPad mini (A17 Pro)"
case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)"
case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)"
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)"
case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)"
case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)"
case "iPad14,3", "iPad14,4": return "iPad Pro (11-inch) (4th generation)"
case "iPad16,3", "iPad16,4": return "iPad Pro (11-inch) (M4)"
case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)"
case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)"
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)"
case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)"
case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)"
case "iPad14,5", "iPad14,6": return "iPad Pro (12.9-inch) (6th generation)"
case "iPad16,5", "iPad16,6": return "iPad Pro (13-inch) (M4)"
case "AppleTV5,3": return "Apple TV"
case "AppleTV6,2": return "Apple TV 4K"
case "AudioAccessory1,1": return "HomePod"
case "AudioAccessory5,1": return "HomePod mini"
case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
default: return identifier
}
#elseif os(tvOS)
switch identifier {
case "AppleTV5,3": return "Apple TV 4"
case "AppleTV6,2", "AppleTV11,1", "AppleTV14,1": return "Apple TV 4K"
case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))"
default: return identifier
}
#elseif os(visionOS)
switch identifier {
case "RealityDevice14,1": return "Apple Vision Pro"
default: return identifier
}
#endif
}
return mapToDevice(identifier: identifier)
}()
}

View file

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

View file

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

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,372 +0,0 @@
//
// ControllerView.swift
// Pomelo-V2
//
// Created by Stossy11 on 16/7/2024.
//
import SwiftUI
import GameController
import SwiftUIJoystick
import CoreMotion
struct ControllerView: View {
// MARK: - Properties
@AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0
@AppStorage("stick-button") private var stickButton = false
@State private var isPortrait = true
@Environment(\.verticalSizeClass) var verticalSizeClass
// MARK: - Body
var body: some View {
Group {
let isPad = UIDevice.current.userInterfaceIdiom == .pad
if isPortrait && !isPad {
portraitLayout
} else {
landscapeLayout
}
}
.padding()
.onChange(of: verticalSizeClass) { _ in
updateOrientation()
}
.onAppear(perform: updateOrientation)
}
// MARK: - Layouts
private var portraitLayout: some View {
VStack {
Spacer()
VStack(spacing: 20) {
HStack(spacing: 30) {
VStack(spacing: 15) {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
VStack(spacing: 15) {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true)
ABXYView()
}
}
}
HStack(spacing: 60) {
HStack {
ButtonView(button: .leftStick)
.padding()
ButtonView(button: .start)
}
HStack {
ButtonView(button: .back)
ButtonView(button: .rightStick)
.padding()
}
}
}
}
}
private var landscapeLayout: some View {
VStack {
Spacer()
HStack {
VStack(spacing: 15) {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
Spacer()
centerButtons
Spacer()
VStack(spacing: 15) {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true)
ABXYView()
}
}
}
}
}
private var centerButtons: some View {
Group {
if stickButton {
VStack {
HStack(spacing: 50) {
ButtonView(button: .leftStick)
.padding()
Spacer()
ButtonView(button: .rightStick)
.padding()
}
.padding(.top, 30)
HStack(spacing: 50) {
ButtonView(button: .back)
Spacer()
ButtonView(button: .start)
}
}
.padding(.bottom, 20)
} else {
HStack(spacing: 50) {
ButtonView(button: .back)
Spacer()
ButtonView(button: .start)
}
.padding(.bottom, 20)
}
}
}
// MARK: - Methods
private func updateOrientation() {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
isPortrait = window.bounds.size.height > window.bounds.size.width
}
}
}
struct ShoulderButtonsViewLeft: View {
@State private var width: CGFloat = 160
@State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack(spacing: 20) {
ButtonView(button: .leftTrigger)
ButtonView(button: .leftShoulder)
}
.frame(width: width, height: height)
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
}
}
}
struct ShoulderButtonsViewRight: View {
@State private var width: CGFloat = 160
@State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack(spacing: 20) {
ButtonView(button: .rightShoulder)
ButtonView(button: .rightTrigger)
}
.frame(width: width, height: height)
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
}
}
}
struct DPadView: View {
@State private var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack(spacing: 5) {
ButtonView(button: .dPadUp)
HStack(spacing: 20) {
ButtonView(button: .dPadLeft)
Spacer(minLength: 20)
ButtonView(button: .dPadRight)
}
ButtonView(button: .dPadDown)
}
.frame(width: size, height: size)
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
size *= CGFloat(controllerScale)
}
}
}
struct ABXYView: View {
@State private var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack(spacing: 5) {
ButtonView(button: .X)
HStack(spacing: 20) {
ButtonView(button: .Y)
Spacer(minLength: 20)
ButtonView(button: .A)
}
ButtonView(button: .B)
}
.frame(width: size, height: size)
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
size *= CGFloat(controllerScale)
}
}
}
struct ButtonView: View {
var button: VirtualControllerButton
@State private var width: CGFloat = 45
@State private var height: CGFloat = 45
@State private var isPressed = false
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.presentationMode) var presentationMode
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State private var debounceTimer: Timer?
var body: some View {
Image(systemName: buttonText)
.resizable()
.scaledToFit()
.frame(width: width, height: height)
.foregroundColor(true ? Color.white.opacity(0.9) : Color.black.opacity(0.9))
.background(
Group {
if !button.isTrigger {
Circle()
.fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
.frame(width: width * 1.25, height: height * 1.25)
} else {
Image(systemName: buttonText)
.resizable()
.scaledToFit()
.frame(width: width * 1.25, height: height * 1.25)
.foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
}
}
)
.opacity(isPressed ? 0.6 : 1.0)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
handleButtonPress()
}
.onEnded { _ in
handleButtonRelease()
}
)
.onAppear {
configureSizeForButton()
}
}
private func handleButtonPress() {
if !isPressed {
isPressed = true
debounceTimer?.invalidate()
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.medium)
}
}
private func handleButtonRelease() {
if isPressed {
isPressed = false
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { _ in
Ryujinx.shared.virtualController.setButtonState(0, for: button)
}
}
}
private func configureSizeForButton() {
if button.isTrigger {
width = 70
height = 40
} else if button.isSmall {
width = 35
height = 35
}
// Adjust for iPad
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
}
private var buttonText: String {
switch button {
case .A:
return "a.circle.fill"
case .B:
return "b.circle.fill"
case .X:
return "x.circle.fill"
case .Y:
return "y.circle.fill"
case .leftStick:
return "l.joystick.press.down.fill"
case .rightStick:
return "r.joystick.press.down.fill"
case .dPadUp:
return "arrowtriangle.up.circle.fill"
case .dPadDown:
return "arrowtriangle.down.circle.fill"
case .dPadLeft:
return "arrowtriangle.left.circle.fill"
case .dPadRight:
return "arrowtriangle.right.circle.fill"
case .leftTrigger:
return "zl.rectangle.roundedtop.fill"
case .rightTrigger:
return "zr.rectangle.roundedtop.fill"
case .leftShoulder:
return "l.rectangle.roundedbottom.fill"
case .rightShoulder:
return "r.rectangle.roundedbottom.fill"
case .start:
return "plus.circle.fill"
case .back:
return "minus.circle.fill"
case .guide:
return "house.circle.fill"
}
}
}

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

View file

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

View file

@ -0,0 +1,558 @@
//
// ControllerView.swift
// Pomelo-V2
//
// Created by Stossy11 on 16/7/2024.
//
import SwiftUI
import GameController
import CoreMotion
struct ControllerView: View {
// MARK: - Properties
@AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0
@AppStorage("stick-button") private var stickButton = false
@State private var isPortrait = true
@State var hideDpad = false
@State var hideABXY = false
@Environment(\.verticalSizeClass) var verticalSizeClass
// MARK: - Body
var body: some View {
Group {
let isPad = UIDevice.current.userInterfaceIdiom == .pad
if isPortrait && !isPad {
portraitLayout
} else {
landscapeLayout
}
}
.padding()
.onChange(of: verticalSizeClass) { _ in
updateOrientation()
}
.onAppear(perform: updateOrientation)
}
// MARK: - Layouts
private var portraitLayout: some View {
VStack {
Spacer()
VStack(spacing: 20) {
HStack(spacing: 30) {
VStack(spacing: 15) {
ShoulderButtonsViewLeft()
.padding(.vertical)
ZStack {
JoystickController(showBackground: $hideDpad)
DPadView()
.opacity(hideDpad ? 0 : 1)
.allowsHitTesting(!hideDpad)
.animation(.easeInOut(duration: 0.2), value: hideDpad)
}
}
VStack(spacing: 15) {
ShoulderButtonsViewRight()
.padding(.vertical)
ZStack {
JoystickController(iscool: true, showBackground: $hideABXY)
ABXYView()
.opacity(hideABXY ? 0 : 1)
.allowsHitTesting(!hideABXY)
.animation(.easeInOut(duration: 0.2), value: hideABXY)
}
}
}
HStack(spacing: 60) {
HStack {
ButtonView(button: .leftStick)
.padding()
ButtonView(button: .back)
}
HStack {
ButtonView(button: .start)
ButtonView(button: .rightStick)
.padding()
}
}
}
}
}
private var landscapeLayout: some View {
VStack {
Spacer()
HStack {
VStack(spacing: 20) {
ShoulderButtonsViewLeft()
.padding(.vertical)
ZStack {
JoystickController(showBackground: $hideDpad)
DPadView()
.opacity(hideDpad ? 0 : 1)
.allowsHitTesting(!hideDpad)
.animation(.easeInOut(duration: 0.2), value: hideDpad)
}
}
Spacer()
centerButtons
Spacer()
VStack(spacing: 20) {
ShoulderButtonsViewRight()
.padding(.vertical)
ZStack {
JoystickController(iscool: true, showBackground: $hideABXY)
ABXYView()
.opacity(hideABXY ? 0 : 1)
.allowsHitTesting(!hideABXY)
.animation(.easeInOut(duration: 0.2), value: hideABXY)
}
}
}
}
}
private var centerButtons: some View {
Group {
if stickButton {
VStack {
HStack(spacing: 50) {
ButtonView(button: .leftStick)
.padding()
Spacer()
ButtonView(button: .rightStick)
.padding()
}
.padding(.top, 30)
HStack(spacing: 50) {
ButtonView(button: .back)
Spacer()
ButtonView(button: .start)
}
}
.padding(.bottom, 20)
} else {
HStack(spacing: 50) {
ButtonView(button: .back)
Spacer()
ButtonView(button: .start)
}
.padding(.bottom, 20)
}
}
}
// MARK: - Methods
private func updateOrientation() {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
isPortrait = window.bounds.size.height > window.bounds.size.width
}
}
}
struct ShoulderButtonsViewLeft: View {
@State private var width: CGFloat = 160
@State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack(spacing: 20) {
ButtonView(button: .leftTrigger)
ButtonView(button: .leftShoulder)
}
.frame(width: width, height: height)
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
}
}
}
struct ShoulderButtonsViewRight: View {
@State private var width: CGFloat = 160
@State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack(spacing: 20) {
ButtonView(button: .rightShoulder)
ButtonView(button: .rightTrigger)
}
.frame(width: width, height: height)
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
}
}
}
struct DPadView: View {
@State private var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack(spacing: 7) {
ButtonView(button: .dPadUp)
HStack(spacing: 22) {
ButtonView(button: .dPadLeft)
Spacer(minLength: 22)
ButtonView(button: .dPadRight)
}
ButtonView(button: .dPadDown)
}
.frame(width: size, height: size)
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
size *= CGFloat(controllerScale)
}
}
}
struct ABXYView: View {
@State private var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack(spacing: 7) {
ButtonView(button: .X)
HStack(spacing: 22) {
ButtonView(button: .Y)
Spacer(minLength: 22)
ButtonView(button: .A)
}
ButtonView(button: .B)
}
.frame(width: size, height: size)
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
size *= CGFloat(controllerScale)
}
}
}
struct ButtonView: View {
var button: VirtualControllerButton
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@Environment(\.presentationMode) var presentationMode
@AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState()
@State private var istoggle = false
@State private var isPressed = false
@State private var toggleState = false
@State private var size: CGSize = .zero
var body: some View {
Circle()
.foregroundStyle(.clear.opacity(0))
.overlay {
Image(systemName: buttonConfig.iconName)
.resizable()
.scaledToFit()
.frame(width: size.width, height: size.height)
.foregroundStyle(.white)
.opacity(isPressed ? 0.6 : 0.8)
.allowsHitTesting(false)
}
.frame(width: size.width, height: size.height)
.background(
buttonBackground
)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in handleButtonPress() }
.onEnded { _ in handleButtonRelease() }
)
.onAppear {
istoggle = (toggleButtons.toggle1 && button == .A) || (toggleButtons.toggle2 && button == .B) || (toggleButtons.toggle3 && button == .X) || (toggleButtons.toggle4 && button == .Y)
size = calculateButtonSize()
}
.onChange(of: controllerScale) { _ in
size = calculateButtonSize()
}
}
private var buttonBackground: some View {
Group {
if !button.isTrigger && button != .leftStick && button != .rightStick {
Circle()
.fill(Color.gray.opacity(0.4))
.frame(width: size.width * 1.25, height: size.height * 1.25)
} else if button == .leftStick || button == .rightStick {
Image(systemName: buttonConfig.iconName)
.resizable()
.scaledToFit()
.frame(width: size.width * 1.25, height: size.height * 1.25)
.foregroundColor(Color.gray.opacity(0.4))
} else if button.isTrigger {
Image(systemName: convertTriggerIconToButton(buttonConfig.iconName))
.resizable()
.scaledToFit()
.frame(width: size.width * 1.25, height: size.height * 1.25)
.foregroundColor(Color.gray.opacity(0.4))
}
}
}
private func convertTriggerIconToButton(_ iconName: String) -> String {
if iconName.hasPrefix("zl") || iconName.hasPrefix("zr") {
var converted = String(iconName.dropFirst(3))
converted = converted.replacingOccurrences(of: "rectangle", with: "button")
converted = converted.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
return converted
} else {
var converted = String(iconName.dropFirst(2))
converted = converted.replacingOccurrences(of: "rectangle", with: "button")
converted = converted.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
return converted
}
}
private func handleButtonPress() {
guard !isPressed || istoggle else { return }
if istoggle {
toggleState.toggle()
isPressed = toggleState
let value = toggleState ? 1 : 0
Ryujinx.shared.virtualController.setButtonState(Uint8(value), for: button)
Haptics.shared.play(.medium)
} else {
isPressed = true
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.medium)
}
}
private func handleButtonRelease() {
if istoggle { return }
guard isPressed else { return }
isPressed = false
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.05) {
Ryujinx.shared.virtualController.setButtonState(0, for: button)
}
}
private func calculateButtonSize() -> CGSize {
let baseWidth: CGFloat
let baseHeight: CGFloat
if button.isTrigger {
baseWidth = 70
baseHeight = 40
} else if button.isSmall {
baseWidth = 35
baseHeight = 35
} else {
baseWidth = 45
baseHeight = 45
}
let deviceMultiplier = UIDevice.current.userInterfaceIdiom == .pad ? 1.2 : 1.0
let scaleMultiplier = CGFloat(controllerScale)
return CGSize(
width: baseWidth * deviceMultiplier * scaleMultiplier,
height: baseHeight * deviceMultiplier * scaleMultiplier
)
}
// Centralized button configuration
private var buttonConfig: ButtonConfiguration {
switch button {
case .A:
return ButtonConfiguration(iconName: "a.circle.fill")
case .B:
return ButtonConfiguration(iconName: "b.circle.fill")
case .X:
return ButtonConfiguration(iconName: "x.circle.fill")
case .Y:
return ButtonConfiguration(iconName: "y.circle.fill")
case .leftStick:
return ButtonConfiguration(iconName: "l.joystick.press.down.fill")
case .rightStick:
return ButtonConfiguration(iconName: "r.joystick.press.down.fill")
case .dPadUp:
return ButtonConfiguration(iconName: "arrowtriangle.up.circle.fill")
case .dPadDown:
return ButtonConfiguration(iconName: "arrowtriangle.down.circle.fill")
case .dPadLeft:
return ButtonConfiguration(iconName: "arrowtriangle.left.circle.fill")
case .dPadRight:
return ButtonConfiguration(iconName: "arrowtriangle.right.circle.fill")
case .leftTrigger:
return ButtonConfiguration(iconName: "zl.rectangle.roundedtop.fill")
case .rightTrigger:
return ButtonConfiguration(iconName: "zr.rectangle.roundedtop.fill")
case .leftShoulder:
return ButtonConfiguration(iconName: "l.rectangle.roundedbottom.fill")
case .rightShoulder:
return ButtonConfiguration(iconName: "r.rectangle.roundedbottom.fill")
case .start:
return ButtonConfiguration(iconName: "plus.circle.fill")
case .back:
return ButtonConfiguration(iconName: "minus.circle.fill")
case .guide:
return ButtonConfiguration(iconName: "house.circle.fill")
}
}
struct ButtonConfiguration {
let iconName: String
}
}
struct ExtButtonIconView: View {
var button: VirtualControllerButton
var opacity = 0.8
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State private var size: CGSize = .zero
var body: some View {
Circle()
.foregroundStyle(.clear.opacity(0))
.overlay {
Image(systemName: buttonConfig.iconName)
.resizable()
.scaledToFit()
.frame(width: size.width / 1.5, height: size.height / 1.5)
.foregroundStyle(.white)
.opacity(opacity)
.allowsHitTesting(false)
}
.frame(width: size.width, height: size.height)
.background(
buttonBackground
)
.onAppear {
size = calculateButtonSize()
}
.onChange(of: controllerScale) { _ in
size = calculateButtonSize()
}
}
private var buttonBackground: some View {
Group {
if !button.isTrigger && button != .leftStick && button != .rightStick {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: size.width * 1.25, height: size.height * 1.25)
} else if button == .leftStick || button == .rightStick {
Image(systemName: buttonConfig.iconName)
.resizable()
.scaledToFit()
.frame(width: size.width * 1.25, height: size.height * 1.25)
.foregroundColor(Color.gray.opacity(0.4))
} else if button.isTrigger {
Image(systemName: convertTriggerIconToButton(buttonConfig.iconName))
.resizable()
.scaledToFit()
.frame(width: size.width * 1.25, height: size.height * 1.25)
.foregroundColor(Color.gray.opacity(0.4))
}
}
}
private func convertTriggerIconToButton(_ iconName: String) -> String {
var converted = iconName
if iconName.hasPrefix("zl") || iconName.hasPrefix("zr") {
converted = String(iconName.dropFirst(3))
} else {
converted = String(iconName.dropFirst(2))
}
converted = converted
.replacingOccurrences(of: "rectangle", with: "button")
.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
return converted
}
private func calculateButtonSize() -> CGSize {
let baseWidth: CGFloat
let baseHeight: CGFloat
if button.isTrigger {
baseWidth = 70
baseHeight = 40
} else if button.isSmall {
baseWidth = 35
baseHeight = 35
} else {
baseWidth = 45
baseHeight = 45
}
let deviceMultiplier = UIDevice.current.userInterfaceIdiom == .pad ? 1.2 : 1.0
let scaleMultiplier = CGFloat(controllerScale)
return CGSize(
width: baseWidth * deviceMultiplier * scaleMultiplier,
height: baseHeight * deviceMultiplier * scaleMultiplier
)
}
private var buttonConfig: ButtonConfiguration {
switch button {
case .A: return .init(iconName: "a.circle.fill")
case .B: return .init(iconName: "b.circle.fill")
case .X: return .init(iconName: "x.circle.fill")
case .Y: return .init(iconName: "y.circle.fill")
case .leftStick: return .init(iconName: "l.joystick.press.down.fill")
case .rightStick: return .init(iconName: "r.joystick.press.down.fill")
case .dPadUp: return .init(iconName: "arrowtriangle.up.circle.fill")
case .dPadDown: return .init(iconName: "arrowtriangle.down.circle.fill")
case .dPadLeft: return .init(iconName: "arrowtriangle.left.circle.fill")
case .dPadRight: return .init(iconName: "arrowtriangle.right.circle.fill")
case .leftTrigger: return .init(iconName: "zl.rectangle.roundedtop.fill")
case .rightTrigger: return .init(iconName: "zr.rectangle.roundedtop.fill")
case .leftShoulder: return .init(iconName: "l.rectangle.roundedbottom.fill")
case .rightShoulder: return .init(iconName: "r.rectangle.roundedbottom.fill")
case .start: return .init(iconName: "plus.circle.fill")
case .back: return .init(iconName: "minus.circle.fill")
case .guide: return .init(iconName: "gearshape.fill")
}
}
struct ButtonConfiguration {
let iconName: String
}
}

View file

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

View file

@ -0,0 +1,93 @@
//
// Joystick.swift
// MeloNX
//
// Created by Stossy11 on 21/03/2025.
//
import SwiftUI
struct Joystick: View {
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@Binding var position: CGPoint
@State var joystickSize: CGFloat
var boundarySize: CGFloat
@State private var offset: CGSize = .zero
@Binding var showBackground: Bool
let sensitivity: CGFloat = 1.2
var dragGesture: some Gesture {
DragGesture()
.onChanged { value in
withAnimation(.easeIn) {
showBackground = true
}
let translation = value.translation
let distance = sqrt(translation.width * translation.width + translation.height * translation.height)
let maxRadius = (boundarySize - joystickSize) / 2
let extendedRadius = maxRadius + (joystickSize / 2)
if distance <= extendedRadius {
offset = translation
} else {
let angle = atan2(translation.height, translation.width)
offset = CGSize(width: cos(angle) * extendedRadius, height: sin(angle) * extendedRadius)
}
position = CGPoint(
x: max(-1, min(1, (offset.width / extendedRadius) * sensitivity)),
y: max(-1, min(1, (offset.height / extendedRadius) * sensitivity))
)
}
.onEnded { _ in
offset = .zero
position = .zero
withAnimation(.easeOut) {
showBackground = false
}
}
}
var body: some View {
ZStack {
Circle()
.fill(Color.clear.opacity(0))
.frame(width: boundarySize, height: boundarySize)
.scaleEffect(controllerScale)
if showBackground {
Circle()
.fill(Color.gray.opacity(0.4))
.frame(width: boundarySize, height: boundarySize)
.animation(.easeInOut(duration: 0.1), value: showBackground)
.scaleEffect(controllerScale)
}
Circle()
.fill(Color.white.opacity(0.5))
.frame(width: joystickSize, height: joystickSize)
.background(
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: joystickSize * 1.25, height: joystickSize * 1.25)
)
.offset(offset)
.gesture(dragGesture)
.scaleEffect(controllerScale)
}
.frame(width: boundarySize, height: boundarySize)
.onChange(of: showBackground) { newValue in
if newValue {
joystickSize *= 1.4
} else {
joystickSize = (boundarySize * 0.2)
}
}
}
}

View file

@ -0,0 +1,39 @@
//
// JoystickView.swift
// Pomelo
//
// Created by Stossy11 on 30/9/2024.
// Copyright © 2024 Stossy11. All rights reserved.
//
import SwiftUI
struct JoystickController: View {
@State var iscool: Bool? = nil
@Environment(\.colorScheme) var colorScheme
@Binding var showBackground: Bool
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State var position: CGPoint = CGPoint(x: 0, y: 0)
var dragDiameter: CGFloat {
var selfs = CGFloat(160)
// selfs *= controllerScale
if UIDevice.current.systemName.contains("iPadOS") {
return selfs * 1.2
}
return selfs
}
public var body: some View {
VStack {
Joystick(position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground)
.onChange(of: position) { newValue in
if iscool != nil {
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
} else {
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: newValue.x, y: newValue.y)
}
}
}
}
}

View file

@ -14,11 +14,20 @@ struct EmulationView: View {
@AppStorage("showScreenShotButton") var ssb: Bool = false
@AppStorage("showlogsgame") var showlogsgame: Bool = false
@AppStorage("On-ScreenControllerOpacity") var controllerOpacity: Double = 1.0
@AppStorage("disableTouch") var blackScreen = false
@State var isPresentedThree: Bool = false
@State var isAirplaying = Air.shared.connected
@Binding var startgame: Game?
@Environment(\.scenePhase) var scenePhase
@State private var isInBackground = false
@State var showSettings = false
@State var pauseEmu = true
@AppStorage("location-enabled") var locationenabled: Bool = false
var body: some View {
ZStack {
if isAirplaying {
@ -26,8 +35,13 @@ struct EmulationView: View {
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
.onAppear {
Air.play(AnyView(MetalView().ignoresSafeArea()))
Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
}
Color.black
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
.allowsHitTesting(false)
} else {
MetalView() // The Emulation View
.ignoresSafeArea()
@ -38,6 +52,8 @@ struct EmulationView: View {
if isVCA {
ControllerView() // Virtual Controller
.opacity(controllerOpacity)
.allowsHitTesting(true)
}
Group {
@ -62,38 +78,98 @@ struct EmulationView: View {
Spacer()
}
Spacer()
if ssb {
HStack {
Button {
if let screenshot = Ryujinx.shared.emulationUIView?.screenshot() {
UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
Menu {
/*
Button {
showSettings.toggle()
} label: {
Label {
Text("Game Settings")
} icon: {
Image(systemName: "gearshape.circle")
}
}
*/
Button {
pause_emulation(pauseEmu)
pauseEmu.toggle()
} label: {
Label {
Text(pauseEmu ? "Pause" : "Play")
} icon: {
Image(systemName: pauseEmu ? "pause.circle" : "play.circle")
}
}
Button(role: .destructive) {
startgame = nil
stop_emulation()
try? Ryujinx.shared.stop()
} label: {
Label {
Text("Exit (Unstable)")
} icon: {
Image(systemName: "x.circle")
}
}
} label: {
Image(systemName: "square.and.arrow.up")
ExtButtonIconView(button: .guide, opacity: 0.4)
}
.frame(width: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45, height: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45)
.padding()
Spacer()
}
}
Spacer()
}
}
}
.onAppear {
LocationManager.sharedInstance.startUpdatingLocation()
Air.shared.connectionCallbacks.append { cool in
DispatchQueue.main.async {
isAirplaying = cool
// print(cool)
}
}
RegisterCallback("exit-emulation") { cool in
DispatchQueue.main.async {
print(cool)
startgame = nil
stop_emulation()
try? Ryujinx.shared.stop()
}
}
}
.onChange(of: scenePhase) { newPhase in
// Detect when the app enters the background
if newPhase == .background {
pause_emulation(true)
isInBackground = true
} else if newPhase == .active {
pause_emulation(false)
isInBackground = false
} else if newPhase == .inactive {
pause_emulation(true)
isInBackground = true
}
}
.sheet(isPresented: $showSettings) {
// PerGameSettingsView(titleId: startgame?.titleId ?? "", manager: InGameSettingsManager.shared)
// .onDisappear() {
// InGameSettingsManager.shared.saveSettings()
// }
}
}
}

View file

@ -0,0 +1,61 @@
//
// InGameSettingsManager.swift
// MeloNX
//
// Created by Stossy11 on 12/06/2025.
//
import Foundation
class InGameSettingsManager: PerGameSettingsManaging {
@Published var config: [String: Ryujinx.Arguments]
private var saveWorkItem: DispatchWorkItem?
public static var shared = InGameSettingsManager()
private init() {
self.config = PerGameSettingsManager.loadSettings() ?? [:]
}
func debouncedSave() {
saveWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
self.saveSettings()
}
saveWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
}
func saveSettings() {
if let currentgame = Ryujinx.shared.games.first(where: { $0.fileURL == URL(string: Ryujinx.shared.config?.gamepath ?? "") }) {
Ryujinx.shared.config = config[currentgame.titleId]
let args = Ryujinx.shared.buildCommandLineArgs(from: config[currentgame.titleId] ?? Ryujinx.Arguments())
let cArgs = args.map { strdup($0) }
defer { cArgs.forEach { free($0) } }
var argvPtrs = cArgs
let result = update_settings_external(Int32(args.count), &argvPtrs)
print(result)
}
}
static func loadSettings() -> [String: Ryujinx.Arguments]? {
var cool: [String: Ryujinx.Arguments] = [:]
if let currentgame = Ryujinx.shared.games.first(where: { $0.fileURL == URL(string: Ryujinx.shared.config?.gamepath ?? "") }) {
cool[currentgame.titleId] = Ryujinx.shared.config
return cool
} else {
return nil
}
}
func loadSettings() {
self.config = PerGameSettingsManager.loadSettings() ?? [:]
}
}

View file

@ -0,0 +1,70 @@
//
// Untitled.swift
// MeloNX
//
// Created by Stossy11 on 21/12/2024.
//
import SwiftUI
struct PerformanceOverlayView: View {
@StateObject private var memorymonitor = MemoryUsageMonitor()
@StateObject private var fpsmonitor = FPSMonitor()
var body: some View {
VStack {
Text("\(fpsmonitor.formatFPS())")
.foregroundStyle(.white)
.stroke(color: .black, width: 2)
Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage))
.foregroundStyle(.white)
.stroke(color: .black, width: 2)
}
}
}
extension View {
func stroke(color: Color, width: CGFloat = 1) -> some View {
modifier(StrokeModifier(strokeSize: width, strokeColor: color))
}
}
struct StrokeModifier: ViewModifier {
private let id = UUID()
var strokeSize: CGFloat = 1
var strokeColor: Color = .blue
func body(content: Content) -> some View {
if strokeSize > 0 {
appliedStrokeBackground(content: content)
} else {
content
}
}
private func appliedStrokeBackground(content: Content) -> some View {
content
.padding(strokeSize*2)
.background(
Rectangle()
.foregroundColor(strokeColor)
.mask(alignment: .center) {
mask(content: content)
}
)
}
func mask(content: Content) -> some View {
Canvas { context, size in
context.addFilter(.alphaThreshold(min: 0.01))
if let resolvedView = context.resolveSymbol(id: id) {
context.draw(resolvedView, at: .init(x: size.width/2, y: size.height/2))
}
} symbols: {
content
.tag(id)
.blur(radius: strokeSize)
}
}
}

View file

@ -9,10 +9,10 @@ import MetalKit
import UIKit
class MeloMTKView: MTKView {
private var activeTouches: [UITouch] = []
private var ignoredTouches: Set<UITouch> = []
private var touchIndexMap: [UITouch: Int] = [:]
private let baseWidth: CGFloat = 1280
private let baseHeight: CGFloat = 720
private var aspectRatio: AspectRatio = .fixed16x9
@ -84,71 +84,112 @@ class MeloMTKView: MTKView {
return CGPoint(x: scaledX, y: scaledY)
}
private func getNextAvailableIndex() -> Int {
for i in 0..<Int.max {
if !touchIndexMap.values.contains(i) {
return i
}
}
return 0
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
guard !disabled else { return }
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
for touch in touches {
let location = touch.location(in: self)
if scaleToTargetResolution(location) == nil {
guard let scaledLocation = scaleToTargetResolution(location) else {
ignoredTouches.insert(touch)
continue
}
let index = getNextAvailableIndex()
touchIndexMap[touch] = index
activeTouches.append(touch)
let index = activeTouches.firstIndex(of: touch)!
let scaledLocation = scaleToTargetResolution(location)!
print("Touch began at: \(scaledLocation) and \(self.aspectRatio)")
touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
for touch in touches {
if ignoredTouches.contains(touch) {
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
guard !disabled else {
for touch in touches {
ignoredTouches.remove(touch)
if let index = activeTouches.firstIndex(of: touch) {
activeTouches.remove(at: index)
}
touchIndexMap.removeValue(forKey: touch)
}
return
}
for touch in touches {
if ignoredTouches.remove(touch) != nil {
continue
}
if let index = activeTouches.firstIndex(of: touch) {
activeTouches.remove(at: index)
if let touchIndex = touchIndexMap[touch] {
touch_ended(Int32(touchIndex))
print("Touch ended for index \(index)")
touch_ended(Int32(index))
if let arrayIndex = activeTouches.firstIndex(of: touch) {
activeTouches.remove(at: arrayIndex)
}
touchIndexMap.removeValue(forKey: touch)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
guard !disabled else { return }
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
for touch in touches {
if ignoredTouches.contains(touch) {
continue
}
let location = touch.location(in: self)
guard let scaledLocation = scaleToTargetResolution(location) else {
if let index = activeTouches.firstIndex(of: touch) {
activeTouches.remove(at: index)
print("Touch left active area, removed index \(index)")
touch_ended(Int32(index))
}
guard let touchIndex = touchIndexMap[touch] else {
continue
}
if let index = activeTouches.firstIndex(of: touch) {
print("Touch moved to: \(scaledLocation)")
touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
let location = touch.location(in: self)
guard let scaledLocation = scaleToTargetResolution(location) else {
touch_ended(Int32(touchIndex))
if let arrayIndex = activeTouches.firstIndex(of: touch) {
activeTouches.remove(at: arrayIndex)
}
touchIndexMap.removeValue(forKey: touch)
ignoredTouches.insert(touch)
continue
}
touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(touchIndex))
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
touchesEnded(touches, with: event)
}
func resetTouchTracking() {
activeTouches.removeAll()
ignoredTouches.removeAll()
touchIndexMap.removeAll()
}
}

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 UIKit
import MetalKit
import CoreLocation
struct MoltenVKSettings: Codable, Hashable {
let string: String
@ -31,19 +32,25 @@ struct ContentView: View {
@AppStorage("isVirtualController") var isVCA: Bool = true
// Settings and Configuration
@State private var config: Ryujinx.Configuration
private var config: Ryujinx.Arguments {
settingsManager.config
}
@StateObject private var settingsManager = SettingsManager.shared
@State var settings: [MoltenVKSettings]
@AppStorage("useTrollStore") var useTrollStore: Bool = false
// JIT
@AppStorage("useTrollStore") var useTrollStore: Bool = false
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
@AppStorage("stikJIT") var stikJIT: Bool = false
// Other Configuration
@State var isMK8: Bool = false
@AppStorage("quit") var quit: Bool = false
@State var quits: Bool = false
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
// Loading Animation
@ -53,6 +60,9 @@ struct ContentView: View {
private let animationDuration: Double = 1.0
@State private var isAnimating = false
@State var isLoading = true
// MARK: - CORE
@StateObject var ryujinx = Ryujinx.shared
// MARK: - SDL
@ -60,14 +70,6 @@ struct ContentView: View {
// MARK: - Initialization
init() {
var defaultConfig = loadSettings()
if defaultConfig == nil {
saveSettings(config: .init(gamepath: ""))
defaultConfig = loadSettings()
}
_config = State(initialValue: defaultConfig!)
let defaultSettings: [MoltenVKSettings] = [
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
@ -79,8 +81,6 @@ struct ContentView: View {
_settings = State(initialValue: defaultSettings)
print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
initializeSDL()
}
@ -119,7 +119,7 @@ struct ContentView: View {
private var jitErrorView: some View {
Text("")
.sheet(isPresented:Binding(
.fullScreenCover(isPresented:Binding(
get: { !ryujinx.jitenabled },
set: { newValue in
ryujinx.jitenabled = newValue
@ -130,14 +130,12 @@ struct ContentView: View {
JITPopover() {
ryujinx.jitenabled = false
}
.interactiveDismissDisabled()
}
}
private var mainMenuView: some View {
MainTabView(
startemu: $game,
config: $config,
MVKconfig: $settings,
controllersList: $controllersList,
currentControllers: $currentControllers,
@ -152,24 +150,18 @@ struct ContentView: View {
refreshControllersList()
}
print(MTLHud.shared.isEnabled)
UserDefaults.standard.set(false, forKey: "lockInApp")
initControllerObservers()
Air.play(AnyView(
VStack {
Image(systemName: "gamecontroller")
.font(.system(size: 300))
.foregroundColor(.gray)
.padding(.bottom, 10)
Text("Select Game")
.font(.system(size: 150))
.bold()
}
ControllerListView(game: $game)
))
refreshControllersList()
ryujinx.addGames()
checkJitStatus()
}
.onOpenURL { url in
@ -288,7 +280,6 @@ struct ContentView: View {
queue: .main
) { notification in
if let controller = notification.object as? GCController {
print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller)
refreshControllersList()
}
@ -300,7 +291,8 @@ struct ContentView: View {
queue: .main
) { notification in
if let controller = notification.object as? GCController {
print("Controller disconnected: \(controller.productCategory)")
currentControllers = []
controllersList = []
nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil
refreshControllersList()
@ -317,6 +309,9 @@ struct ContentView: View {
}
private func refreshControllersList() {
currentControllers = []
controllersList = []
controllersList = ryujinx.getConnectedControllers()
if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
@ -325,8 +320,6 @@ struct ContentView: View {
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
currentControllers = []
if controllersList.count == 1 {
currentControllers.append(controllersList[0])
@ -339,22 +332,54 @@ struct ContentView: View {
}
}
private func registerMotionForMatchingControllers() {
// Loop through currentControllers with index
for (index, controller) in currentControllers.enumerated() {
let slot = UInt8(index)
// Check native controllers
for (_, nativeController) in nativeControllers where nativeController.controllername == String("GC - \(controller.name)") && nativeController.tryGetMotionProvider() == nil {
nativeController.tryRegisterMotion(slot: slot)
continue
}
// Check virtual controller if active
if Ryujinx.shared.virtualController.controllername == controller.name && Ryujinx.shared.virtualController.tryGetMotionProvider() == nil {
Ryujinx.shared.virtualController.tryRegisterMotion(slot: slot)
continue
}
}
}
@StateObject private var persettings = PerGameSettingsManager.shared
private func start(displayid: UInt32) {
guard let game else { return }
var config = self.config
persettings.loadSettings()
if let customgame = persettings.config[game.titleId] {
config = customgame
}
config.gamepath = game.fileURL.path
config.inputids = Array(Set(currentControllers.map(\.id)))
configureEnvironmentVariables()
if config.inputids.isEmpty {
config.inputids.append("0")
registerMotionForMatchingControllers()
config.inputids.isEmpty ? config.inputids.append("0") : ()
// Local DSU loopback to ryujinx per input id
for _ in config.inputids {
config.inputDSUServers.append("127.0.0.1:26760")
}
do {
try ryujinx.start(with: config)
} catch {
print("Error: \(error.localizedDescription)")
// print("Error: \(error.localizedDescription)")
}
}
@ -365,7 +390,7 @@ struct ContentView: View {
}
if syncqsubmits {
setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "2", 1)
setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "1", 1)
}
}
@ -377,13 +402,25 @@ struct ContentView: View {
private func checkJitStatus() {
ryujinx.ryuIsJITEnabled()
if jitStreamerEB {
jitStreamerEB = false // byee jitstreamer eb
}
if !ryujinx.jitenabled {
if useTrollStore {
askForJIT()
} else if stikJIT {
enableJITStik()
} else if jitStreamerEB {
enableJITEB()
} else {
print("no JIT")
if !allocateTest(), checkDebugged() {
loop_heartbeat()
sleep(5)
let cool = String(cString: attach(getpid())!)
print(cool)
}
}
}
}
@ -391,6 +428,9 @@ struct ContentView: View {
private func handleDeepLink(_ url: URL) {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" {
refreshControllersList()
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = ryujinx.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
@ -407,3 +447,135 @@ extension Array {
}
}
}
class LocationManager: NSObject, CLLocationManagerDelegate {
private var locationManager: CLLocationManager
static let sharedInstance = LocationManager()
private override init() {
locationManager = CLLocationManager()
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.pausesLocationUpdatesAutomatically = false
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// print("wow")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location manager failed with: \(error)")
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if manager.authorizationStatus == .denied {
print("Location services are disabled in settings.")
} else {
startUpdatingLocation()
}
}
func stop() {
if UserDefaults.standard.bool(forKey: "location-enabled") {
locationManager.stopUpdatingLocation()
}
}
func startUpdatingLocation() {
if UserDefaults.standard.bool(forKey: "location-enabled") {
locationManager.requestAlwaysAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
}
}
}
struct ControllerListView: View {
@State private var selectedIndex = 0
@Binding var game: Game?
@ObservedObject private var ryujinx = Ryujinx.shared
var body: some View {
List(ryujinx.games.indices, id: \.self) { index in
let game = ryujinx.games[index]
HStack(spacing: 16) {
// Game Icon
Group {
if let icon = game.icon {
Image(uiImage: icon)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
ZStack {
RoundedRectangle(cornerRadius: 10)
Image(systemName: "gamecontroller.fill")
.font(.system(size: 24))
.foregroundColor(.gray)
}
}
}
.frame(width: 55, height: 55)
.cornerRadius(10)
// Game Info
VStack(alignment: .leading, spacing: 4) {
Text(game.titleName)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.primary)
HStack(spacing: 4) {
Text(game.developer)
if !game.version.isEmpty && game.version != "0" {
Text("")
Text("v\(game.version)")
}
}
.font(.system(size: 14))
.foregroundColor(.secondary)
}
Spacer()
}
.background(selectedIndex == index ? Color.blue.opacity(0.3) : .clear)
}
.onAppear(perform: setupControllerObservers)
}
private func setupControllerObservers() {
let dpadHandler: GCControllerDirectionPadValueChangedHandler = { _, _, yValue in
if yValue == 1.0 {
selectedIndex = max(0, selectedIndex - 1)
} else if yValue == -1.0 {
selectedIndex = min(ryujinx.games.count - 1, selectedIndex + 1)
}
}
for controller in GCController.controllers() {
print("Controller connected: \(controller.vendorName ?? "Unknown")")
controller.playerIndex = .index1
controller.microGamepad?.dpad.valueChangedHandler = dpadHandler
controller.extendedGamepad?.dpad.valueChangedHandler = dpadHandler
controller.extendedGamepad?.buttonA.pressedChangedHandler = { _, _, pressed in
if pressed {
print("A button pressed")
game = ryujinx.games[selectedIndex]
}
}
}
NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main
) { _ in
setupControllerObservers()
}
}
}

View file

@ -0,0 +1,44 @@
//
// GameRequirementsCache.swift
// MeloNX
//
// Created by Stossy11 on 21/03/2025.
//
import Foundation
class GameCompatibiliryCache {
static let shared = GameCompatibiliryCache()
private let cacheKey = "gameRequirementsCache"
private let timestampKey = "gameRequirementsCacheTimestamp"
private let cacheDuration: TimeInterval = Double.random(in: 3...5) * 24 * 60 * 60 // Randomly pick 3-5 days
func getCachedData() -> [GameRequirements]? {
guard let cachedData = UserDefaults.standard.data(forKey: cacheKey),
let timestamp = UserDefaults.standard.object(forKey: timestampKey) as? Date else {
return nil
}
let timeElapsed = Date().timeIntervalSince(timestamp)
if timeElapsed > cacheDuration {
clearCache()
return nil
}
return try? JSONDecoder().decode([GameRequirements].self, from: cachedData)
}
func setCachedData(_ data: [GameRequirements]) {
if let encodedData = try? JSONEncoder().encode(data) {
UserDefaults.standard.set(encodedData, forKey: cacheKey)
UserDefaults.standard.set(Date(), forKey: timestampKey)
}
}
func clearCache() {
UserDefaults.standard.removeObject(forKey: cacheKey)
UserDefaults.standard.removeObject(forKey: timestampKey)
}
}

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -0,0 +1,66 @@
//
// LogEntry.swift
// MeloNX
//
// Created by Stossy11 on 09/02/2025.
//
import SwiftUI
import Combine
struct LogFileView: View {
@StateObject var logsModel = LogViewModel()
@State private var showingLogs = false
public var isfps: Bool
private let fileManager = FileManager.default
private let maxDisplayLines = 4
var body: some View {
VStack(alignment: .leading, spacing: 4) {
ForEach(logsModel.logs.suffix(maxDisplayLines), id: \.self) { log in
Text(log)
.font(.caption)
.foregroundColor(.white)
.padding(4)
.background(Color.black.opacity(0.7))
.cornerRadius(4)
.transition(.opacity)
}
}
.padding()
}
private func stopLogFileWatching() {
showingLogs = false
}
}
class LogViewModel: ObservableObject {
@Published var logs: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
_ = LogCapture.shared
NotificationCenter.default.publisher(for: .newLogCaptured)
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.updateLogs()
}
.store(in: &cancellables)
updateLogs()
}
func updateLogs() {
logs = LogCapture.shared.capturedLogs
}
func clearLogs() {
LogCapture.shared.capturedLogs = []
updateLogs()
}
}

View file

@ -0,0 +1,236 @@
//
// AppIconSwitcher.swift
// MeloNX
//
// Created by Stossy11 on 02/06/2025.
//
import SwiftUI
struct AppIcon: Identifiable, Equatable {
var id: String { creator }
var iconNames: [String: String]
var creator: String
}
struct AppIconSwitcherView: View {
@Environment(\.dismiss) private var dismiss
@State var appIcons: [AppIcon] = [
AppIcon(iconNames: ["Default": UIImage.appIcon(), "Dark Mode": "DarkMode", "Round": "RoundAppIcon"], creator: "CycloKid"),
AppIcon(iconNames: ["Pixel Default": "PixelAppIcon", "Pixel Round": "PixelRoundAppIcon"], creator: "Nobody"),
AppIcon(iconNames: ["\"UwU\"": "uwuAppIcon"], creator: "𝒰𝓃𝓀𝓃𝑜𝓌𝓃")
]
@State var columns: [GridItem] = [
GridItem(.flexible(), spacing: 20),
GridItem(.flexible(), spacing: 20),
GridItem(.flexible(), spacing: 20)
]
@State private var currentIconName: String? = nil
@State var refresh = 0
var body: some View {
NavigationView {
ZStack {
LinearGradient(
gradient: Gradient(colors: [
Color(.systemBackground).opacity(0.95),
Color(.systemGroupedBackground)
]),
startPoint: .top,
endPoint: .bottom
)
.ignoresSafeArea()
ScrollView {
LazyVStack(spacing: 32) {
ForEach(appIcons.indices, id: \.self) { index in
let iconGroup = appIcons[index]
VStack(alignment: .leading, spacing: 20) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(iconGroup.creator)
.font(.title2)
.fontWeight(.bold)
.foregroundStyle(.primary)
Text("\(iconGroup.iconNames.count) icons")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.horizontal, 24)
LazyVGrid(columns: columns, spacing: 20) {
ForEach(Array(iconGroup.iconNames.keys.sorted()), id: \.self) { key in
if let iconName = iconGroup.iconNames[key] {
Button {
selectIcon(iconName)
} label: {
ZStack {
AppIconView(app: (iconName, key))
if iconName == currentIconName ?? UIImage.appIcon() {
VStack {
HStack {
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 24, weight: .bold))
.foregroundStyle(.white)
.background(
Circle()
.fill(
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 28, height: 28)
)
}
Spacer()
}
.frame(width: 80, height: 80)
.offset(x: 6, y: -6)
}
}
}
.buttonStyle(PlainButtonStyle())
.scaleEffect(isCurrentIcon(iconName) ? 0.95 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isCurrentIcon(iconName))
}
}
}
.padding(.horizontal, 24)
}
// Stylized divider
if index < appIcons.count - 1 {
Rectangle()
.fill(
LinearGradient(
colors: [.clear, Color(.separator), .clear],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(height: 1)
.padding(.horizontal, 40)
}
}
}
.padding(.vertical, 32)
}
}
.navigationTitle("Choose App Icon")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(.blue)
}
}
}
.onAppear(perform: setupColumns)
.onAppear(perform: getCurrentIconName)
}
private func setupColumns() {
if #available(iOS 18.5, *) {
//
} else {
if checkforOld() {
if let value = appIcons[0].iconNames.removeValue(forKey: "Round") {
appIcons[0].iconNames["PomeloNX"] = value
}
if let value = appIcons[1].iconNames.removeValue(forKey: "Pixel Round") {
appIcons[1].iconNames["Pixel PomeloNX"] = value
}
}
}
}
private func getCurrentIconName() {
currentIconName = UIApplication.shared.alternateIconName ?? UIImage.appIcon()
}
private func isCurrentIcon(_ iconName: String) -> Bool {
let currentIcon = UIApplication.shared.alternateIconName ?? UIImage.appIcon()
return currentIcon == iconName
}
private func selectIcon(_ iconName: String) {
// Haptic feedback
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
if iconName == UIImage.appIcon() {
UIApplication.shared.setAlternateIconName(nil) { error in
if let error = error {
print("Error setting icon: \(error)")
} else {
DispatchQueue.main.async {
currentIconName = nil
refresh = Int.random(in: 0...100)
}
}
}
} else {
UIApplication.shared.setAlternateIconName(iconName) { error in
if let error = error {
print("Error setting icon: \(error)")
} else {
DispatchQueue.main.async {
currentIconName = iconName
refresh = Int.random(in: 0...100)
}
}
}
}
}
}
struct AppIconView: View {
let app: (String, String)
var body: some View {
VStack(spacing: 7) {
ZStack {
Image(uiImage: UIImage(named: app.0)!)
.resizable()
.cornerRadius(15)
.frame(width: 62, height: 62)
.shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
}
Text(app.1)
.font(.system(size: 12, weight: .medium))
.foregroundColor(.white)
.multilineTextAlignment(.center)
.shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
.frame(width: 100)
.lineLimit(1)
}
}
}
extension UIImage {
static func appIcon() -> String {
if let icons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any],
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
let lastIcon = iconFiles.last {
return lastIcon
}
return "AppIcon"
}
}

View file

@ -0,0 +1,707 @@
//
// PerGameSettingsView.swift
// MeloNX
//
// Created by Stossy11 on 12/06/2025.
//
import SwiftUI
protocol PerGameSettingsManaging: ObservableObject {
var config: [String: Ryujinx.Arguments] { get set }
func debouncedSave()
func saveSettings()
func loadSettings()
static func loadSettings() -> [String: Ryujinx.Arguments]?
}
class PerGameSettingsManager: PerGameSettingsManaging {
@Published var config: [String: Ryujinx.Arguments] {
didSet {
debouncedSave()
}
}
private var saveWorkItem: DispatchWorkItem?
public static var shared = PerGameSettingsManager()
private init() {
self.config = PerGameSettingsManager.loadSettings() ?? [:]
}
func debouncedSave() {
saveWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
self.saveSettings()
}
saveWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
}
func saveSettings() {
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(config)
let fileURL = URL.documentsDirectory.appendingPathComponent("config-pergame.json")
try data.write(to: fileURL)
print("Settings saved successfully")
} catch {
print("Failed to save settings: \(error)")
}
}
static func loadSettings() -> [String: Ryujinx.Arguments]? {
do {
let fileURL = URL.documentsDirectory.appendingPathComponent("config-pergame.json")
guard FileManager.default.fileExists(atPath: fileURL.path) else {
print("Config file does not exist, creating new config")
return nil
}
let data = try Data(contentsOf: fileURL)
let decoder = JSONDecoder()
let configs = try decoder.decode([String: Ryujinx.Arguments].self, from: data)
return configs
} catch {
print("Failed to load settings: \(error)")
return nil
}
}
func loadSettings() {
self.config = PerGameSettingsManager.loadSettings() ?? [:]
}
}
struct PerGameSettingsView: View {
@StateObject private var settingsManager: PerGameSettingsManager
var titleId: String
init(titleId: String, manager: any PerGameSettingsManaging = PerGameSettingsManager.shared) {
self._settingsManager = StateObject(wrappedValue: manager as! PerGameSettingsManager)
self.titleId = titleId
}
private var config: Binding<Ryujinx.Arguments> {
return Binding<Ryujinx.Arguments> {
return settingsManager.config[titleId] ?? Ryujinx.Arguments()
} set: { newValue in
settingsManager.config[titleId] = newValue
settingsManager.debouncedSave()
}
}
var memoryManagerModes = [
("HostMapped", "Host (fast)"),
("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
("SoftwarePageTable", "Software (slow)"),
]
let totalMemory = ProcessInfo.processInfo.physicalMemory
@State private var showResolutionInfo = false
@State private var showAnisotropicInfo = false
@State private var showControllerInfo = false
@State private var showAppIconSwitcher = false
@State private var searchText = ""
@StateObject var ryujinx = Ryujinx.shared
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
@State private var selectedCategory: PerSettingsCategory = .graphics
@StateObject var metalHudEnabler = MTLHud.shared
var filteredMemoryModes: [(String, String)] {
guard !searchText.isEmpty else { return memoryManagerModes }
return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
}
var appVersion: String {
guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
return "Unknown"
}
return version
}
@FocusState private var isArgumentsKeyboardVisible: Bool
@State private var selectedView = "Data Management"
@State private var sidebar = true
enum PerSettingsCategory: String, CaseIterable, Identifiable {
case graphics = "Graphics"
case system = "System"
case advanced = "Advanced"
var id: String { self.rawValue }
var icon: String {
switch self {
case .graphics: return "paintbrush.fill"
case .system: return "gearshape.fill"
case .advanced: return "terminal.fill"
}
}
}
var body: some View {
iOSNav {
ZStack {
Color(UIColor.systemBackground)
.ignoresSafeArea()
VStack(spacing: 0) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(PerSettingsCategory.allCases, id: \.id) { category in
CategoryButton(
title: category.rawValue,
icon: category.icon,
isSelected: selectedCategory == category
) {
selectedCategory = category
}
}
}
.padding(.horizontal)
.padding(.vertical, 8)
}
Divider()
// Settings content
ScrollView {
VStack(spacing: 24) {
switch selectedCategory {
case .graphics:
graphicsSettings
.padding(.top)
case .system:
systemSettings
.padding(.top)
case .advanced:
advancedSettings
.padding(.top)
}
Spacer(minLength: 50)
}
.padding(.bottom)
}
.scrollDismissesKeyboardIfAvailable()
}
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
settingsManager.debouncedSave()
dismiss()
}
}
ToolbarItem(placement: .cancellationAction) {
Button("Reset") {
dismiss()
settingsManager.config[titleId] = nil
settingsManager.saveSettings()
}
}
}
// .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
.onAppear {
// if let configs = SettingsManager.loadSettings() {
// settingsManager.loadSettings()
// } else {
// settingsManager.saveSettings()
//}
print(titleId)
if settingsManager.config[titleId] == nil {
settingsManager.config[titleId] = Ryujinx.Arguments()
settingsManager.debouncedSave()
}
}
}
}
// MARK: - Graphics Settings
private var graphicsSettings: some View {
SettingsSection(title: "Graphics & Performance") {
// Resolution scale card
SettingsCard {
VStack(alignment: .leading, spacing: 12) {
HStack {
labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
.font(.headline)
Spacer()
Button {
showResolutionInfo.toggle()
} label: {
Image(systemName: "info.circle")
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
.alert(isPresented: $showResolutionInfo) {
Alert(
title: Text("Resolution Scale"),
message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
dismissButton: .default(Text("OK"))
)
}
}
VStack(spacing: 8) {
Slider(value: config.resscale, in: 0.1...3.0, step: 0.05)
HStack {
Text("0.1x")
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
Text("\(config.resscale.wrappedValue, specifier: "%.2f")x")
.font(.headline)
.foregroundColor(.blue)
Spacer()
Text("3.0x")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
}
// Anisotropic filtering card
SettingsCard {
VStack(alignment: .leading, spacing: 12) {
HStack {
labelWithIcon("Max Anisotropic Filtering", iconName: "magnifyingglass")
.font(.headline)
Spacer()
Button {
showAnisotropicInfo.toggle()
} label: {
Image(systemName: "info.circle")
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
.alert(isPresented: $showAnisotropicInfo) {
Alert(
title: Text("Max Anisotropic Filtering"),
message: Text("Adjust the internal Anisotropic filtering. Higher values improve texture quality at angles but may reduce performance. Default at 0 lets game decide."),
dismissButton: .default(Text("OK"))
)
}
}
VStack(spacing: 8) {
Slider(value: config.maxAnisotropy, in: 0...16.0, step: 0.1)
HStack {
Text("Off")
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
Text("\(config.maxAnisotropy.wrappedValue, specifier: "%.1f")x")
.font(.headline)
.foregroundColor(.blue)
Spacer()
Text("16x")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
}
// Toggle options card
SettingsCard {
VStack(spacing: 4) {
PerSettingsToggle(isOn: config.disableShaderCache, icon: "memorychip", label: "Shader Cache")
Divider()
PerSettingsToggle(isOn: config.disablevsync, icon: "arrow.triangle.2.circlepath", label: "Disable VSync")
Divider()
PerSettingsToggle(isOn: config.enableTextureRecompression, icon: "rectangle.compress.vertical", label: "Texture Recompression")
Divider()
PerSettingsToggle(isOn: config.disableDockedMode, icon: "dock.rectangle", label: "Docked Mode")
Divider()
PerSettingsToggle(isOn: config.macroHLE, icon: "gearshape", label: "Macro HLE")
}
}
// Aspect ratio card
SettingsCard {
VStack(alignment: .leading, spacing: 12) {
labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
.font(.headline)
if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) {
Picker(selection: config.aspectRatio) {
ForEach(AspectRatio.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
EmptyView()
}
.pickerStyle(.segmented)
} else {
Picker(selection: config.aspectRatio) {
ForEach(AspectRatio.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
EmptyView()
}
.pickerStyle(.menu)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
}
}
}
}
}
// MARK: - System Settings
private var systemSettings: some View {
SettingsSection(title: "System Configuration") {
// Language and region card
SettingsCard {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
labelWithIcon("System Language", iconName: "character.bubble")
.font(.headline)
Picker(selection: config.language) {
ForEach(SystemLanguage.allCases, id: \.self) { language in
Text(language.displayName).tag(language)
}
} label: {
EmptyView()
}
.pickerStyle(.menu)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
}
Divider()
VStack(alignment: .leading, spacing: 8) {
labelWithIcon("Region", iconName: "globe")
.font(.headline)
Picker(selection: config.regioncode) {
ForEach(SystemRegionCode.allCases, id: \.self) { region in
Text(region.displayName).tag(region)
}
} label: {
EmptyView()
}
.pickerStyle(.menu)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
}
}
}
// CPU options card
SettingsCard {
VStack(alignment: .leading, spacing: 16) {
Text("CPU Configuration")
.font(.headline)
.foregroundColor(.primary)
VStack(alignment: .leading, spacing: 8) {
Text("Memory Manager Mode")
.font(.subheadline)
.foregroundColor(.secondary)
Picker(selection: config.memoryManagerMode) {
ForEach(filteredMemoryModes, id: \.0) { key, displayName in
Text(displayName).tag(key)
}
} label: {
EmptyView()
}
.pickerStyle(.segmented)
}
Divider()
PerSettingsToggle(isOn: config.disablePTC, icon: "cpu", label: "Disable PTC")
if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
Divider()
if #available(iOS 16.4, *) {
PerSettingsToggle(isOn: .constant(false), icon: "bolt", label: "Hypervisor")
.disabled(true)
} else if checkAppEntitlement("com.apple.private.hypervisor") {
PerSettingsToggle(isOn: config.hypervisor, icon: "bolt", label: "Hypervisor")
}
}
}
}
// Controller options card
SettingsCard {
VStack(alignment: .leading, spacing: 16) {
Text("Controller Configuration")
.font(.headline)
.foregroundColor(.primary)
PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
}
}
}
}
// MARK: - Advanced Settings
private var advancedSettings: some View {
SettingsSection(title: "Advanced Options") {
// Debug options card
SettingsCard {
VStack(spacing: 4) {
PerSettingsToggle(isOn: config.debuglogs, icon: "exclamationmark.bubble", label: "Debug Logs")
Divider()
PerSettingsToggle(isOn: config.tracelogs, icon: "waveform.path", label: "Trace Logs")
}
}
// Advanced toggles card
SettingsCard {
VStack(spacing: 4) {
PerSettingsToggle(isOn: config.dfsIntegrityChecks, icon: "checkmark.shield", label: "Disable FS Integrity Checks")
.accentColor(.red)
Divider()
PerSettingsToggle(isOn: config.expandRam, icon: "exclamationmark.bubble", label: "Expand Guest RAM")
.accentColor(.red)
.disabled(totalMemory < 5723)
Divider()
PerSettingsToggle(isOn: config.ignoreMissingServices, icon: "waveform.path", label: "Ignore Missing Services")
.accentColor(.red)
}
}
// Additional args card
SettingsCard {
VStack(alignment: .leading, spacing: 12) {
Text("Additional Arguments")
.font(.headline)
.foregroundColor(.primary)
let binding = Binding(
get: {
config.additionalArgs.wrappedValue.joined(separator: ", ")
},
set: { newValue in
let args = newValue
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
config.additionalArgs.wrappedValue = args
}
)
if #available(iOS 15.0, *) {
TextField("Separate arguments with commas", text: binding)
.font(.system(.body, design: .monospaced))
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.none)
.disableAutocorrection(true)
.padding(.vertical, 4)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Dismiss") {
isArgumentsKeyboardVisible = false
}
}
}
.focused($isArgumentsKeyboardVisible)
} else {
TextField("Separate arguments with commas", text: binding)
.font(.system(.body, design: .monospaced))
.textFieldStyle(.roundedBorder)
.disableAutocorrection(true)
.padding(.vertical, 4)
}
}
}
// Page size info card
SettingsCard {
HStack {
labelWithIcon("Page Size", iconName: "textformat.size")
Spacer()
Text("\(String(Int(getpagesize())))")
.font(.system(.body, design: .monospaced))
.foregroundColor(.secondary)
}
}
}
}
// MARK: - Miscellaneous Settings
private var miscSettings: some View {
SettingsSection(title: "Miscellaneous Options") {
SettingsCard {
VStack(spacing: 4) {
PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
}
}
}
}
// MARK: - Helper Functions
func getGPUInfo() -> String? {
let device = MTLCreateSystemDefaultDevice()
return device?.name
}
@ViewBuilder
private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
HStack(spacing: 8) {
if iconName.hasSuffix(".svg") {
if let flipimage, flipimage {
SVGView(svgName: iconName, color: .blue)
// .symbolRenderingMode(.hierarchical)
.frame(width: 20, height: 20)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
} else {
SVGView(svgName: iconName, color: .blue)
// .symbolRenderingMode(.hierarchical)
.frame(width: 20, height: 20)
}
} else if !iconName.isEmpty {
Image(systemName: iconName)
// .symbolRenderingMode(.hierarchical)
.foregroundColor(.blue)
}
Text(text)
}
.font(.body)
}
}
// MARK: - Supporting Views
// PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
struct PerSettingsCard<Content: View>: View {
@Environment(\.colorScheme) var colorScheme
@AppStorage("oldSettingsUI") var oldSettingsUI = false
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ? Color(.systemGray6) : Color.white)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
)
.padding(.horizontal)
}
}
struct PerSettingsToggle: View {
@Binding var isOn: Bool
let icon: String
let label: String
var disabled: Bool = false
@AppStorage("toggleGreen") var toggleGreen: Bool = false
@AppStorage("oldSettingsUI") var oldSettingsUI = false
var body: some View {
Toggle(isOn: $isOn) {
HStack(spacing: 8) {
if icon.hasSuffix(".svg") {
SVGView(svgName: icon, color: .blue)
.frame(width: 20, height: 20)
} else {
Image(systemName: icon)
// .symbolRenderingMode(.hierarchical)
.foregroundColor(.blue)
}
Text(label)
.font(.body)
}
}
.toggleStyle(SwitchToggleStyle(tint: .blue))
.disabled(disabled)
.padding(.vertical, 6)
}
func disabled(_ disabled: Bool) -> PerSettingsToggle {
var view = self
view.disabled = disabled
return view
}
func accentColor(_ color: Color) -> PerSettingsToggle {
var view = self
return view
}
}

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,6 @@ import UniformTypeIdentifiers
struct MainTabView: View {
@Binding var startemu: Game?
@Binding var config: Ryujinx.Configuration
@Binding var MVKconfig: [MoltenVKSettings]
@Binding var controllersList: [Controller]
@Binding var currentControllers: [Controller]
@ -25,7 +24,8 @@ struct MainTabView: View {
Label("Games", systemImage: "gamecontroller.fill")
}
SettingsView(config: $config, MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
// SettingsView(config: $config, MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
SettingsViewNew(MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
.tabItem {
Label("Settings", systemImage: "gear")
}

View file

@ -33,18 +33,32 @@ struct MeloNXUpdateSheet: View {
Spacer()
Button(action: {
if let url = URL(string: updateInfo.download_link) {
UIApplication.shared.open(url)
if #available(iOS 15.0, *) {
Button(action: {
if let url = URL(string: updateInfo.download_link) {
UIApplication.shared.open(url)
}
}) {
Text("Download Now")
.font(.title3)
.bold()
.frame(width: 300, height: 40)
}
}) {
Text("Download Now")
.font(.title3)
.bold()
.frame(width: 300, height: 40)
.buttonStyle(.borderedProminent)
.frame(alignment: .bottom)
} else {
Button(action: {
if let url = URL(string: updateInfo.download_link) {
UIApplication.shared.open(url)
}
}) {
Text("Download Now")
.font(.title3)
.bold()
.frame(width: 300, height: 40)
}
.frame(alignment: .bottom)
}
.buttonStyle(.borderedProminent)
.frame(alignment: .bottom)
}
.padding(.horizontal)
.navigationTitle("Version \(updateInfo.version_number) Available!")

View file

@ -46,7 +46,7 @@ struct DLCManagerSheet: View {
@Binding var game: Game!
@State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = []
@Environment(\.dismiss) private var dismiss
@Environment(\.presentationMode) var presentationMode
// MARK: - Body
var body: some View {
@ -66,7 +66,7 @@ struct DLCManagerSheet: View {
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
dismiss()
presentationMode.wrappedValue.dismiss()
}
}
@ -127,27 +127,56 @@ struct DLCManagerSheet: View {
private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
Button {
toggleDLC(dlc)
} label: {
HStack {
Text(dlc.filename)
.foregroundStyle(.primary)
Spacer()
Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
.foregroundStyle(dlc.isEnabled ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
removeDLC(at: IndexSet(integer: index))
Group {
if #available(iOS 15.0, *) {
Button {
toggleDLC(dlc)
} label: {
HStack {
Text(dlc.filename)
.foregroundColor(.primary)
Spacer()
Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
.foregroundColor(dlc.isEnabled ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
removeDLC(at: IndexSet(integer: index))
}
} label: {
Label("Delete", systemImage: "trash")
}
}
} else {
Button {
toggleDLC(dlc)
} label: {
HStack {
Text(dlc.filename)
.foregroundColor(.primary)
Spacer()
Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
.foregroundColor(dlc.isEnabled ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.contextMenu {
Button {
if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
removeDLC(at: IndexSet(integer: index))
}
} label: {
Label("Delete", systemImage: "trash")
.foregroundColor(.red)
}
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
@ -261,7 +290,7 @@ private extension DLCManagerSheet {
return result
} catch {
print("Error loading DLCs: \(error)")
// print("Error loading DLCs: \(error)")
return []
}
}
@ -300,7 +329,7 @@ extension Array where Element: AnyObject {
// MARK: - URL Extension
extension URL {
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
@available(iOS, introduced: 14.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
static var documentsDirectory: URL {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
return documentDirectory

View file

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

View file

@ -8,8 +8,15 @@
import SwiftUI
import UIKit
import CryptoKit
import UniformTypeIdentifiers
import AVFoundation
extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
}
}
@main
struct MeloNXApp: App {
@ -21,38 +28,73 @@ struct MeloNXApp: App {
@State var showOutOfDateSheet = false
@State var updateInfo: LatestVersionResponse? = nil
@StateObject var metalHudEnabler = MTLHud.shared
@State var finished = false
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
@AppStorage("location-enabled") var locationenabled: Bool = false
@AppStorage("checkForUpdate") var checkForUpdate: Bool = true
@AppStorage("runOnMainThread") var runOnMainThread = false
@AppStorage("autoJIT") var autoJIT = false
@State var fourgbiPad = false
@AppStorage("4GB iPad") var ignores = false
// String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
var body: some Scene {
WindowGroup {
if finishedStorage {
ContentView()
.onAppear {
checkLatestVersion()
}
.sheet(isPresented: Binding(
get: { showOutOfDateSheet && updateInfo != nil },
set: { newValue in
if !newValue {
showOutOfDateSheet = false
updateInfo = nil
Group {
if finishedStorage {
ContentView()
.withFileImporter()
.onAppear {
if checkForUpdate {
checkLatestVersion()
}
print(metalHudEnabler.canMetalHud)
UserDefaults.standard.set(false, forKey: "lockInApp")
}
.sheet(isPresented: Binding(
get: { showOutOfDateSheet && updateInfo != nil },
set: { newValue in
if !newValue {
showOutOfDateSheet = false
updateInfo = nil
}
}
)) {
if let updateInfo = updateInfo {
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
}
}
)) {
if let updateInfo = updateInfo {
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
}
}
} else {
SetupView(finished: $finished)
.onChange(of: finished) { newValue in
withAnimation {
withAnimation {
} else {
SetupView(finished: $finished)
.onChange(of: finished) { newValue in
withAnimation(.easeOut) {
finishedStorage = newValue
}
}
}
}
.onAppear() {
if UIDevice.current.userInterfaceIdiom == .pad && !ignores {
print((Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000))
if round(Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000) <= 4 {
fourgbiPad = true
}
}
}
.alert("Unsupported Device", isPresented: $fourgbiPad) {
Button("Continue") {
ignores = true
fourgbiPad = false
}
} message: {
Text("Your Device is an iPad with \(String(format: "%.0f GB", Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000)) of memory, MeloNX has issues with those devices")
}
}
}
@ -64,22 +106,22 @@ struct MeloNXApp: App {
#if DEBUG
let urlString = "http://192.168.178.116:8000/api/latest_release"
#else
let urlString = "https://melonx.org/api/latest_release"
let urlString = "https://melonx.net/api/latest_release"
#endif
guard let url = URL(string: urlString) else {
print("Invalid URL")
// print("Invalid URL")
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error checking for new version: \(error)")
// print("Error checking for new version: \(error)")
return
}
guard let data = data else {
print("No data received")
// print("No data received")
return
}
@ -94,10 +136,15 @@ struct MeloNXApp: App {
}
}
} catch {
print("Failed to decode response: \(error)")
// print("Failed to decode response: \(error)")
}
}
task.resume()
}
}
func changeAppUI(_ string: String) -> String? {
guard let data = Data(base64Encoded: string) else { return nil }
return String(data: data, encoding: .utf8)
}

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Before After
Before After

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "darker.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "darker.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "MeloNX 1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "MeloNX 1024.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "PixelPomeloNX 1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "PixelPomeloNX 1024.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "copycat.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "copycat.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "melowonx.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "melowonx.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View file

@ -10,7 +10,7 @@
</data>
<key>Info.plist</key>
<data>
UOH9NuuEcz5NQiQlrM2LNFaG2pI=
GYWZONTCP5su4yOAk0d5jCd2K88=
</data>
<key>Modules/module.modulemap</key>
<data>

View file

@ -0,0 +1,330 @@
#if 0
#elif defined(__arm64__) && __arm64__
// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
#ifndef STOSJIT_SWIFT_H
#define STOSJIT_SWIFT_H
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
#if !defined(__has_include)
# define __has_include(x) 0
#endif
#if !defined(__has_attribute)
# define __has_attribute(x) 0
#endif
#if !defined(__has_feature)
# define __has_feature(x) 0
#endif
#if !defined(__has_warning)
# define __has_warning(x) 0
#endif
#if __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
# ifndef __ptrauth_swift_value_witness_function_pointer
# define __ptrauth_swift_value_witness_function_pointer(x)
# endif
# ifndef __ptrauth_swift_class_method_pointer
# define __ptrauth_swift_class_method_pointer(x)
# endif
#pragma clang diagnostic pop
#endif
#pragma clang diagnostic pop
#endif
#if !defined(SWIFT_TYPEDEFS)
# define SWIFT_TYPEDEFS 1
# if __has_include(<uchar.h>)
# include <uchar.h>
# elif !defined(__cplusplus)
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
# endif
typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
#endif
#if !defined(SWIFT_PASTE)
# define SWIFT_PASTE_HELPER(x, y) x##y
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
#endif
#if !defined(SWIFT_METATYPE)
# define SWIFT_METATYPE(X) Class
#endif
#if !defined(SWIFT_CLASS_PROPERTY)
# if __has_feature(objc_class_property)
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
# else
# define SWIFT_CLASS_PROPERTY(...)
# endif
#endif
#if !defined(SWIFT_RUNTIME_NAME)
# if __has_attribute(objc_runtime_name)
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
# else
# define SWIFT_RUNTIME_NAME(X)
# endif
#endif
#if !defined(SWIFT_COMPILE_NAME)
# if __has_attribute(swift_name)
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
# else
# define SWIFT_COMPILE_NAME(X)
# endif
#endif
#if !defined(SWIFT_METHOD_FAMILY)
# if __has_attribute(objc_method_family)
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
# else
# define SWIFT_METHOD_FAMILY(X)
# endif
#endif
#if !defined(SWIFT_NOESCAPE)
# if __has_attribute(noescape)
# define SWIFT_NOESCAPE __attribute__((noescape))
# else
# define SWIFT_NOESCAPE
# endif
#endif
#if !defined(SWIFT_RELEASES_ARGUMENT)
# if __has_attribute(ns_consumed)
# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
# else
# define SWIFT_RELEASES_ARGUMENT
# endif
#endif
#if !defined(SWIFT_WARN_UNUSED_RESULT)
# if __has_attribute(warn_unused_result)
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
# else
# define SWIFT_WARN_UNUSED_RESULT
# endif
#endif
#if !defined(SWIFT_NORETURN)
# if __has_attribute(noreturn)
# define SWIFT_NORETURN __attribute__((noreturn))
# else
# define SWIFT_NORETURN
# endif
#endif
#if !defined(SWIFT_CLASS_EXTRA)
# define SWIFT_CLASS_EXTRA
#endif
#if !defined(SWIFT_PROTOCOL_EXTRA)
# define SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_ENUM_EXTRA)
# define SWIFT_ENUM_EXTRA
#endif
#if !defined(SWIFT_CLASS)
# if __has_attribute(objc_subclassing_restricted)
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# else
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# endif
#endif
#if !defined(SWIFT_RESILIENT_CLASS)
# if __has_attribute(objc_class_stub)
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
# else
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
# endif
#endif
#if !defined(SWIFT_PROTOCOL)
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_EXTENSION)
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
#endif
#if !defined(OBJC_DESIGNATED_INITIALIZER)
# if __has_attribute(objc_designated_initializer)
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
# else
# define OBJC_DESIGNATED_INITIALIZER
# endif
#endif
#if !defined(SWIFT_ENUM_ATTR)
# if __has_attribute(enum_extensibility)
# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
# else
# define SWIFT_ENUM_ATTR(_extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM)
# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# if __has_feature(generalized_swift_name)
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# else
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
#endif
#if !defined(SWIFT_UNAVAILABLE)
# define SWIFT_UNAVAILABLE __attribute__((unavailable))
#endif
#if !defined(SWIFT_UNAVAILABLE_MSG)
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
#endif
#if !defined(SWIFT_AVAILABILITY)
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
#endif
#if !defined(SWIFT_WEAK_IMPORT)
# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
#endif
#if !defined(SWIFT_DEPRECATED)
# define SWIFT_DEPRECATED __attribute__((deprecated))
#endif
#if !defined(SWIFT_DEPRECATED_MSG)
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
#endif
#if !defined(SWIFT_DEPRECATED_OBJC)
# if __has_feature(attribute_diagnose_if_objc)
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
# else
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
# endif
#endif
#if defined(__OBJC__)
#if !defined(IBSegueAction)
# define IBSegueAction
#endif
#endif
#if !defined(SWIFT_EXTERN)
# if defined(__cplusplus)
# define SWIFT_EXTERN extern "C"
# else
# define SWIFT_EXTERN extern
# endif
#endif
#if !defined(SWIFT_CALL)
# define SWIFT_CALL __attribute__((swiftcall))
#endif
#if !defined(SWIFT_INDIRECT_RESULT)
# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
#endif
#if !defined(SWIFT_CONTEXT)
# define SWIFT_CONTEXT __attribute__((swift_context))
#endif
#if !defined(SWIFT_ERROR_RESULT)
# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
#endif
#if defined(__cplusplus)
# define SWIFT_NOEXCEPT noexcept
#else
# define SWIFT_NOEXCEPT
#endif
#if !defined(SWIFT_C_INLINE_THUNK)
# if __has_attribute(always_inline)
# if __has_attribute(nodebug)
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
# else
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
# endif
# else
# define SWIFT_C_INLINE_THUNK inline
# endif
#endif
#if defined(_WIN32)
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
#endif
#else
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL
#endif
#endif
#if defined(__OBJC__)
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
#endif
#endif
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="StosJIT",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__OBJC__)
SWIFT_EXTERN char * _Nullable attach(int32_t pid) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;
SWIFT_EXTERN char * _Nullable debugattachanddetachApp(char * _Nonnull bundleId) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;
SWIFT_EXTERN void detach(void) SWIFT_NOEXCEPT;
SWIFT_EXTERN void loop_heartbeat(void) SWIFT_NOEXCEPT;
SWIFT_EXTERN BOOL writeZeroToMemory(uint64_t addr, int32_t length) SWIFT_NOEXCEPT SWIFT_WARN_UNUSED_RESULT;
#endif
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__cplusplus)
#endif
#pragma clang diagnostic pop
#endif
#else
#error unsupported Swift architecture
#endif

View file

@ -0,0 +1,19 @@
//
// StosJIT.h
// StosJIT
//
// Created by Stossy11 on 10/05/2025.
//
#import <Foundation/Foundation.h>
#import <StosJIT/idevice.h>
//! Project version number for StosJIT.
FOUNDATION_EXPORT double StosJITVersionNumber;
//! Project version string for StosJIT.
FOUNDATION_EXPORT const unsigned char StosJITVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <StosJIT/PublicHeader.h>

View file

@ -0,0 +1,205 @@
<?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>files</key>
<dict>
<key>.DS_Store</key>
<data>
7Mfr8shT4pXWBr/plN+uNkIabdM=
</data>
<key>Headers/StosJIT-Swift.h</key>
<data>
h9vaTwhC6FlnyKmIkaxLQGlFd1g=
</data>
<key>Headers/StosJIT.h</key>
<data>
ggHr5wlLNIIPydwUL9Vxm6abxjo=
</data>
<key>Headers/idevice.h</key>
<data>
mHDz7368FsBID56/epJ2NgIkha4=
</data>
<key>Headers/plist.h</key>
<data>
bL/f0MQDpLfvIcI1zxPwMuJ/PfI=
</data>
<key>Info.plist</key>
<data>
ZTTwPKlta/gjXAr1HIHmyAxeU4E=
</data>
<key>Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo</key>
<data>
nihJghwM5m7kxkQD7UvrWyHkLy8=
</data>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key>
<data>
gcwBsH4BgyFY4sVtNt+/xOKS3vY=
</data>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftdoc</key>
<data>
YPtkDrAuBiPPEp4ZdRdBVlFXnRM=
</data>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftmodule</key>
<data>
9cIInnjJzJFtY+CZm2iNo5qL3MQ=
</data>
<key>Modules/module.modulemap</key>
<data>
cnpvYzvLIwWcxkQodj5uLbHkyRk=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/StosJIT-Swift.h</key>
<dict>
<key>hash2</key>
<data>
1obIr4IjMvtcyNyYIV/Nh/5wahcA1cFjc4n4XVlNt2I=
</data>
</dict>
<key>Headers/StosJIT.h</key>
<dict>
<key>hash2</key>
<data>
yY9KyrRdOYRdlb7G6wVMU2hogasXMjwV5r8jUIk44ok=
</data>
</dict>
<key>Headers/idevice.h</key>
<dict>
<key>hash2</key>
<data>
zR9/TB9Dnv3uRC8qqGvaQ6c2yyOFUURmrHKLdEiUh/g=
</data>
</dict>
<key>Headers/plist.h</key>
<dict>
<key>hash2</key>
<data>
yFbGsiXBBp91tfsSFtS0Utt2Gpc3MEDFiMVXKG9q1rs=
</data>
</dict>
<key>Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo</key>
<dict>
<key>hash2</key>
<data>
+Ehvco7cQbAaF7zufvBYTiGXFp37Hjym/Pav514sGPk=
</data>
</dict>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key>
<dict>
<key>hash2</key>
<data>
Qnesa0n4URGWAopawg9bGx36dUwkYV00BoCJ8LFzlyg=
</data>
</dict>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftdoc</key>
<dict>
<key>hash2</key>
<data>
k7F2Xs2hh9iMbK8IE8TMtN6gjQ9kWs30NUKHeupq6VE=
</data>
</dict>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.swiftmodule</key>
<dict>
<key>hash2</key>
<data>
gMDYNHcBPCNwZw2A5mEUiCyYAS9VhtQG0z+/WqAUrOQ=
</data>
</dict>
<key>Modules/module.modulemap</key>
<dict>
<key>hash2</key>
<data>
FGwGKs5SNvpCyiIWiOP4eml9m2e3KISmtCJVtNnUnUc=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^.*</key>
<true/>
<key>^.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show more