Compare commits
54 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
71056542c4 | ||
![]() |
2348f5f4b1 | ||
![]() |
bfa3e25d9e | ||
![]() |
0fe067ccf6 | ||
![]() |
2e77bb0345 | ||
![]() |
83bf8aecae | ||
![]() |
e2e26eed8b | ||
![]() |
c21dd01a58 | ||
![]() |
fad5915c2e | ||
![]() |
43fb7e2e7f | ||
![]() |
056bcb6736 | ||
![]() |
33fdecf0ad | ||
![]() |
e9b862bfce | ||
![]() |
54ef5018e0 | ||
![]() |
f2cf4edb75 | ||
![]() |
6ece04918d | ||
![]() |
ea5287d9c7 | ||
![]() |
22de4fd6c4 | ||
![]() |
30b9d3cf1c | ||
![]() |
bffc5356a8 | ||
![]() |
d7dad1b848 | ||
![]() |
ad67d8d7df | ||
![]() |
6bc21f13ea | ||
![]() |
19b055de2d | ||
![]() |
29997c46e4 | ||
![]() |
bff023563b | ||
![]() |
90859393a3 | ||
![]() |
c5c79c26ea | ||
![]() |
61fca7892f | ||
![]() |
6b045f3e6f | ||
![]() |
f33e8ed879 | ||
![]() |
c32873a734 | ||
![]() |
c6415d7e32 | ||
![]() |
5c18cb1bbb | ||
![]() |
fc68e3d413 | ||
![]() |
e382a35387 | ||
![]() |
15171a703a | ||
![]() |
4530a8839b | ||
![]() |
4671ec67a2 | ||
![]() |
a5fe1a34c5 | ||
![]() |
b9282a25e8 | ||
![]() |
0bb5389370 | ||
![]() |
8b81cb39d7 | ||
![]() |
ccdb8b76a8 | ||
![]() |
37020a5026 | ||
![]() |
259f6c6872 | ||
![]() |
2b7e29fa21 | ||
![]() |
8917ebf708 | ||
![]() |
d326f5a00b | ||
![]() |
3721a77cc4 | ||
![]() |
667d54ed2d | ||
![]() |
1b70bfea8b | ||
![]() |
33b8571414 | ||
![]() |
33af004d85 |
49
.gitea/workflows/updateApp.yml
Normal 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
|
|
@ -1,3 +1,12 @@
|
||||||
|
Currently licensed under the GNU AFFERO GENERAL PUBLIC LICENSE version 3, or any later version, at your choice.
|
||||||
|
You may obtain a copy of the license at <https://gnu.org/>
|
||||||
|
|
||||||
|
Copyright (c) Rhajune Park and contributors, 2025
|
||||||
|
|
||||||
|
For copyright infringement claims, please contact abuse@pythonplayer123.dev for expedited processing
|
||||||
|
|
||||||
|
Previously licensed under the MeloNX License.
|
||||||
|
|
||||||
MeloNX License
|
MeloNX License
|
||||||
|
|
||||||
Copyright (c) MeloNX Team and Contributors
|
Copyright (c) MeloNX Team and Contributors
|
||||||
|
|
21
README.md
|
@ -28,6 +28,10 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
|
||||||
- Recommended Device: iPhone 15 Pro or newer.
|
- Recommended Device: iPhone 15 Pro or newer.
|
||||||
- Low-End Recommended Device: iPhone 13 Pro.
|
- Low-End Recommended Device: iPhone 13 Pro.
|
||||||
|
|
||||||
|
## Discord Server
|
||||||
|
|
||||||
|
We have a discord server!
|
||||||
|
- https://discord.gg/melonx
|
||||||
|
|
||||||
## How to install
|
## 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**
|
4. **Enable JIT**
|
||||||
- Use your preferred method to enable Just-In-Time (JIT) compilation.
|
- 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**
|
5. **Add Necessary Files**
|
||||||
|
|
||||||
|
@ -90,7 +94,7 @@ If having Issues installing firmware (Make sure your keys are installed first)
|
||||||
|
|
||||||
9. **Enable JIT**
|
9. **Enable JIT**
|
||||||
- Use your preferred method to enable Just-In-Time (JIT) compilation.
|
- 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
|
### TrollStore
|
||||||
|
@ -141,12 +145,12 @@ If having Issues installing firmware (Make sure your keys are installed first)
|
||||||
|
|
||||||
- **GPU**
|
- **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**
|
- **Input**
|
||||||
|
|
||||||
We currently have support for keyboard, touch input, JoyCon input support, and nearly all controllers.
|
We currently have support for keyboard, touch input, JoyCon input support, and nearly all MFI controllers.
|
||||||
Motion controls are natively supported in most cases.
|
Motion controls are natively supported in most cases, however JoyCons do not have motion support doe to an iOS limitation.
|
||||||
|
|
||||||
- **DLC & Modifications**
|
- **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.
|
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.
|
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.
|
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)
|
- [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.
|
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
|
||||||
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
|
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
|
||||||
|
|
|
@ -3,11 +3,13 @@
|
||||||
# Define the destination directory (hardcoded)
|
# Define the destination directory (hardcoded)
|
||||||
DESTINATION_DIR="src/MeloNX/Dependencies/Dynamic\ Libraries/Ryujinx.Headless.SDL2.dylib"
|
DESTINATION_DIR="src/MeloNX/Dependencies/Dynamic\ Libraries/Ryujinx.Headless.SDL2.dylib"
|
||||||
|
|
||||||
|
dotnet clean
|
||||||
|
|
||||||
# Restore the project
|
# Restore the project
|
||||||
dotnet restore
|
dotnet restore
|
||||||
|
|
||||||
# Build the project with the specified version
|
# Build the project with the specified version
|
||||||
dotnet build -c Release
|
# dotnet build -c Release
|
||||||
|
|
||||||
# Publish the project with the specified runtime and settings
|
# 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
|
dotnet publish -c Release -r ios-arm64 -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
#!/bin/bash
|
#!/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=(
|
SEARCH_PATHS=(
|
||||||
"/usr/local/share/dotnet"
|
"/usr/local/share/dotnet"
|
||||||
"/usr/local/bin"
|
"/usr/local/bin"
|
||||||
|
@ -14,10 +13,10 @@ SEARCH_PATHS=(
|
||||||
"$HOME/Developer"
|
"$HOME/Developer"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize DOTNET_PATH as empty
|
|
||||||
|
|
||||||
DOTNET_PATH=""
|
DOTNET_PATH=""
|
||||||
|
|
||||||
# Search in the defined paths
|
|
||||||
for path in "${SEARCH_PATHS[@]}"; do
|
for path in "${SEARCH_PATHS[@]}"; do
|
||||||
if [ -d "$path" ]; then
|
if [ -d "$path" ]; then
|
||||||
DOTNET_PATH=$(find "$path" -name dotnet -type f -print -quit 2>/dev/null)
|
DOTNET_PATH=$(find "$path" -name dotnet -type f -print -quit 2>/dev/null)
|
||||||
|
@ -27,20 +26,8 @@ for path in "${SEARCH_PATHS[@]}"; do
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Check if the path was found
|
|
||||||
if [ -z "$DOTNET_PATH" ]; then
|
if [ -z "$DOTNET_PATH" ]; then
|
||||||
echo "Error: dotnet path not found."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "dotnet path: $DOTNET_PATH"
|
echo "$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"
|
|
||||||
|
|
21
distribution/ios/xc-compile.sh
Executable 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
|
@ -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": []
|
||||||
|
}
|
|
@ -8,6 +8,4 @@
|
||||||
// Configuration settings file format documentation can be found at:
|
// Configuration settings file format documentation can be found at:
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
// https://help.apple.com/xcode/#/dev745c5c974
|
||||||
|
|
||||||
VERSION = 1.7.0
|
VERSION = 2.0.1
|
||||||
|
|
||||||
DOTNET = /usr/local/share/dotnet/dotnet
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
/* End PBXAggregateTarget section */
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
/* Begin PBXBuildFile 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 */; };
|
4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; };
|
||||||
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
||||||
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
|
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
|
||||||
|
@ -32,13 +32,6 @@
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
|
|
||||||
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
|
|
||||||
};
|
|
||||||
4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = {
|
4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
||||||
|
@ -53,6 +46,13 @@
|
||||||
remoteGlobalIDString = 4E80A98C2CD6F54500029585;
|
remoteGlobalIDString = 4E80A98C2CD6F54500029585;
|
||||||
remoteInfo = MeloNX;
|
remoteInfo = MeloNX;
|
||||||
};
|
};
|
||||||
|
4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
|
||||||
|
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
|
||||||
|
};
|
||||||
BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = {
|
BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
||||||
|
@ -120,6 +120,10 @@
|
||||||
CodeSignOnCopy,
|
CodeSignOnCopy,
|
||||||
RemoveHeadersOnCopy,
|
RemoveHeadersOnCopy,
|
||||||
);
|
);
|
||||||
|
"Dependencies/Dynamic Libraries/StosJIT.framework" = (
|
||||||
|
CodeSignOnCopy,
|
||||||
|
RemoveHeadersOnCopy,
|
||||||
|
);
|
||||||
"Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (
|
"Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (
|
||||||
CodeSignOnCopy,
|
CodeSignOnCopy,
|
||||||
);
|
);
|
||||||
|
@ -129,10 +133,6 @@
|
||||||
"Dependencies/Dynamic Libraries/libavutil.dylib" = (
|
"Dependencies/Dynamic Libraries/libavutil.dylib" = (
|
||||||
CodeSignOnCopy,
|
CodeSignOnCopy,
|
||||||
);
|
);
|
||||||
Dependencies/XCFrameworks/MoltenVK.xcframework = (
|
|
||||||
CodeSignOnCopy,
|
|
||||||
RemoveHeadersOnCopy,
|
|
||||||
);
|
|
||||||
Dependencies/XCFrameworks/SDL2.xcframework = (
|
Dependencies/XCFrameworks/SDL2.xcframework = (
|
||||||
CodeSignOnCopy,
|
CodeSignOnCopy,
|
||||||
RemoveHeadersOnCopy,
|
RemoveHeadersOnCopy,
|
||||||
|
@ -178,6 +178,7 @@
|
||||||
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
|
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
|
||||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
|
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
|
||||||
"Dependencies/Dynamic Libraries/RyujinxHelper.framework",
|
"Dependencies/Dynamic Libraries/RyujinxHelper.framework",
|
||||||
|
"Dependencies/Dynamic Libraries/StosJIT.framework",
|
||||||
Dependencies/XCFrameworks/libavcodec.xcframework,
|
Dependencies/XCFrameworks/libavcodec.xcframework,
|
||||||
Dependencies/XCFrameworks/libavfilter.xcframework,
|
Dependencies/XCFrameworks/libavfilter.xcframework,
|
||||||
Dependencies/XCFrameworks/libavformat.xcframework,
|
Dependencies/XCFrameworks/libavformat.xcframework,
|
||||||
|
@ -186,7 +187,6 @@
|
||||||
Dependencies/XCFrameworks/libswresample.xcframework,
|
Dependencies/XCFrameworks/libswresample.xcframework,
|
||||||
Dependencies/XCFrameworks/libswscale.xcframework,
|
Dependencies/XCFrameworks/libswscale.xcframework,
|
||||||
Dependencies/XCFrameworks/libteakra.xcframework,
|
Dependencies/XCFrameworks/libteakra.xcframework,
|
||||||
Dependencies/XCFrameworks/MoltenVK.xcframework,
|
|
||||||
Dependencies/XCFrameworks/SDL2.xcframework,
|
Dependencies/XCFrameworks/SDL2.xcframework,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -203,8 +203,8 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
|
|
||||||
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
|
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
|
||||||
|
4549A31C2DD8795900EC8D88 /* CocoaAsyncSocket in Frameworks */,
|
||||||
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
|
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
|
||||||
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
|
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
|
@ -264,12 +264,12 @@
|
||||||
/* Begin PBXLegacyTarget section */
|
/* Begin PBXLegacyTarget section */
|
||||||
BD43C61D2D1B23AB003BBC42 /* Ryujinx */ = {
|
BD43C61D2D1B23AB003BBC42 /* Ryujinx */ = {
|
||||||
isa = PBXLegacyTarget;
|
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" */;
|
buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
);
|
);
|
||||||
buildToolPath = "$(DOTNET)";
|
buildToolPath = /bin/sh;
|
||||||
buildWorkingDirectory = "$(SRCROOT)/../..";
|
buildWorkingDirectory = "$(SRCROOT)/../../";
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = Ryujinx;
|
name = Ryujinx;
|
||||||
|
@ -294,15 +294,15 @@
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
4E2953AC2D803BC9000497CD /* PBXTargetDependency */,
|
4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
4E80A98F2CD6F54500029585 /* MeloNX */,
|
4E80A98F2CD6F54500029585 /* MeloNX */,
|
||||||
);
|
);
|
||||||
name = MeloNX;
|
name = MeloNX;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
|
|
||||||
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
|
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
|
||||||
|
4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */,
|
||||||
);
|
);
|
||||||
productName = MeloNX;
|
productName = MeloNX;
|
||||||
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
|
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
|
||||||
|
@ -393,8 +393,8 @@
|
||||||
mainGroup = 4E80A9842CD6F54500029585;
|
mainGroup = 4E80A9842CD6F54500029585;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
|
|
||||||
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
|
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
|
||||||
|
4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 56;
|
preferredProjectObjectVersion = 56;
|
||||||
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
|
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
|
||||||
|
@ -482,12 +482,6 @@
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
platformFilter = ios;
|
|
||||||
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
|
|
||||||
targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = {
|
4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
||||||
|
@ -498,6 +492,11 @@
|
||||||
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
||||||
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
|
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
|
||||||
|
targetProxy = 4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
|
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
|
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
|
||||||
|
@ -571,6 +570,7 @@
|
||||||
ONLY_ACTIVE_ARCH = NO;
|
ONLY_ACTIVE_ARCH = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
|
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
};
|
};
|
||||||
|
@ -637,6 +637,7 @@
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_DISABLE_SAFETY_CHECKS = YES;
|
SWIFT_DISABLE_SAFETY_CHECKS = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
|
SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "debug-only";
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
|
@ -646,13 +647,16 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
|
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
|
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
||||||
|
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_TESTABILITY = NO;
|
ENABLE_TESTABILITY = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
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",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = z;
|
GCC_OPTIMIZATION_LEVEL = z;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
@ -733,10 +803,10 @@
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -863,6 +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",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = "$(VERSION)";
|
MARKETING_VERSION = "$(VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
|
@ -880,13 +1063,16 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
|
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
|
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
||||||
|
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
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",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = z;
|
GCC_OPTIMIZATION_LEVEL = z;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
@ -967,10 +1219,10 @@
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -1097,6 +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",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = "$(VERSION)";
|
MARKETING_VERSION = "$(VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
|
@ -1298,12 +1663,12 @@
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
|
4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
|
repositoryURL = "https://github.com/robbiehanson/CocoaAsyncSocket";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 1.5.0;
|
minimumVersion = 7.6.5;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
|
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
|
||||||
|
@ -1317,10 +1682,10 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
|
4549A31B2DD8795900EC8D88 /* CocoaAsyncSocket */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;
|
package = 4549A31A2DD8795900EC8D88 /* XCRemoteSwiftPackageReference "CocoaAsyncSocket" */;
|
||||||
productName = SwiftUIJoystick;
|
productName = CocoaAsyncSocket;
|
||||||
};
|
};
|
||||||
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
|
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
{
|
{
|
||||||
"originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b",
|
"originHash" : "b4a593815773c4e9eedb98cabe88f41620776314bffb6c39d5a41cb743e4d390",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "cocoaasyncsocket",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/robbiehanson/CocoaAsyncSocket",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "dbdc00669c1ced63b27c3c5f052ee4d28f10150c",
|
||||||
|
"version" : "7.6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swiftsvg",
|
"identity" : "swiftsvg",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
@ -9,15 +18,6 @@
|
||||||
"branch" : "master",
|
"branch" : "master",
|
||||||
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
|
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swiftuijoystick",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/michael94ellis/SwiftUIJoystick",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65",
|
|
||||||
"version" : "1.5.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1620"
|
LastUpgradeVersion = "1620"
|
||||||
version = "1.7">
|
version = "2.0">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES"
|
buildImplicitDependencies = "YES"
|
||||||
|
@ -58,16 +58,19 @@
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
disableMainThreadChecker = "YES"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
|
debugXPCServices = "NO"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
enableGPUValidationMode = "1"
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES"
|
allowLocationSimulation = "YES"
|
||||||
viewDebuggingEnabled = "No"
|
queueDebuggingEnabled = "No"
|
||||||
consoleMode = "0"
|
consoleMode = "0"
|
||||||
structuredConsoleMode = "2">
|
structuredConsoleMode = "2"
|
||||||
|
disablePerformanceAntipatternChecker = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
|
|
|
@ -17,11 +17,20 @@ func SecTaskCopyValueForEntitlement(
|
||||||
_ error: NSErrorPointer
|
_ error: NSErrorPointer
|
||||||
) -> CFTypeRef?
|
) -> CFTypeRef?
|
||||||
|
|
||||||
|
@_silgen_name("SecTaskCopyTeamIdentifier")
|
||||||
|
func SecTaskCopyTeamIdentifier(
|
||||||
|
_ task: SecTaskRef,
|
||||||
|
_ error: NSErrorPointer
|
||||||
|
) -> NSString?
|
||||||
|
|
||||||
@_silgen_name("SecTaskCreateFromSelf")
|
@_silgen_name("SecTaskCreateFromSelf")
|
||||||
func SecTaskCreateFromSelf(
|
func SecTaskCreateFromSelf(
|
||||||
_ allocator: CFAllocator?
|
_ allocator: CFAllocator?
|
||||||
) -> SecTaskRef?
|
) -> SecTaskRef?
|
||||||
|
|
||||||
|
@_silgen_name("CFRelease")
|
||||||
|
func CFRelease(_ cf: CFTypeRef)
|
||||||
|
|
||||||
@_silgen_name("SecTaskCopyValuesForEntitlements")
|
@_silgen_name("SecTaskCopyValuesForEntitlements")
|
||||||
func SecTaskCopyValuesForEntitlements(
|
func SecTaskCopyValuesForEntitlements(
|
||||||
_ task: SecTaskRef,
|
_ task: SecTaskRef,
|
||||||
|
@ -29,30 +38,43 @@ func SecTaskCopyValuesForEntitlements(
|
||||||
_ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
|
_ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
|
||||||
) -> CFDictionary?
|
) -> CFDictionary?
|
||||||
|
|
||||||
|
func releaseSecTask(_ task: SecTaskRef) {
|
||||||
|
let cf = unsafeBitCast(task, to: CFTypeRef.self)
|
||||||
|
CFRelease(cf)
|
||||||
|
}
|
||||||
|
|
||||||
func checkAppEntitlements(_ ents: [String]) -> [String: Any] {
|
func checkAppEntitlements(_ ents: [String]) -> [String: Any] {
|
||||||
guard let task = SecTaskCreateFromSelf(nil) else {
|
guard let task = SecTaskCreateFromSelf(nil) else {
|
||||||
print("Failed to create SecTask")
|
|
||||||
return [:]
|
return [:]
|
||||||
}
|
}
|
||||||
|
defer {
|
||||||
|
releaseSecTask(task)
|
||||||
|
}
|
||||||
|
|
||||||
guard let entitlements = SecTaskCopyValuesForEntitlements(task, ents as CFArray, nil) else {
|
guard let entitlements = SecTaskCopyValuesForEntitlements(task, ents as CFArray, nil) else {
|
||||||
print("Failed to get entitlements")
|
|
||||||
return [:]
|
return [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
return (entitlements as? [String: Any]) ?? [:]
|
return (entitlements as NSDictionary) as? [String: Any] ?? [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAppEntitlement(_ ent: String) -> Bool {
|
func checkAppEntitlement(_ ent: String) -> Bool {
|
||||||
guard let task = SecTaskCreateFromSelf(nil) else {
|
guard let task = SecTaskCreateFromSelf(nil) else {
|
||||||
print("Failed to create SecTask")
|
return false
|
||||||
|
}
|
||||||
|
defer {
|
||||||
|
releaseSecTask(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let entitlement = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let entitlements = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
|
if let number = entitlement as? NSNumber {
|
||||||
print("Failed to get entitlements")
|
return number.boolValue
|
||||||
return false
|
} else if let bool = entitlement as? Bool {
|
||||||
|
return bool
|
||||||
}
|
}
|
||||||
|
|
||||||
return entitlements.boolValue != nil && entitlements.boolValue
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,14 @@
|
||||||
|
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <SDL2/SDL_syswm.h>
|
#include <SDL2/SDL_syswm.h>
|
||||||
|
#include <StosJIT/StosJIT-Swift.h>
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
struct GameInfo {
|
struct GameInfo {
|
||||||
long FileSize;
|
long FileSize;
|
||||||
char TitleName[512];
|
char TitleName[512];
|
||||||
|
@ -40,6 +43,10 @@ struct DlcNcaList {
|
||||||
struct DlcNcaListItem* items;
|
struct DlcNcaListItem* items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef void (^SwiftCallback)(NSString *result);
|
||||||
|
|
||||||
|
void RegisterCallback(NSString *identifier, SwiftCallback callback);
|
||||||
|
|
||||||
extern struct GameInfo get_game_info(int, char*);
|
extern struct GameInfo get_game_info(int, char*);
|
||||||
|
|
||||||
extern struct DlcNcaList get_dlc_nca_list(const char* titleIdPtr, const char* pathPtr);
|
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 set_native_window(void *layerPtr);
|
||||||
|
|
||||||
|
void pause_emulation(bool shouldPause);
|
||||||
|
|
||||||
void stop_emulation();
|
void stop_emulation();
|
||||||
|
|
||||||
void initialize();
|
void initialize();
|
||||||
|
|
||||||
int main_ryujinx_sdl(int argc, char **argv);
|
int main_ryujinx_sdl(int argc, char **argv);
|
||||||
|
|
||||||
|
int update_settings_external(int argc, char **argv);
|
||||||
|
|
||||||
int get_current_fps();
|
int get_current_fps();
|
||||||
|
|
||||||
void touch_began(float x, float y, int index);
|
void touch_began(float x, float y, int index);
|
||||||
|
|
|
@ -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
|
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 {
|
func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
|
||||||
var region: vm_address_t = vm_address_t(UInt(bitPattern: address))
|
var region: vm_address_t = vm_address_t(UInt(bitPattern: address))
|
||||||
var regionSize: vm_size_t = 0
|
var regionSize: vm_size_t = 0
|
||||||
|
@ -34,7 +42,7 @@ func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
if result != KERN_SUCCESS {
|
if result != KERN_SUCCESS {
|
||||||
print("Failed to reach \(address)")
|
// print("Failed to reach \(address)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ func enableJITEB() {
|
||||||
|
|
||||||
func enableJITEBRequest() {
|
func enableJITEBRequest() {
|
||||||
let pid = Int(getpid())
|
let pid = Int(getpid())
|
||||||
print(pid)
|
// print(pid)
|
||||||
|
|
||||||
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
|
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
|
||||||
var request = URLRequest(url: address)
|
var request = URLRequest(url: address)
|
||||||
|
@ -90,7 +90,7 @@ func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping
|
||||||
|
|
||||||
let task = session.dataTask(with: request) { _, response, error in
|
let task = session.dataTask(with: request) { _, response, error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
print("Ping failed: \(error.localizedDescription)")
|
// print("Ping failed: \(error.localizedDescription)")
|
||||||
completion(false)
|
completion(false)
|
||||||
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
||||||
completion(true)
|
completion(true)
|
||||||
|
@ -118,6 +118,7 @@ func presentAlert(title: String, message: String, completion: (() -> Void)? = ni
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct LaunchApp: Codable {
|
struct LaunchApp: Codable {
|
||||||
let success: Bool
|
let success: Bool
|
||||||
let message: String
|
let message: String
|
||||||
|
@ -140,12 +141,12 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
|
||||||
viewController.present(alert, animated: true)
|
viewController.present(alert, animated: true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print("Hopefully JIT is enabled now...")
|
// print("Hopefully JIT is enabled now...")
|
||||||
Ryujinx.shared.ryuIsJITEnabled()
|
Ryujinx.shared.ryuIsJITEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
print(String(data: jsonData, encoding: .utf8))
|
// print(String(data: jsonData, encoding: .utf8))
|
||||||
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
|
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
|
||||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
|
||||||
|
|
74
src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?
|
||||||
|
}
|
|
@ -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) }
|
||||||
|
}
|
|
@ -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]() // slot→provider
|
||||||
|
private var packetNumber = [UInt8 : UInt32]() // per-slot counter
|
||||||
|
|
||||||
|
// ───────── UDP setup
|
||||||
|
private func configureSocket() {
|
||||||
|
socket = GCDAsyncUdpSocket(delegate: self, delegateQueue: .main)
|
||||||
|
do {
|
||||||
|
try socket?.bind(toPort: C.port)
|
||||||
|
try socket?.beginReceiving()
|
||||||
|
//print("🟢 DSU server listening on UDP \(C.port)")
|
||||||
|
} catch {
|
||||||
|
//print("❌ DSU socket error:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//──────────────────────────────────────────────────────────────────────── MARK:- UDP delegate
|
||||||
|
|
||||||
|
extension DSUServer: GCDAsyncUdpSocketDelegate {
|
||||||
|
|
||||||
|
func udpSocket(_ sock: GCDAsyncUdpSocket,
|
||||||
|
didReceive data: Data,
|
||||||
|
fromAddress addr: Data,
|
||||||
|
withFilterContext ctx: Any?) {
|
||||||
|
|
||||||
|
lastClientAddress = addr
|
||||||
|
|
||||||
|
// Light validation
|
||||||
|
guard data.count >= 20,
|
||||||
|
String(decoding: data[0..<4], as: UTF8.self) == C.headerMagic,
|
||||||
|
data.readUInt16LE(at: 4) == C.protocolVersion
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
let type = data.readUInt32LE(at: 16)
|
||||||
|
switch type {
|
||||||
|
case 0x100001: sendPortInfo(to: addr) // client asks for port list
|
||||||
|
case 0x100002: break // subscription acknowledged
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func udpSocketDidClose(_ sock: GCDAsyncUdpSocket, withError err: Error?) {
|
||||||
|
//print("UDP closed:", err?.localizedDescription ?? "nil")
|
||||||
|
lastClientAddress = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//──────────────────────────────────────────────────────────────────────── MARK:- Packet helpers
|
||||||
|
|
||||||
|
private extension DSUServer {
|
||||||
|
|
||||||
|
// ── Header (16 bytes)
|
||||||
|
func appendHeader(into d: inout Data, payloadSize: UInt16) {
|
||||||
|
d.append(C.headerMagic.data(using: .utf8)!) // "DSUS"
|
||||||
|
d.append(C.protocolVersion.leData) // Protocol Version
|
||||||
|
d.append(payloadSize.leData) // Payload Size
|
||||||
|
d.append(Data(repeating: 0, count: 4)) // CRC-stub
|
||||||
|
d.append(serverID.leData) // Server ID
|
||||||
|
}
|
||||||
|
func patchCRC32(of packet: inout Data) {
|
||||||
|
let crc = packet.withUnsafeBytes { ptr in
|
||||||
|
crc32(0, ptr.baseAddress, uInt(packet.count))
|
||||||
|
}.littleEndian
|
||||||
|
let crcLE = UInt32(crc).littleEndian
|
||||||
|
let crcData = withUnsafeBytes(of: crcLE) { Data($0) }
|
||||||
|
packet.replaceSubrange(8..<12, with: crcData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 0x100001 DSUSPortInfo
|
||||||
|
func sendPortInfo(to addr: Data) {
|
||||||
|
for p in providers.values {
|
||||||
|
var pkt = Data()
|
||||||
|
appendHeader(into: &pkt, payloadSize: 12)
|
||||||
|
pkt.append(UInt32(0x100001).leData)
|
||||||
|
|
||||||
|
pkt.append(p.slot)
|
||||||
|
pkt.append(UInt8(2)) // connected
|
||||||
|
pkt.append(UInt8(2)) // full gyro
|
||||||
|
pkt.append(p.connectionType)
|
||||||
|
pkt.append(p.mac, count: 6)
|
||||||
|
pkt.append(p.batteryLevel)
|
||||||
|
pkt.append(UInt8(0)) // padding
|
||||||
|
|
||||||
|
patchCRC32(of: &pkt)
|
||||||
|
socket?.send(pkt, toAddress: addr, withTimeout: -1, tag: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 0x100002 DSUSPadDataRsp
|
||||||
|
func sendPadData(sample s: DSUMotionSample,
|
||||||
|
from p: DSUMotionProvider,
|
||||||
|
to addr: Data) {
|
||||||
|
|
||||||
|
var pkt = Data()
|
||||||
|
appendHeader(into: &pkt, payloadSize: 84)
|
||||||
|
pkt.append(UInt32(0x100002).leData)
|
||||||
|
|
||||||
|
pkt.append(p.slot)
|
||||||
|
pkt.append(UInt8(2)) // connected
|
||||||
|
pkt.append(UInt8(2)) // full gyro
|
||||||
|
pkt.append(p.connectionType)
|
||||||
|
pkt.append(p.mac, count: 6)
|
||||||
|
pkt.append(p.batteryLevel)
|
||||||
|
pkt.append(UInt8(1)) // is connected
|
||||||
|
|
||||||
|
let num = packetNumber[p.slot, default: 0]
|
||||||
|
pkt.append(num.leData)
|
||||||
|
packetNumber[p.slot] = num &+ 1
|
||||||
|
|
||||||
|
pkt.append(UInt16(0).leData) // buttons
|
||||||
|
pkt.append(contentsOf: [0,0]) // HOME / Touch
|
||||||
|
pkt.append(contentsOf: [128,128,128,128]) // sticks
|
||||||
|
pkt.append(Data(repeating: 0, count: 12)) // d-pad / face / trig
|
||||||
|
pkt.append(Data(repeating: 0, count: 12)) // touch 1 & 2
|
||||||
|
pkt.append(s.timestampUS.leData)
|
||||||
|
|
||||||
|
pkt.append(s.accel.x.leData)
|
||||||
|
pkt.append(s.accel.y.leData)
|
||||||
|
pkt.append(s.accel.z.leData)
|
||||||
|
|
||||||
|
pkt.append(s.gyroDeg.x.leData)
|
||||||
|
pkt.append(s.gyroDeg.y.leData)
|
||||||
|
pkt.append(s.gyroDeg.z.leData)
|
||||||
|
|
||||||
|
patchCRC32(of: &pkt)
|
||||||
|
socket?.send(pkt, toAddress: addr, withTimeout: -1, tag: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//──────────────────────────────────────────────────────────────────────── MARK:- Helper funcs / ext
|
||||||
|
|
||||||
|
private extension FixedWidthInteger {
|
||||||
|
var leData: Data {
|
||||||
|
var v = self.littleEndian
|
||||||
|
return Data(bytes: &v, count: MemoryLayout<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
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,18 +8,36 @@
|
||||||
import CoreHaptics
|
import CoreHaptics
|
||||||
import GameController
|
import GameController
|
||||||
|
|
||||||
class NativeController: Hashable {
|
class NativeController: Hashable, BaseController {
|
||||||
private var instanceID: SDL_JoystickID = -1
|
private var instanceID: SDL_JoystickID = -1
|
||||||
private var controller: OpaquePointer?
|
private var controller: OpaquePointer?
|
||||||
private var nativeController: GCController
|
private var nativeController: GCController
|
||||||
|
private var controllerMotionProvider: DSUMotionProvider?
|
||||||
|
|
||||||
private let controllerHaptics: CHHapticEngine?
|
private let controllerHaptics: CHHapticEngine?
|
||||||
|
private let rumbleController: RumbleController?
|
||||||
|
|
||||||
public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" }
|
public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" }
|
||||||
|
|
||||||
init(_ controller: GCController) {
|
init(_ controller: GCController) {
|
||||||
nativeController = controller
|
nativeController = controller
|
||||||
controllerHaptics = nativeController.haptics?.createEngine(withLocality: .default)
|
var ncontrollerHaptics = nativeController.haptics?.createEngine(withLocality: .default)
|
||||||
try? controllerHaptics?.start()
|
|
||||||
|
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()
|
setupHandheldController()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +45,21 @@ class NativeController: Hashable {
|
||||||
cleanup()
|
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() {
|
private func setupHandheldController() {
|
||||||
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
|
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
|
||||||
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
|
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
|
||||||
|
@ -49,39 +82,40 @@ class NativeController: Hashable {
|
||||||
// Update joystick state here
|
// Update joystick state here
|
||||||
},
|
},
|
||||||
SetPlayerIndex: { userdata, playerIndex in
|
SetPlayerIndex: { userdata, playerIndex in
|
||||||
print("Player index set to \(playerIndex)")
|
// print("Player index set to \(playerIndex)")
|
||||||
|
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
|
Rumble: { userdata, lowFreq, highFreq in
|
||||||
print("Rumble with \(lowFreq), \(highFreq)")
|
|
||||||
guard let userdata else { return 0 }
|
guard let userdata else { return 0 }
|
||||||
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
|
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics)
|
_self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
RumbleTriggers: { userdata, leftRumble, rightRumble in
|
RumbleTriggers: { userdata, leftRumble, rightRumble in
|
||||||
print("Trigger rumble with \(leftRumble), \(rightRumble)")
|
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
SetLED: { userdata, red, green, blue in
|
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
|
return 0
|
||||||
},
|
},
|
||||||
SendEffect: { userdata, data, size in
|
SendEffect: { userdata, data, size in
|
||||||
print("Effect sent with size \(size)")
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
|
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)
|
||||||
if instanceID < 0 {
|
if instanceID < 0 {
|
||||||
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
controller = SDL_GameControllerOpen(Int32(instanceID))
|
controller = SDL_GameControllerOpen(Int32(instanceID))
|
||||||
|
|
||||||
if controller == nil {
|
if controller == nil {
|
||||||
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,10 +123,10 @@ class NativeController: Hashable {
|
||||||
guard let gamepad = nativeController.extendedGamepad
|
guard let gamepad = nativeController.extendedGamepad
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
setupButtonChangeListener(gamepad.buttonA, for: .A)
|
setupButtonChangeListener(gamepad.buttonA, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .B : .A)
|
||||||
setupButtonChangeListener(gamepad.buttonB, for: .B)
|
setupButtonChangeListener(gamepad.buttonB, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .A : .B)
|
||||||
setupButtonChangeListener(gamepad.buttonX, for: .X)
|
setupButtonChangeListener(gamepad.buttonX, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .Y : .X)
|
||||||
setupButtonChangeListener(gamepad.buttonY, for: .Y)
|
setupButtonChangeListener(gamepad.buttonY, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .X : .Y)
|
||||||
|
|
||||||
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
|
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
|
||||||
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
|
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
|
||||||
|
@ -139,49 +173,13 @@ class NativeController: Hashable {
|
||||||
|
|
||||||
func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) {
|
func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) {
|
||||||
button.valueChangedHandler = { [unowned self] _, value, pressed in
|
button.valueChangedHandler = { [unowned self] _, value, pressed in
|
||||||
// print("Value: \(value), Is pressed: \(pressed)")
|
// // print("Value: \(value), Is pressed: \(pressed)")
|
||||||
let axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
let axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
||||||
let scaledValue = Sint16(value * 32767.0)
|
let scaledValue = Sint16(value * 32767.0)
|
||||||
updateAxisValue(value: scaledValue, forAxis: axis)
|
updateAxisValue(value: scaledValue, forAxis: axis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
|
||||||
guard controller != nil else { return }
|
guard controller != nil else { return }
|
||||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
||||||
|
@ -206,7 +204,6 @@ class NativeController: Hashable {
|
||||||
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
|
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
|
||||||
guard controller != nil else { return }
|
guard controller != nil else { return }
|
||||||
|
|
||||||
// print("Button: \(button.rawValue) {state: \(state)}")
|
|
||||||
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
|
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
|
||||||
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
||||||
let value: Int = (state == 1) ? 32767 : 0
|
let value: Int = (state == 1) ? 32767 : 0
|
||||||
|
|
|
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,16 +9,49 @@ import Foundation
|
||||||
import CoreHaptics
|
import CoreHaptics
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class VirtualController {
|
class VirtualController : BaseController {
|
||||||
private var instanceID: SDL_JoystickID = -1
|
private var instanceID: SDL_JoystickID = -1
|
||||||
private var controller: OpaquePointer?
|
private var controller: OpaquePointer?
|
||||||
|
private let hapticEngine: CHHapticEngine?
|
||||||
|
private let rumbleController: RumbleController?
|
||||||
|
private var deviceMotionProvider: DeviceMotionProvider?
|
||||||
|
|
||||||
public let controllername = "MeloNX Touch Controller"
|
public let controllername = "MeloNX Touch Controller"
|
||||||
|
|
||||||
init() {
|
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()
|
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() {
|
private func setupVirtualController() {
|
||||||
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
|
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
|
||||||
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
|
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
|
||||||
|
@ -36,94 +69,50 @@ class VirtualController {
|
||||||
button_mask: 0,
|
button_mask: 0,
|
||||||
axis_mask: 0,
|
axis_mask: 0,
|
||||||
name: controllername.withCString { $0 },
|
name: controllername.withCString { $0 },
|
||||||
userdata: nil,
|
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
||||||
Update: { userdata in
|
Update: { userdata in
|
||||||
// Update joystick state here
|
// Update joystick state here
|
||||||
},
|
},
|
||||||
SetPlayerIndex: { userdata, playerIndex in
|
SetPlayerIndex: { userdata, playerIndex in
|
||||||
print("Player index set to \(playerIndex)")
|
// print("Player index set to \(playerIndex)")
|
||||||
},
|
},
|
||||||
Rumble: { userdata, lowFreq, highFreq in
|
Rumble: { userdata, lowFreq, highFreq in
|
||||||
print("Rumble with \(lowFreq), \(highFreq)")
|
// print("Rumble with \(lowFreq), \(highFreq)")
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
|
guard let userdata else { return 0 }
|
||||||
|
let _self = Unmanaged<VirtualController>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
_self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
RumbleTriggers: { userdata, leftRumble, rightRumble in
|
RumbleTriggers: { userdata, leftRumble, rightRumble in
|
||||||
print("Trigger rumble with \(leftRumble), \(rightRumble)")
|
// print("Trigger rumble with \(leftRumble), \(rightRumble)")
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
SetLED: { userdata, red, green, blue in
|
SetLED: { userdata, red, green, blue in
|
||||||
print("Set LED to RGB(\(red), \(green), \(blue))")
|
// print("Set LED to RGB(\(red), \(green), \(blue))")
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
SendEffect: { userdata, data, size in
|
SendEffect: { userdata, data, size in
|
||||||
print("Effect sent with size \(size)")
|
// print("Effect sent with size \(size)")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
|
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
|
||||||
if instanceID < 0 {
|
if instanceID < 0 {
|
||||||
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
|
// print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
controller = SDL_GameControllerOpen(Int32(instanceID))
|
controller = SDL_GameControllerOpen(Int32(instanceID))
|
||||||
|
|
||||||
if controller == nil {
|
if controller == nil {
|
||||||
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
|
// print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
|
||||||
guard controller != nil else { return }
|
guard controller != nil else { return }
|
||||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
||||||
|
@ -131,10 +120,8 @@ class VirtualController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
|
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
|
||||||
let scaleFactor = 32767.0 / 160
|
let scaledX = Int16(min(32767.0, max(-32768.0, x * 32767.0)))
|
||||||
|
let scaledY = Int16(min(32767.0, max(-32768.0, y * 32767.0)))
|
||||||
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
|
|
||||||
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
|
|
||||||
|
|
||||||
if stick == .right {
|
if stick == .right {
|
||||||
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
|
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
|
||||||
|
@ -148,7 +135,7 @@ class VirtualController {
|
||||||
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
|
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
|
||||||
guard controller != nil else { return }
|
guard controller != nil else { return }
|
||||||
|
|
||||||
print("Button: \(button.rawValue) {state: \(state)}")
|
// // print("Button: \(button.rawValue) {state: \(state)}")
|
||||||
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
|
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
|
||||||
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
||||||
let value: Int = (state == 1) ? 32767 : 0
|
let value: Int = (state == 1) ? 32767 : 0
|
||||||
|
|
|
@ -13,7 +13,7 @@ class MemoryUsageMonitor: ObservableObject {
|
||||||
private var timer: Timer?
|
private var timer: Timer?
|
||||||
|
|
||||||
init() {
|
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()
|
self?.updateMemoryUsage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,11 +32,12 @@ class MemoryUsageMonitor: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
if result == KERN_SUCCESS {
|
if result == KERN_SUCCESS {
|
||||||
|
memoryUsage = 0
|
||||||
memoryUsage = taskInfo.phys_footprint
|
memoryUsage = taskInfo.phys_footprint
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
print("Error with task_info(): " +
|
// print("Error with task_info(): " +
|
||||||
(String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
// (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +47,6 @@ class MemoryUsageMonitor: ObservableObject {
|
||||||
formatter.countStyle = .memory
|
formatter.countStyle = .memory
|
||||||
return formatter.string(fromByteCount: Int64(bytes))
|
return formatter.string(fromByteCount: Int64(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,34 +6,28 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class MTLHud: ObservableObject {
|
||||||
class MTLHud {
|
|
||||||
|
|
||||||
@Published var canMetalHud: Bool = false
|
@Published var canMetalHud: Bool = false
|
||||||
|
|
||||||
var isEnabled: Bool {
|
@AppStorage("MTL_HUD_ENABLED") var metalHudEnabled: Bool = false {
|
||||||
if let getenv = getenv("MTL_HUD_ENABLED") {
|
didSet {
|
||||||
return String(cString: getenv).contains("1")
|
if metalHudEnabled {
|
||||||
|
enable()
|
||||||
|
} else {
|
||||||
|
disable()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static let shared = MTLHud()
|
static let shared = MTLHud()
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
let _ = openMetalDylib() // i'm fixing the warnings just because you said i suck at coding Autumn (propenchiefer,
|
canMetalHud = openMetalDylib() // i'm fixing the warnings just because you said i suck at coding Autumn (propenchiefer, https://youtu.be/tc65SNOTMz4 7:23)
|
||||||
https://youtu.be/tc65SNOTMz4 7:23)
|
|
||||||
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
|
|
||||||
enable()
|
|
||||||
} else {
|
|
||||||
disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toggle() {
|
if metalHudEnabled {
|
||||||
print(UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED"))
|
|
||||||
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
|
|
||||||
enable()
|
enable()
|
||||||
} else {
|
} else {
|
||||||
disable()
|
disable()
|
||||||
|
@ -44,14 +38,8 @@ class MTLHud {
|
||||||
let path = "/usr/lib/libMTLHud.dylib"
|
let path = "/usr/lib/libMTLHud.dylib"
|
||||||
|
|
||||||
if dlopen(path, RTLD_NOW) != nil {
|
if dlopen(path, RTLD_NOW) != nil {
|
||||||
print("Library loaded from \(path)")
|
|
||||||
canMetalHud = true
|
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
if let error = String(validatingUTF8: dlerror()) {
|
|
||||||
print("Error loading library: \(error)")
|
|
||||||
}
|
|
||||||
canMetalHud = false
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,94 @@ import SwiftUI
|
||||||
import GameController
|
import GameController
|
||||||
import MetalKit
|
import MetalKit
|
||||||
import Metal
|
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 {
|
struct Controller: Identifiable, Hashable {
|
||||||
var id: String
|
var id: String
|
||||||
|
@ -30,22 +118,28 @@ struct iOSNav<Content: View>: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func threadEntry(_ arg: () -> Void) -> UnsafeMutableRawPointer? {
|
||||||
|
arg()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Ryujinx : ObservableObject {
|
class Ryujinx : ObservableObject {
|
||||||
private var isRunning = false
|
@Published var isRunning = false
|
||||||
|
|
||||||
let virtualController = VirtualController()
|
let virtualController = VirtualController()
|
||||||
|
|
||||||
@Published var controllerMap: [Controller] = []
|
@Published var controllerMap: [Controller] = []
|
||||||
@Published var metalLayer: CAMetalLayer? = nil
|
@Published var metalLayer: CAMetalLayer? = nil
|
||||||
|
@Published var isPortrait = false
|
||||||
@Published var firmwareversion = "0"
|
@Published var firmwareversion = "0"
|
||||||
@Published var emulationUIView: MeloMTKView? = nil
|
@Published var emulationUIView: MeloMTKView? = nil
|
||||||
@Published var config: Ryujinx.Configuration? = nil
|
@Published var config: Ryujinx.Arguments? = nil
|
||||||
@Published var games: [Game] = []
|
@Published var games: [Game] = []
|
||||||
|
|
||||||
@Published var defMLContentSize: CGFloat?
|
@Published var defMLContentSize: CGFloat?
|
||||||
|
|
||||||
var thread: Thread!
|
var thread: pthread_t? = nil
|
||||||
|
|
||||||
@Published var jitenabled = false
|
@Published var jitenabled = false
|
||||||
|
|
||||||
|
@ -55,40 +149,75 @@ class Ryujinx : ObservableObject {
|
||||||
|
|
||||||
static let shared = Ryujinx()
|
static let shared = Ryujinx()
|
||||||
|
|
||||||
private init() {
|
func addGames() {
|
||||||
self.games = loadGames()
|
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 gamepath: String
|
||||||
var inputids: [String]
|
var inputids: [String]
|
||||||
var resscale: Float
|
var inputDSUServers: [String]
|
||||||
var debuglogs: Bool
|
var resscale: Float = 1.0
|
||||||
var tracelogs: Bool
|
var debuglogs: Bool = false
|
||||||
var nintendoinput: Bool
|
var tracelogs: Bool = false
|
||||||
var enableInternet: Bool
|
var nintendoinput: Bool = true
|
||||||
var listinputids: Bool
|
var enableInternet: Bool = false
|
||||||
var aspectRatio: AspectRatio
|
var listinputids: Bool = false
|
||||||
var memoryManagerMode: String
|
var aspectRatio: AspectRatio = .fixed16x9
|
||||||
var disableShaderCache: Bool
|
var memoryManagerMode: String = "HostMappedUnsafe"
|
||||||
var hypervisor: Bool
|
var disableShaderCache: Bool = false
|
||||||
var disableDockedMode: Bool
|
var hypervisor: Bool = false
|
||||||
var enableTextureRecompression: Bool
|
var disableDockedMode: Bool = false
|
||||||
var additionalArgs: [String]
|
var enableTextureRecompression: Bool = true
|
||||||
var maxAnisotropy: Float
|
var additionalArgs: [String] = []
|
||||||
var macroHLE: Bool
|
var maxAnisotropy: Float = 1.0
|
||||||
var ignoreMissingServices: Bool
|
var macroHLE: Bool = true
|
||||||
var expandRam: Bool
|
var ignoreMissingServices: Bool = false
|
||||||
var dfsIntegrityChecks: Bool
|
var expandRam: Bool = false
|
||||||
var disablePTC: Bool
|
var dfsIntegrityChecks: Bool = false
|
||||||
var disablevsync: Bool
|
var disablePTC: Bool = false
|
||||||
var language: SystemLanguage
|
var disablevsync: Bool = false
|
||||||
var regioncode: SystemRegionCode
|
var language: SystemLanguage = .americanEnglish
|
||||||
var handHeldController: Bool
|
var regioncode: SystemRegionCode = .usa
|
||||||
|
var handHeldController: Bool = true
|
||||||
|
|
||||||
|
|
||||||
init(gamepath: String,
|
init(gamepath: String = "",
|
||||||
inputids: [String] = [],
|
inputids: [String] = [],
|
||||||
|
inputDSUServers: [String] = [],
|
||||||
debuglogs: Bool = false,
|
debuglogs: Bool = false,
|
||||||
tracelogs: Bool = false,
|
tracelogs: Bool = false,
|
||||||
listinputids: Bool = false,
|
listinputids: Bool = false,
|
||||||
|
@ -111,10 +240,11 @@ class Ryujinx : ObservableObject {
|
||||||
disablevsync: Bool = false,
|
disablevsync: Bool = false,
|
||||||
language: SystemLanguage = .americanEnglish,
|
language: SystemLanguage = .americanEnglish,
|
||||||
regioncode: SystemRegionCode = .usa,
|
regioncode: SystemRegionCode = .usa,
|
||||||
handHeldController: Bool = false
|
handHeldController: Bool = false,
|
||||||
) {
|
) {
|
||||||
self.gamepath = gamepath
|
self.gamepath = gamepath
|
||||||
self.inputids = inputids
|
self.inputids = inputids
|
||||||
|
self.inputDSUServers = inputDSUServers
|
||||||
self.debuglogs = debuglogs
|
self.debuglogs = debuglogs
|
||||||
self.tracelogs = tracelogs
|
self.tracelogs = tracelogs
|
||||||
self.listinputids = listinputids
|
self.listinputids = listinputids
|
||||||
|
@ -139,17 +269,71 @@ class Ryujinx : ObservableObject {
|
||||||
self.regioncode = regioncode
|
self.regioncode = regioncode
|
||||||
self.handHeldController = handHeldController
|
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 {
|
guard !isRunning else {
|
||||||
throw RyujinxError.alreadyRunning
|
throw RyujinxError.alreadyRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
self.config = config
|
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
|
isRunning = true
|
||||||
|
|
||||||
|
@ -169,7 +353,9 @@ class Ryujinx : ObservableObject {
|
||||||
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
|
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
|
||||||
|
|
||||||
if result != 0 {
|
if result != 0 {
|
||||||
self.isRunning = false
|
DispatchQueue.main.async {
|
||||||
|
self.isRunning = false
|
||||||
|
}
|
||||||
if let accessing, accessing {
|
if let accessing, accessing {
|
||||||
url!.stopAccessingSecurityScopedResource()
|
url!.stopAccessingSecurityScopedResource()
|
||||||
}
|
}
|
||||||
|
@ -178,14 +364,100 @@ class Ryujinx : ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
self.isRunning = false
|
DispatchQueue.main.async {
|
||||||
Self.log("Emulation failed to start: \(error)")
|
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
|
return nil
|
||||||
thread.name = "MeloNX"
|
|
||||||
thread.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -196,15 +468,12 @@ class Ryujinx : ObservableObject {
|
||||||
|
|
||||||
isRunning = false
|
isRunning = false
|
||||||
|
|
||||||
|
UserDefaults.standard.set(false, forKey: "lockInApp")
|
||||||
|
|
||||||
self.emulationUIView = nil
|
self.emulationUIView = nil
|
||||||
self.metalLayer = nil
|
self.metalLayer = nil
|
||||||
|
|
||||||
stop_emulation()
|
|
||||||
thread.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
var running: Bool {
|
|
||||||
return isRunning
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -218,7 +487,7 @@ class Ryujinx : ObservableObject {
|
||||||
do {
|
do {
|
||||||
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to create roms directory: \(error)")
|
// print("Failed to create roms directory: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var games: [Game] = []
|
var games: [Game] = []
|
||||||
|
@ -243,19 +512,18 @@ class Ryujinx : ObservableObject {
|
||||||
|
|
||||||
games.append(game)
|
games.append(game)
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
// print(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return games
|
return games
|
||||||
} catch {
|
} catch {
|
||||||
print("Error loading games from roms folder: \(error)")
|
// print("Error loading games from roms folder: \(error)")
|
||||||
return games
|
return games
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildCommandLineArgs(from config: Configuration) -> [String] {
|
func buildCommandLineArgs(from config: Arguments) -> [String] {
|
||||||
var args: [String] = []
|
var args: [String] = []
|
||||||
|
|
||||||
// Add the game path
|
// Add the game path
|
||||||
|
@ -267,18 +535,41 @@ class Ryujinx : ObservableObject {
|
||||||
|
|
||||||
args.append(contentsOf: ["--memory-manager-mode", config.memoryManagerMode])
|
args.append(contentsOf: ["--memory-manager-mode", config.memoryManagerMode])
|
||||||
|
|
||||||
// args.append(contentsOf: ["--exclusive-fullscreen", String(true)])
|
args.append(contentsOf: ["--exclusive-fullscreen", String(true)])
|
||||||
// args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"])
|
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-height", "\(Int(UIScreen.main.bounds.height))"])
|
||||||
// We don't need this. Ryujinx should handle it fine :3
|
// We don't need this. Ryujinx should handle it fine :3
|
||||||
// this also causes crashes in some games :3
|
// this also causes crashes in some games :3
|
||||||
|
|
||||||
|
var model = ""
|
||||||
|
|
||||||
|
var systemInfo = utsname()
|
||||||
|
uname(&systemInfo)
|
||||||
|
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||||
|
model = machineMirror.children.reduce("") { identifier, element in
|
||||||
|
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||||
|
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
args.append(contentsOf: ["--device-model", model])
|
||||||
|
|
||||||
|
args.append(contentsOf: ["--device-display-name", UIDevice.modelName])
|
||||||
|
|
||||||
|
if checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") {
|
||||||
|
args.append("--has-memory-entitlement")
|
||||||
|
}
|
||||||
|
|
||||||
args.append(contentsOf: ["--system-language", config.language.rawValue])
|
args.append(contentsOf: ["--system-language", config.language.rawValue])
|
||||||
|
|
||||||
args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
|
args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
|
||||||
|
|
||||||
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
|
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
|
||||||
|
|
||||||
|
args.append(contentsOf: ["--system-timezone", TimeZone.current.identifier])
|
||||||
|
|
||||||
|
// args.append(contentsOf: ["--system-time-offset", String(TimeZone.current.secondsFromGMT())])
|
||||||
|
|
||||||
|
|
||||||
if config.nintendoinput {
|
if config.nintendoinput {
|
||||||
args.append("--correct-controller")
|
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)
|
args.append(contentsOf: config.additionalArgs)
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
@ -383,7 +684,7 @@ class Ryujinx : ObservableObject {
|
||||||
|
|
||||||
func installFirmware(firmwarePath: String) {
|
func installFirmware(firmwarePath: String) {
|
||||||
guard let cString = firmwarePath.cString(using: .utf8) else {
|
guard let cString = firmwarePath.cString(using: .utf8) else {
|
||||||
print("Invalid firmware path")
|
// print("Invalid firmware path")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,12 +700,12 @@ class Ryujinx : ObservableObject {
|
||||||
guard let titleIdCString = titleId.cString(using: .utf8),
|
guard let titleIdCString = titleId.cString(using: .utf8),
|
||||||
let pathCString = path.cString(using: .utf8)
|
let pathCString = path.cString(using: .utf8)
|
||||||
else {
|
else {
|
||||||
print("Invalid path")
|
// print("Invalid path")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
let listPointer = get_dlc_nca_list(titleIdCString, pathCString)
|
let listPointer = get_dlc_nca_list(titleIdCString, pathCString)
|
||||||
print("DLC parcing success: \(listPointer.success)")
|
// print("DLC parcing success: \(listPointer.success)")
|
||||||
guard listPointer.success else { return [] }
|
guard listPointer.success else { return [] }
|
||||||
|
|
||||||
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
|
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
|
||||||
|
@ -456,7 +757,7 @@ class Ryujinx : ObservableObject {
|
||||||
let guid = generateGamepadId(joystickIndex: i)
|
let guid = generateGamepadId(joystickIndex: i)
|
||||||
let name = String(cString: SDL_GameControllerName(controller))
|
let name = String(cString: SDL_GameControllerName(controller))
|
||||||
|
|
||||||
print("Controller \(i): \(name), GUID: \(guid ?? "")")
|
// print("Controller \(i): \(name), GUID: \(guid ?? "")")
|
||||||
|
|
||||||
guard let guid else {
|
guard let guid else {
|
||||||
SDL_GameControllerClose(controller)
|
SDL_GameControllerClose(controller)
|
||||||
|
@ -487,33 +788,163 @@ class Ryujinx : ObservableObject {
|
||||||
do {
|
do {
|
||||||
if fileManager.fileExists(atPath: registeredFolder) {
|
if fileManager.fileExists(atPath: registeredFolder) {
|
||||||
try fileManager.removeItem(atPath: registeredFolder)
|
try fileManager.removeItem(atPath: registeredFolder)
|
||||||
print("Folder removed successfully.")
|
// print("Folder removed successfully.")
|
||||||
let version = fetchFirmwareVersion()
|
let version = fetchFirmwareVersion()
|
||||||
|
|
||||||
if version.isEmpty {
|
if version.isEmpty {
|
||||||
self.firmwareversion = "0"
|
self.firmwareversion = "0"
|
||||||
} else {
|
} else {
|
||||||
print("Firmware eeeeee \(version)")
|
// print("Firmware eeeeee \(version)")
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
print("Folder does not exist.")
|
// print("Folder does not exist.")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Error removing folder: \(error)")
|
// print("Error removing folder: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static func log(_ message: String) {
|
static func log(_ message: String) {
|
||||||
print("[Ryujinx] \(message)")
|
// print("[Ryujinx] \(message)")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateOrientation() -> Bool {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let window = windowScene.windows.first {
|
||||||
|
return (window.bounds.size.height > window.bounds.size.width)
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func ryuIsJITEnabled() {
|
func ryuIsJITEnabled() {
|
||||||
jitenabled = isJITEnabled()
|
jitenabled = isJITEnabled()
|
||||||
print("JIT \(jitenabled)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public extension UIDevice {
|
||||||
|
static let modelName: String = {
|
||||||
|
var systemInfo = utsname()
|
||||||
|
uname(&systemInfo)
|
||||||
|
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||||
|
let identifier = machineMirror.children.reduce("") { identifier, element in
|
||||||
|
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||||
|
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity
|
||||||
|
#if os(iOS)
|
||||||
|
switch identifier {
|
||||||
|
case "iPod5,1": return "iPod touch (5th generation)"
|
||||||
|
case "iPod7,1": return "iPod touch (6th generation)"
|
||||||
|
case "iPod9,1": return "iPod touch (7th generation)"
|
||||||
|
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4"
|
||||||
|
case "iPhone4,1": return "iPhone 4s"
|
||||||
|
case "iPhone5,1", "iPhone5,2": return "iPhone 5"
|
||||||
|
case "iPhone5,3", "iPhone5,4": return "iPhone 5c"
|
||||||
|
case "iPhone6,1", "iPhone6,2": return "iPhone 5s"
|
||||||
|
case "iPhone7,2": return "iPhone 6"
|
||||||
|
case "iPhone7,1": return "iPhone 6 Plus"
|
||||||
|
case "iPhone8,1": return "iPhone 6s"
|
||||||
|
case "iPhone8,2": return "iPhone 6s Plus"
|
||||||
|
case "iPhone9,1", "iPhone9,3": return "iPhone 7"
|
||||||
|
case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus"
|
||||||
|
case "iPhone10,1", "iPhone10,4": return "iPhone 8"
|
||||||
|
case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus"
|
||||||
|
case "iPhone10,3", "iPhone10,6": return "iPhone X"
|
||||||
|
case "iPhone11,2": return "iPhone XS"
|
||||||
|
case "iPhone11,4", "iPhone11,6": return "iPhone XS Max"
|
||||||
|
case "iPhone11,8": return "iPhone XR"
|
||||||
|
case "iPhone12,1": return "iPhone 11"
|
||||||
|
case "iPhone12,3": return "iPhone 11 Pro"
|
||||||
|
case "iPhone12,5": return "iPhone 11 Pro Max"
|
||||||
|
case "iPhone13,1": return "iPhone 12 mini"
|
||||||
|
case "iPhone13,2": return "iPhone 12"
|
||||||
|
case "iPhone13,3": return "iPhone 12 Pro"
|
||||||
|
case "iPhone13,4": return "iPhone 12 Pro Max"
|
||||||
|
case "iPhone14,4": return "iPhone 13 mini"
|
||||||
|
case "iPhone14,5": return "iPhone 13"
|
||||||
|
case "iPhone14,2": return "iPhone 13 Pro"
|
||||||
|
case "iPhone14,3": return "iPhone 13 Pro Max"
|
||||||
|
case "iPhone14,7": return "iPhone 14"
|
||||||
|
case "iPhone14,8": return "iPhone 14 Plus"
|
||||||
|
case "iPhone15,2": return "iPhone 14 Pro"
|
||||||
|
case "iPhone15,3": return "iPhone 14 Pro Max"
|
||||||
|
case "iPhone15,4": return "iPhone 15"
|
||||||
|
case "iPhone15,5": return "iPhone 15 Plus"
|
||||||
|
case "iPhone16,1": return "iPhone 15 Pro"
|
||||||
|
case "iPhone16,2": return "iPhone 15 Pro Max"
|
||||||
|
case "iPhone17,3": return "iPhone 16"
|
||||||
|
case "iPhone17,4": return "iPhone 16 Plus"
|
||||||
|
case "iPhone17,1": return "iPhone 16 Pro"
|
||||||
|
case "iPhone17,2": return "iPhone 16 Pro Max"
|
||||||
|
case "iPhone17,5": return "iPhone 16e"
|
||||||
|
case "iPhone8,4": return "iPhone SE"
|
||||||
|
case "iPhone12,8": return "iPhone SE (2nd generation)"
|
||||||
|
case "iPhone14,6": return "iPhone SE (3rd generation)"
|
||||||
|
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2"
|
||||||
|
case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)"
|
||||||
|
case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)"
|
||||||
|
case "iPad6,11", "iPad6,12": return "iPad (5th generation)"
|
||||||
|
case "iPad7,5", "iPad7,6": return "iPad (6th generation)"
|
||||||
|
case "iPad7,11", "iPad7,12": return "iPad (7th generation)"
|
||||||
|
case "iPad11,6", "iPad11,7": return "iPad (8th generation)"
|
||||||
|
case "iPad12,1", "iPad12,2": return "iPad (9th generation)"
|
||||||
|
case "iPad13,18", "iPad13,19": return "iPad (10th generation)"
|
||||||
|
case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air"
|
||||||
|
case "iPad5,3", "iPad5,4": return "iPad Air 2"
|
||||||
|
case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)"
|
||||||
|
case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
|
||||||
|
case "iPad13,16", "iPad13,17": return "iPad Air (5th generation)"
|
||||||
|
case "iPad14,8", "iPad14,9": return "iPad Air (11-inch) (M2)"
|
||||||
|
case "iPad14,10", "iPad14,11": return "iPad Air (13-inch) (M2)"
|
||||||
|
case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini"
|
||||||
|
case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2"
|
||||||
|
case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3"
|
||||||
|
case "iPad5,1", "iPad5,2": return "iPad mini 4"
|
||||||
|
case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)"
|
||||||
|
case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)"
|
||||||
|
case "iPad16,1", "iPad16,2": return "iPad mini (A17 Pro)"
|
||||||
|
case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)"
|
||||||
|
case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)"
|
||||||
|
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)"
|
||||||
|
case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)"
|
||||||
|
case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)"
|
||||||
|
case "iPad14,3", "iPad14,4": return "iPad Pro (11-inch) (4th generation)"
|
||||||
|
case "iPad16,3", "iPad16,4": return "iPad Pro (11-inch) (M4)"
|
||||||
|
case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)"
|
||||||
|
case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)"
|
||||||
|
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)"
|
||||||
|
case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)"
|
||||||
|
case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)"
|
||||||
|
case "iPad14,5", "iPad14,6": return "iPad Pro (12.9-inch) (6th generation)"
|
||||||
|
case "iPad16,5", "iPad16,6": return "iPad Pro (13-inch) (M4)"
|
||||||
|
case "AppleTV5,3": return "Apple TV"
|
||||||
|
case "AppleTV6,2": return "Apple TV 4K"
|
||||||
|
case "AudioAccessory1,1": return "HomePod"
|
||||||
|
case "AudioAccessory5,1": return "HomePod mini"
|
||||||
|
case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
|
||||||
|
default: return identifier
|
||||||
|
}
|
||||||
|
#elseif os(tvOS)
|
||||||
|
switch identifier {
|
||||||
|
case "AppleTV5,3": return "Apple TV 4"
|
||||||
|
case "AppleTV6,2", "AppleTV11,1", "AppleTV14,1": return "Apple TV 4K"
|
||||||
|
case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))"
|
||||||
|
default: return identifier
|
||||||
|
}
|
||||||
|
#elseif os(visionOS)
|
||||||
|
switch identifier {
|
||||||
|
case "RealityDevice14,1": return "Apple Vision Pro"
|
||||||
|
default: return identifier
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapToDevice(identifier: identifier)
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ struct LaunchGameIntentDef: AppIntent {
|
||||||
let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName))
|
let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName))
|
||||||
|
|
||||||
let urlString = "melonx://game?name=\(name ?? gameName)"
|
let urlString = "melonx://game?name=\(name ?? gameName)"
|
||||||
print(urlString)
|
// print(urlString)
|
||||||
if let url = URL(string: urlString) {
|
if let url = URL(string: urlString) {
|
||||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ public struct Game: Identifiable, Equatable, Hashable {
|
||||||
|
|
||||||
gameTemp.icon = UIImage(data: imageData)
|
gameTemp.icon = UIImage(data: imageData)
|
||||||
} else {
|
} else {
|
||||||
print("Invalid image size.")
|
// print("Invalid image size.")
|
||||||
}
|
}
|
||||||
return gameTemp
|
return gameTemp
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ public struct Game: Identifiable, Equatable, Hashable {
|
||||||
|
|
||||||
let imageSize = Int(gameInfoValue.ImageSize)
|
let imageSize = Int(gameInfoValue.ImageSize)
|
||||||
guard imageSize > 0, imageSize <= 1024 * 1024 else {
|
guard imageSize > 0, imageSize <= 1024 * 1024 else {
|
||||||
print("Invalid image size.")
|
// print("Invalid image size.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
27
src/MeloNX/MeloNX/App/Models/ToggleButtonsState.swift
Normal 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)
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
125
src/MeloNX/MeloNX/App/Views/Main/Elements/FileImporter.swift
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,7 +58,7 @@ public class Air {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func didConnect(sender: NSNotification) {
|
@objc func didConnect(sender: NSNotification) {
|
||||||
print("AirKit - Connect")
|
// print("AirKit - Connect")
|
||||||
self.connected = true
|
self.connected = true
|
||||||
guard let screen: UIScreen = sender.object as? UIScreen else { return }
|
guard let screen: UIScreen = sender.object as? UIScreen else { return }
|
||||||
add(screen: screen) { success in
|
add(screen: screen) { success in
|
||||||
|
@ -69,35 +69,35 @@ public class Air {
|
||||||
|
|
||||||
func add(screen: UIScreen, completion: @escaping (Bool) -> ()) {
|
func add(screen: UIScreen, completion: @escaping (Bool) -> ()) {
|
||||||
|
|
||||||
print("AirKit - Add Screen")
|
// print("AirKit - Add Screen")
|
||||||
|
|
||||||
airScreen = screen
|
airScreen = screen
|
||||||
|
|
||||||
airWindow = UIWindow(frame: airScreen!.bounds)
|
airWindow = UIWindow(frame: airScreen!.bounds)
|
||||||
|
|
||||||
guard let viewController: UIViewController = hostingController else {
|
guard let viewController: UIViewController = hostingController else {
|
||||||
print("AirKit - Add - Failed: Hosting Controller Not Found")
|
// print("AirKit - Add - Failed: Hosting Controller Not Found")
|
||||||
completion(false)
|
completion(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
findWindowScene(for: airScreen!) { windowScene in
|
findWindowScene(for: airScreen!) { windowScene in
|
||||||
guard let airWindowScene: UIWindowScene = windowScene else {
|
guard let airWindowScene: UIWindowScene = windowScene else {
|
||||||
print("AirKit - Add - Failed: Window Scene Not Found")
|
// print("AirKit - Add - Failed: Window Scene Not Found")
|
||||||
completion(false)
|
completion(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.airWindow?.rootViewController = viewController
|
self.airWindow?.rootViewController = viewController
|
||||||
self.airWindow?.windowScene = airWindowScene
|
self.airWindow?.windowScene = airWindowScene
|
||||||
self.airWindow?.isHidden = false
|
self.airWindow?.isHidden = false
|
||||||
print("AirKit - Add Screen - Done")
|
// print("AirKit - Add Screen - Done")
|
||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func findWindowScene(for screen: UIScreen, shouldRecurse: Bool = true, completion: @escaping (UIWindowScene?) -> ()) {
|
func findWindowScene(for screen: UIScreen, shouldRecurse: Bool = true, completion: @escaping (UIWindowScene?) -> ()) {
|
||||||
print("AirKit - Find Window Scene")
|
// print("AirKit - Find Window Scene")
|
||||||
var matchingWindowScene: UIWindowScene? = nil
|
var matchingWindowScene: UIWindowScene? = nil
|
||||||
let scenes = UIApplication.shared.connectedScenes
|
let scenes = UIApplication.shared.connectedScenes
|
||||||
for scene in scenes {
|
for scene in scenes {
|
||||||
|
@ -120,23 +120,23 @@ public class Air {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func didDisconnect() {
|
@objc func didDisconnect() {
|
||||||
print("AirKit - Disconnect")
|
// print("AirKit - Disconnect")
|
||||||
remove()
|
remove()
|
||||||
connected = false
|
connected = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove() {
|
func remove() {
|
||||||
print("AirKit - Remove")
|
// print("AirKit - Remove")
|
||||||
airWindow = nil
|
airWindow = nil
|
||||||
airScreen = nil
|
airScreen = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func didBecomeActive() {
|
@objc func didBecomeActive() {
|
||||||
print("AirKit - App Active")
|
// print("AirKit - App Active")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func willResignActive() {
|
@objc func willResignActive() {
|
||||||
print("AirKit - App Inactive")
|
// print("AirKit - App Inactive")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import SwiftUI
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
||||||
func airPlay() -> some View {
|
func airPlay() -> some View {
|
||||||
print("AirKit - airPlay")
|
// print("AirKit - airPlay")
|
||||||
Air.play(AnyView(self))
|
Air.play(AnyView(self))
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,6 @@ class Haptics {
|
||||||
private init() { }
|
private init() { }
|
||||||
|
|
||||||
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
|
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||||
print("haptics")
|
|
||||||
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
|
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,11 +14,20 @@ struct EmulationView: View {
|
||||||
@AppStorage("showScreenShotButton") var ssb: Bool = false
|
@AppStorage("showScreenShotButton") var ssb: Bool = false
|
||||||
@AppStorage("showlogsgame") var showlogsgame: 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 isPresentedThree: Bool = false
|
||||||
@State var isAirplaying = Air.shared.connected
|
@State var isAirplaying = Air.shared.connected
|
||||||
@Binding var startgame: Game?
|
@Binding var startgame: Game?
|
||||||
|
|
||||||
@Environment(\.scenePhase) var scenePhase
|
@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 {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if isAirplaying {
|
if isAirplaying {
|
||||||
|
@ -26,8 +35,13 @@ struct EmulationView: View {
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
Air.play(AnyView(MetalView().ignoresSafeArea()))
|
Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color.black
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.allowsHitTesting(false)
|
||||||
} else {
|
} else {
|
||||||
MetalView() // The Emulation View
|
MetalView() // The Emulation View
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
@ -38,6 +52,8 @@ struct EmulationView: View {
|
||||||
|
|
||||||
if isVCA {
|
if isVCA {
|
||||||
ControllerView() // Virtual Controller
|
ControllerView() // Virtual Controller
|
||||||
|
.opacity(controllerOpacity)
|
||||||
|
.allowsHitTesting(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
|
@ -62,38 +78,98 @@ struct EmulationView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if ssb {
|
if ssb {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
Button {
|
Menu {
|
||||||
if let screenshot = Ryujinx.shared.emulationUIView?.screenshot() {
|
|
||||||
UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
|
/*
|
||||||
|
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: {
|
} 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()
|
.padding()
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
LocationManager.sharedInstance.startUpdatingLocation()
|
||||||
Air.shared.connectionCallbacks.append { cool in
|
Air.shared.connectionCallbacks.append { cool in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
isAirplaying = cool
|
isAirplaying = cool
|
||||||
|
// print(cool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterCallback("exit-emulation") { cool in
|
||||||
|
DispatchQueue.main.async {
|
||||||
print(cool)
|
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()
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() ?? [:]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,9 +9,9 @@ import MetalKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class MeloMTKView: MTKView {
|
class MeloMTKView: MTKView {
|
||||||
|
|
||||||
private var activeTouches: [UITouch] = []
|
private var activeTouches: [UITouch] = []
|
||||||
private var ignoredTouches: Set<UITouch> = []
|
private var ignoredTouches: Set<UITouch> = []
|
||||||
|
private var touchIndexMap: [UITouch: Int] = [:]
|
||||||
|
|
||||||
private let baseWidth: CGFloat = 1280
|
private let baseWidth: CGFloat = 1280
|
||||||
private let baseHeight: CGFloat = 720
|
private let baseHeight: CGFloat = 720
|
||||||
|
@ -84,23 +84,34 @@ class MeloMTKView: MTKView {
|
||||||
return CGPoint(x: scaledX, y: scaledY)
|
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?) {
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
super.touchesBegan(touches, with: event)
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
|
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
|
||||||
|
guard !disabled else { return }
|
||||||
|
|
||||||
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
||||||
|
|
||||||
for touch in touches {
|
for touch in touches {
|
||||||
let location = touch.location(in: self)
|
let location = touch.location(in: self)
|
||||||
if scaleToTargetResolution(location) == nil {
|
guard let scaledLocation = scaleToTargetResolution(location) else {
|
||||||
ignoredTouches.insert(touch)
|
ignoredTouches.insert(touch)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let index = getNextAvailableIndex()
|
||||||
|
touchIndexMap[touch] = index
|
||||||
activeTouches.append(touch)
|
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))
|
touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,19 +119,30 @@ class MeloMTKView: MTKView {
|
||||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
super.touchesEnded(touches, with: event)
|
super.touchesEnded(touches, with: event)
|
||||||
|
|
||||||
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
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 {
|
for touch in touches {
|
||||||
if ignoredTouches.contains(touch) {
|
if ignoredTouches.remove(touch) != nil {
|
||||||
ignoredTouches.remove(touch)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if let index = activeTouches.firstIndex(of: touch) {
|
if let touchIndex = touchIndexMap[touch] {
|
||||||
activeTouches.remove(at: index)
|
touch_ended(Int32(touchIndex))
|
||||||
|
|
||||||
print("Touch ended for index \(index)")
|
if let arrayIndex = activeTouches.firstIndex(of: touch) {
|
||||||
touch_ended(Int32(index))
|
activeTouches.remove(at: arrayIndex)
|
||||||
|
}
|
||||||
|
touchIndexMap.removeValue(forKey: touch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,6 +150,9 @@ class MeloMTKView: MTKView {
|
||||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
super.touchesMoved(touches, with: event)
|
super.touchesMoved(touches, with: event)
|
||||||
|
|
||||||
|
let disabled = UserDefaults.standard.bool(forKey: "disableTouch")
|
||||||
|
guard !disabled else { return }
|
||||||
|
|
||||||
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
||||||
|
|
||||||
for touch in touches {
|
for touch in touches {
|
||||||
|
@ -135,20 +160,36 @@ class MeloMTKView: MTKView {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = touch.location(in: self)
|
guard let touchIndex = touchIndexMap[touch] else {
|
||||||
guard let scaledLocation = scaleToTargetResolution(location) else {
|
|
||||||
if let index = activeTouches.firstIndex(of: touch) {
|
|
||||||
activeTouches.remove(at: index)
|
|
||||||
print("Touch left active area, removed index \(index)")
|
|
||||||
touch_ended(Int32(index))
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if let index = activeTouches.firstIndex(of: touch) {
|
let location = touch.location(in: self)
|
||||||
print("Touch moved to: \(scaledLocation)")
|
guard let scaledLocation = scaleToTargetResolution(location) else {
|
||||||
touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,6 +10,7 @@ import GameController
|
||||||
import Darwin
|
import Darwin
|
||||||
import UIKit
|
import UIKit
|
||||||
import MetalKit
|
import MetalKit
|
||||||
|
import CoreLocation
|
||||||
|
|
||||||
struct MoltenVKSettings: Codable, Hashable {
|
struct MoltenVKSettings: Codable, Hashable {
|
||||||
let string: String
|
let string: String
|
||||||
|
@ -31,19 +32,25 @@ struct ContentView: View {
|
||||||
@AppStorage("isVirtualController") var isVCA: Bool = true
|
@AppStorage("isVirtualController") var isVCA: Bool = true
|
||||||
|
|
||||||
// Settings and Configuration
|
// 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]
|
@State var settings: [MoltenVKSettings]
|
||||||
@AppStorage("useTrollStore") var useTrollStore: Bool = false
|
|
||||||
|
|
||||||
// JIT
|
// JIT
|
||||||
|
@AppStorage("useTrollStore") var useTrollStore: Bool = false
|
||||||
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
|
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
|
||||||
|
@AppStorage("stikJIT") var stikJIT: Bool = false
|
||||||
|
|
||||||
// Other Configuration
|
// Other Configuration
|
||||||
@State var isMK8: Bool = false
|
@State var isMK8: Bool = false
|
||||||
@AppStorage("quit") var quit: Bool = false
|
@AppStorage("quit") var quit: Bool = false
|
||||||
@State var quits: Bool = false
|
@State var quits: Bool = false
|
||||||
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
|
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
|
||||||
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true
|
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
|
||||||
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
||||||
|
|
||||||
// Loading Animation
|
// Loading Animation
|
||||||
|
@ -53,6 +60,9 @@ struct ContentView: View {
|
||||||
private let animationDuration: Double = 1.0
|
private let animationDuration: Double = 1.0
|
||||||
@State private var isAnimating = false
|
@State private var isAnimating = false
|
||||||
@State var isLoading = true
|
@State var isLoading = true
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - CORE
|
||||||
@StateObject var ryujinx = Ryujinx.shared
|
@StateObject var ryujinx = Ryujinx.shared
|
||||||
|
|
||||||
// MARK: - SDL
|
// MARK: - SDL
|
||||||
|
@ -60,14 +70,6 @@ struct ContentView: View {
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
init() {
|
init() {
|
||||||
var defaultConfig = loadSettings()
|
|
||||||
if defaultConfig == nil {
|
|
||||||
saveSettings(config: .init(gamepath: ""))
|
|
||||||
defaultConfig = loadSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
_config = State(initialValue: defaultConfig!)
|
|
||||||
|
|
||||||
let defaultSettings: [MoltenVKSettings] = [
|
let defaultSettings: [MoltenVKSettings] = [
|
||||||
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
|
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_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)
|
_settings = State(initialValue: defaultSettings)
|
||||||
|
|
||||||
print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
|
|
||||||
|
|
||||||
initializeSDL()
|
initializeSDL()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ struct ContentView: View {
|
||||||
|
|
||||||
private var jitErrorView: some View {
|
private var jitErrorView: some View {
|
||||||
Text("")
|
Text("")
|
||||||
.sheet(isPresented:Binding(
|
.fullScreenCover(isPresented:Binding(
|
||||||
get: { !ryujinx.jitenabled },
|
get: { !ryujinx.jitenabled },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
ryujinx.jitenabled = newValue
|
ryujinx.jitenabled = newValue
|
||||||
|
@ -130,14 +130,12 @@ struct ContentView: View {
|
||||||
JITPopover() {
|
JITPopover() {
|
||||||
ryujinx.jitenabled = false
|
ryujinx.jitenabled = false
|
||||||
}
|
}
|
||||||
.interactiveDismissDisabled()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mainMenuView: some View {
|
private var mainMenuView: some View {
|
||||||
MainTabView(
|
MainTabView(
|
||||||
startemu: $game,
|
startemu: $game,
|
||||||
config: $config,
|
|
||||||
MVKconfig: $settings,
|
MVKconfig: $settings,
|
||||||
controllersList: $controllersList,
|
controllersList: $controllersList,
|
||||||
currentControllers: $currentControllers,
|
currentControllers: $currentControllers,
|
||||||
|
@ -152,24 +150,18 @@ struct ContentView: View {
|
||||||
refreshControllersList()
|
refreshControllersList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UserDefaults.standard.set(false, forKey: "lockInApp")
|
||||||
print(MTLHud.shared.isEnabled)
|
|
||||||
|
|
||||||
initControllerObservers()
|
initControllerObservers()
|
||||||
|
|
||||||
Air.play(AnyView(
|
Air.play(AnyView(
|
||||||
VStack {
|
ControllerListView(game: $game)
|
||||||
Image(systemName: "gamecontroller")
|
|
||||||
.font(.system(size: 300))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
|
|
||||||
Text("Select Game")
|
|
||||||
.font(.system(size: 150))
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
))
|
))
|
||||||
|
|
||||||
|
refreshControllersList()
|
||||||
|
|
||||||
|
ryujinx.addGames()
|
||||||
|
|
||||||
checkJitStatus()
|
checkJitStatus()
|
||||||
}
|
}
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
|
@ -288,7 +280,6 @@ struct ContentView: View {
|
||||||
queue: .main
|
queue: .main
|
||||||
) { notification in
|
) { notification in
|
||||||
if let controller = notification.object as? GCController {
|
if let controller = notification.object as? GCController {
|
||||||
print("Controller connected: \(controller.productCategory)")
|
|
||||||
nativeControllers[controller] = .init(controller)
|
nativeControllers[controller] = .init(controller)
|
||||||
refreshControllersList()
|
refreshControllersList()
|
||||||
}
|
}
|
||||||
|
@ -300,7 +291,8 @@ struct ContentView: View {
|
||||||
queue: .main
|
queue: .main
|
||||||
) { notification in
|
) { notification in
|
||||||
if let controller = notification.object as? GCController {
|
if let controller = notification.object as? GCController {
|
||||||
print("Controller disconnected: \(controller.productCategory)")
|
currentControllers = []
|
||||||
|
controllersList = []
|
||||||
nativeControllers[controller]?.cleanup()
|
nativeControllers[controller]?.cleanup()
|
||||||
nativeControllers[controller] = nil
|
nativeControllers[controller] = nil
|
||||||
refreshControllersList()
|
refreshControllersList()
|
||||||
|
@ -317,6 +309,9 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshControllersList() {
|
private func refreshControllersList() {
|
||||||
|
currentControllers = []
|
||||||
|
controllersList = []
|
||||||
|
|
||||||
controllersList = ryujinx.getConnectedControllers()
|
controllersList = ryujinx.getConnectedControllers()
|
||||||
|
|
||||||
if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
|
if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
|
||||||
|
@ -326,8 +321,6 @@ struct ContentView: View {
|
||||||
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
|
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
|
||||||
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
|
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
|
||||||
|
|
||||||
currentControllers = []
|
|
||||||
|
|
||||||
if controllersList.count == 1 {
|
if controllersList.count == 1 {
|
||||||
currentControllers.append(controllersList[0])
|
currentControllers.append(controllersList[0])
|
||||||
} else if (controllersList.count - 1) >= 1 {
|
} else if (controllersList.count - 1) >= 1 {
|
||||||
|
@ -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) {
|
private func start(displayid: UInt32) {
|
||||||
guard let game else { return }
|
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.gamepath = game.fileURL.path
|
||||||
config.inputids = Array(Set(currentControllers.map(\.id)))
|
config.inputids = Array(Set(currentControllers.map(\.id)))
|
||||||
|
|
||||||
configureEnvironmentVariables()
|
configureEnvironmentVariables()
|
||||||
|
|
||||||
if config.inputids.isEmpty {
|
registerMotionForMatchingControllers()
|
||||||
config.inputids.append("0")
|
|
||||||
|
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 {
|
do {
|
||||||
try ryujinx.start(with: config)
|
try ryujinx.start(with: config)
|
||||||
} catch {
|
} catch {
|
||||||
print("Error: \(error.localizedDescription)")
|
// print("Error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,7 +390,7 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if syncqsubmits {
|
if syncqsubmits {
|
||||||
setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "2", 1)
|
setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "1", 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,13 +402,25 @@ struct ContentView: View {
|
||||||
|
|
||||||
private func checkJitStatus() {
|
private func checkJitStatus() {
|
||||||
ryujinx.ryuIsJITEnabled()
|
ryujinx.ryuIsJITEnabled()
|
||||||
|
if jitStreamerEB {
|
||||||
|
jitStreamerEB = false // byee jitstreamer eb
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if !ryujinx.jitenabled {
|
if !ryujinx.jitenabled {
|
||||||
if useTrollStore {
|
if useTrollStore {
|
||||||
askForJIT()
|
askForJIT()
|
||||||
|
} else if stikJIT {
|
||||||
|
enableJITStik()
|
||||||
} else if jitStreamerEB {
|
} else if jitStreamerEB {
|
||||||
enableJITEB()
|
enableJITEB()
|
||||||
} else {
|
} else {
|
||||||
print("no JIT")
|
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) {
|
private func handleDeepLink(_ url: URL) {
|
||||||
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
components.host == "game" {
|
components.host == "game" {
|
||||||
|
|
||||||
|
refreshControllersList()
|
||||||
|
|
||||||
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
||||||
game = ryujinx.games.first(where: { $0.titleId == text })
|
game = ryujinx.games.first(where: { $0.titleId == text })
|
||||||
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
||||||
struct GameInfoSheet: View {
|
struct GameInfoSheet: View {
|
||||||
let game: Game
|
let game: Game
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
iOSNav {
|
||||||
|
@ -44,7 +44,7 @@ struct GameInfoSheet: View {
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
Text(game.developer)
|
Text(game.developer)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 3)
|
.padding(.vertical, 3)
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ struct GameInfoSheet: View {
|
||||||
Text("**Version**")
|
Text("**Version**")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(game.version)
|
Text(game.version)
|
||||||
.foregroundStyle(Color.secondary)
|
.foregroundColor(Color.secondary)
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Text("**Title ID**")
|
Text("**Title ID**")
|
||||||
|
@ -69,36 +69,36 @@ struct GameInfoSheet: View {
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(game.titleId)
|
Text(game.titleId)
|
||||||
.foregroundStyle(Color.secondary)
|
.foregroundColor(Color.secondary)
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Text("**Game Size**")
|
Text("**Game Size**")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes")
|
Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes")
|
||||||
.foregroundStyle(Color.secondary)
|
.foregroundColor(Color.secondary)
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Text("**File Type**")
|
Text("**File Type**")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(getFileType(game.fileURL))
|
Text(getFileType(game.fileURL))
|
||||||
.foregroundStyle(Color.secondary)
|
.foregroundColor(Color.secondary)
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("**Game URL**")
|
Text("**Game URL**")
|
||||||
Text(trimGameURL(game.fileURL))
|
Text(trimGameURL(game.fileURL))
|
||||||
.foregroundStyle(Color.secondary)
|
.foregroundColor(Color.secondary)
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Information")
|
Text("Information")
|
||||||
}
|
}
|
||||||
.headerProminence(.increased)
|
// .headerProminence(.increased)
|
||||||
}
|
}
|
||||||
.navigationTitle(game.titleName)
|
.navigationTitle(game.titleName)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
Button("Done") {
|
Button("Dismiss") {
|
||||||
dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ struct GameInfoSheet: View {
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Error getting file size: \(error)")
|
// print("Error getting file size: \(error)")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
1267
src/MeloNX/MeloNX/App/Views/Main/UI/GamesList/GameListView.swift
Normal file
|
@ -9,7 +9,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct JITPopover: View {
|
struct JITPopover: View {
|
||||||
var onJITEnabled: () -> Void
|
var onJITEnabled: () -> Void
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
@State var isJIT: Bool = false
|
@State var isJIT: Bool = false
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -35,7 +35,7 @@ struct JITPopover: View {
|
||||||
|
|
||||||
|
|
||||||
if isJIT {
|
if isJIT {
|
||||||
dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
onJITEnabled()
|
onJITEnabled()
|
||||||
|
|
||||||
Ryujinx.shared.ryuIsJITEnabled()
|
Ryujinx.shared.ryuIsJITEnabled()
|
66
src/MeloNX/MeloNX/App/Views/Main/UI/Logging/Logs.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
1669
src/MeloNX/MeloNX/App/Views/Main/UI/SettingsView/SettingsView.swift
Normal file
|
@ -11,7 +11,6 @@ import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct MainTabView: View {
|
struct MainTabView: View {
|
||||||
@Binding var startemu: Game?
|
@Binding var startemu: Game?
|
||||||
@Binding var config: Ryujinx.Configuration
|
|
||||||
@Binding var MVKconfig: [MoltenVKSettings]
|
@Binding var MVKconfig: [MoltenVKSettings]
|
||||||
@Binding var controllersList: [Controller]
|
@Binding var controllersList: [Controller]
|
||||||
@Binding var currentControllers: [Controller]
|
@Binding var currentControllers: [Controller]
|
||||||
|
@ -25,7 +24,8 @@ struct MainTabView: View {
|
||||||
Label("Games", systemImage: "gamecontroller.fill")
|
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 {
|
.tabItem {
|
||||||
Label("Settings", systemImage: "gear")
|
Label("Settings", systemImage: "gear")
|
||||||
}
|
}
|
|
@ -33,18 +33,32 @@ struct MeloNXUpdateSheet: View {
|
||||||
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: {
|
if #available(iOS 15.0, *) {
|
||||||
if let url = URL(string: updateInfo.download_link) {
|
Button(action: {
|
||||||
UIApplication.shared.open(url)
|
if let url = URL(string: updateInfo.download_link) {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Download Now")
|
||||||
|
.font(.title3)
|
||||||
|
.bold()
|
||||||
|
.frame(width: 300, height: 40)
|
||||||
}
|
}
|
||||||
}) {
|
.buttonStyle(.borderedProminent)
|
||||||
Text("Download Now")
|
.frame(alignment: .bottom)
|
||||||
.font(.title3)
|
} else {
|
||||||
.bold()
|
Button(action: {
|
||||||
.frame(width: 300, height: 40)
|
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)
|
.padding(.horizontal)
|
||||||
.navigationTitle("Version \(updateInfo.version_number) Available!")
|
.navigationTitle("Version \(updateInfo.version_number) Available!")
|
|
@ -46,7 +46,7 @@ struct DLCManagerSheet: View {
|
||||||
@Binding var game: Game!
|
@Binding var game: Game!
|
||||||
@State private var isSelectingGameDLC = false
|
@State private var isSelectingGameDLC = false
|
||||||
@State private var dlcs: [DownloadableContentContainer] = []
|
@State private var dlcs: [DownloadableContentContainer] = []
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -66,7 +66,7 @@ struct DLCManagerSheet: View {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Done") {
|
Button("Done") {
|
||||||
dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,27 +127,56 @@ struct DLCManagerSheet: View {
|
||||||
|
|
||||||
|
|
||||||
private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
|
private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
|
||||||
Button {
|
Group {
|
||||||
toggleDLC(dlc)
|
if #available(iOS 15.0, *) {
|
||||||
} label: {
|
Button {
|
||||||
HStack {
|
toggleDLC(dlc)
|
||||||
Text(dlc.filename)
|
} label: {
|
||||||
.foregroundStyle(.primary)
|
HStack {
|
||||||
Spacer()
|
Text(dlc.filename)
|
||||||
Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
|
.foregroundColor(.primary)
|
||||||
.foregroundStyle(dlc.isEnabled ? .primary : .secondary)
|
Spacer()
|
||||||
.imageScale(.large)
|
Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
|
||||||
}
|
.foregroundColor(dlc.isEnabled ? .primary : .secondary)
|
||||||
.contentShape(Rectangle())
|
.imageScale(.large)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.contentShape(Rectangle())
|
||||||
.swipeActions(edge: .trailing) {
|
}
|
||||||
Button(role: .destructive) {
|
.buttonStyle(.plain)
|
||||||
if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
|
.swipeActions(edge: .trailing) {
|
||||||
removeDLC(at: IndexSet(integer: index))
|
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
|
return result
|
||||||
} catch {
|
} catch {
|
||||||
print("Error loading DLCs: \(error)")
|
// print("Error loading DLCs: \(error)")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -300,7 +329,7 @@ extension Array where Element: AnyObject {
|
||||||
|
|
||||||
// MARK: - URL Extension
|
// MARK: - URL Extension
|
||||||
extension URL {
|
extension URL {
|
||||||
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
|
@available(iOS, introduced: 14.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
|
||||||
static var documentsDirectory: URL {
|
static var documentsDirectory: URL {
|
||||||
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
return documentDirectory
|
return documentDirectory
|
|
@ -14,7 +14,7 @@ struct UpdateManagerSheet: View {
|
||||||
@Binding var game: Game?
|
@Binding var game: Game?
|
||||||
@State private var isSelectingGameUpdate = false
|
@State private var isSelectingGameUpdate = false
|
||||||
@State private var jsonURL: URL? = nil
|
@State private var jsonURL: URL? = nil
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
// MARK: - Models
|
// MARK: - Models
|
||||||
class UpdateItem: Identifiable, ObservableObject {
|
class UpdateItem: Identifiable, ObservableObject {
|
||||||
|
@ -51,7 +51,7 @@ struct UpdateManagerSheet: View {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Done") {
|
Button("Done") {
|
||||||
dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,15 +106,26 @@ struct UpdateManagerSheet: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateRow(_ update: UpdateItem) -> some View {
|
private func updateRow(_ update: UpdateItem) -> some View {
|
||||||
|
Group {
|
||||||
|
if #available(iOS 15, *) {
|
||||||
|
updateRowNew(update)
|
||||||
|
} else {
|
||||||
|
updateRowOld(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 15, *)
|
||||||
|
private func updateRowNew(_ update: UpdateItem) -> some View {
|
||||||
Button {
|
Button {
|
||||||
toggleSelection(update)
|
toggleSelection(update)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text(update.filename)
|
Text(update.filename)
|
||||||
.foregroundStyle(.primary)
|
.foregroundColor(.primary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
|
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
|
||||||
.foregroundStyle(update.isSelected ? .primary : .secondary)
|
.foregroundColor(update.isSelected ? .primary : .secondary)
|
||||||
.imageScale(.large)
|
.imageScale(.large)
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
@ -131,6 +142,31 @@ struct UpdateManagerSheet: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateRowOld(_ update: UpdateItem) -> some View {
|
||||||
|
Button {
|
||||||
|
toggleSelection(update)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(update.filename)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
|
||||||
|
.foregroundColor(update.isSelected ? .primary : .secondary)
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
if let index = updates.firstIndex(where: { $0.path == update.path }) {
|
||||||
|
removeUpdate(at: IndexSet(integer: index))
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Functions
|
// MARK: - Functions
|
||||||
private func loadData() {
|
private func loadData() {
|
||||||
guard let game = game else { return }
|
guard let game = game else { return }
|
||||||
|
@ -244,14 +280,13 @@ struct UpdateManagerSheet: View {
|
||||||
print("toggle selection \(update.path)")
|
print("toggle selection \(update.path)")
|
||||||
|
|
||||||
updates = updates.map { item in
|
updates = updates.map { item in
|
||||||
var mutableItem = item
|
item.isSelected = item.path == update.path && !update.isSelected
|
||||||
mutableItem.isSelected = item.path == update.path && !update.isSelected
|
// print(mutableItem.isSelected)
|
||||||
print(mutableItem.isSelected)
|
// print(update.isSelected)
|
||||||
print(update.isSelected)
|
return item
|
||||||
return mutableItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print(updates)
|
// print(updates)
|
||||||
|
|
||||||
saveJSON()
|
saveJSON()
|
||||||
}
|
}
|
|
@ -8,8 +8,15 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
|
||||||
|
extension UIDocumentPickerViewController {
|
||||||
|
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
|
||||||
|
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct MeloNXApp: App {
|
struct MeloNXApp: App {
|
||||||
|
@ -21,38 +28,73 @@ struct MeloNXApp: App {
|
||||||
@State var showOutOfDateSheet = false
|
@State var showOutOfDateSheet = false
|
||||||
@State var updateInfo: LatestVersionResponse? = nil
|
@State var updateInfo: LatestVersionResponse? = nil
|
||||||
|
|
||||||
|
@StateObject var metalHudEnabler = MTLHud.shared
|
||||||
|
|
||||||
@State var finished = false
|
@State var finished = false
|
||||||
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
|
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
|
||||||
|
|
||||||
|
@AppStorage("location-enabled") var locationenabled: Bool = false
|
||||||
|
@AppStorage("checkForUpdate") var checkForUpdate: Bool = true
|
||||||
|
|
||||||
|
@AppStorage("runOnMainThread") var runOnMainThread = false
|
||||||
|
|
||||||
|
@AppStorage("autoJIT") var autoJIT = false
|
||||||
|
|
||||||
|
@State var fourgbiPad = false
|
||||||
|
@AppStorage("4GB iPad") var ignores = false
|
||||||
|
// String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
if finishedStorage {
|
Group {
|
||||||
ContentView()
|
if finishedStorage {
|
||||||
.onAppear {
|
ContentView()
|
||||||
checkLatestVersion()
|
.withFileImporter()
|
||||||
}
|
.onAppear {
|
||||||
.sheet(isPresented: Binding(
|
if checkForUpdate {
|
||||||
get: { showOutOfDateSheet && updateInfo != nil },
|
checkLatestVersion()
|
||||||
set: { newValue in
|
}
|
||||||
if !newValue {
|
|
||||||
showOutOfDateSheet = false
|
print(metalHudEnabler.canMetalHud)
|
||||||
updateInfo = nil
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)) {
|
} else {
|
||||||
if let updateInfo = updateInfo {
|
SetupView(finished: $finished)
|
||||||
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
|
.onChange(of: finished) { newValue in
|
||||||
}
|
withAnimation(.easeOut) {
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SetupView(finished: $finished)
|
|
||||||
.onChange(of: finished) { newValue in
|
|
||||||
withAnimation {
|
|
||||||
withAnimation {
|
|
||||||
finishedStorage = newValue
|
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
|
#if DEBUG
|
||||||
let urlString = "http://192.168.178.116:8000/api/latest_release"
|
let urlString = "http://192.168.178.116:8000/api/latest_release"
|
||||||
#else
|
#else
|
||||||
let urlString = "https://melonx.org/api/latest_release"
|
let urlString = "https://melonx.net/api/latest_release"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
print("Invalid URL")
|
// print("Invalid URL")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
print("Error checking for new version: \(error)")
|
// print("Error checking for new version: \(error)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let data = data else {
|
guard let data = data else {
|
||||||
print("No data received")
|
// print("No data received")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,10 +136,15 @@ struct MeloNXApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to decode response: \(error)")
|
// print("Failed to decode response: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task.resume()
|
task.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func changeAppUI(_ string: String) -> String? {
|
||||||
|
guard let data = Data(base64Encoded: string) else { return nil }
|
||||||
|
return String(data: data, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
|
@ -54,12 +54,17 @@ struct SetupView: View {
|
||||||
) { result in
|
) { result in
|
||||||
handleFirmwareImport(result: result)
|
handleFirmwareImport(result: result)
|
||||||
}
|
}
|
||||||
.alert(alertMessage, isPresented: $showAlert) {
|
.alert(isPresented: $showAlert) {
|
||||||
Button("OK", role: .cancel) {}
|
Alert(title: Text(alertMessage), dismissButton: .default(Text("OK")))
|
||||||
}
|
}
|
||||||
.alert("Skip Setup?", isPresented: $showSkipAlert) {
|
.alert(isPresented: $showSkipAlert) {
|
||||||
Button("Skip", role: .destructive) { finished = true }
|
Alert(
|
||||||
Button("Cancel", role: .cancel) {}
|
title: Text("Skip Setup?"),
|
||||||
|
primaryButton: .destructive(Text("Skip")) {
|
||||||
|
finished = true
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
initialize()
|
initialize()
|
||||||
|
@ -390,7 +395,7 @@ struct SetupView: View {
|
||||||
|
|
||||||
let iconFileName = iconFiles.last else {
|
let iconFileName = iconFiles.last else {
|
||||||
|
|
||||||
print("Could not find icons in bundle")
|
// print("Could not find icons in bundle")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 203 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/darker.png
vendored
Normal file
After Width: | Height: | Size: 203 KiB |
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 24 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/MeloNX 1024.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 24 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/PixelPomeloNX 1024.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 383 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/copycat.png
vendored
Normal file
After Width: | Height: | Size: 383 KiB |
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 269 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/melowonx.png
vendored
Normal file
After Width: | Height: | Size: 269 KiB |
|
@ -10,7 +10,7 @@
|
||||||
</data>
|
</data>
|
||||||
<key>Info.plist</key>
|
<key>Info.plist</key>
|
||||||
<data>
|
<data>
|
||||||
UOH9NuuEcz5NQiQlrM2LNFaG2pI=
|
GYWZONTCP5su4yOAk0d5jCd2K88=
|
||||||
</data>
|
</data>
|
||||||
<key>Modules/module.modulemap</key>
|
<key>Modules/module.modulemap</key>
|
||||||
<data>
|
<data>
|
||||||
|
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
||||||
|
|
BIN
src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/StosJIT.framework/StosJIT
Executable 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>
|