forked from MeloNX/MeloNX
Compare commits
26 Commits
update-vie
...
XC-ios-ht
Author | SHA1 | Date | |
---|---|---|---|
|
0bb5389370 | ||
|
8b81cb39d7 | ||
|
ccdb8b76a8 | ||
37020a5026 | |||
259f6c6872 | |||
|
2b7e29fa21 | ||
8917ebf708 | |||
d326f5a00b | |||
3721a77cc4 | |||
667d54ed2d | |||
1b70bfea8b | |||
33b8571414 | |||
33af004d85 | |||
|
54cb7eb953 | ||
ceab2f0ac8 | |||
7986859398 | |||
c4506da8a1 | |||
9f72c9da10 | |||
ba0c49f545 | |||
80148ac69a | |||
|
7417ddfeef | ||
|
f66590203a | ||
|
f091e6c5ea | ||
|
75a66586b2 | ||
|
3207e1e739 | ||
|
a6b4f2d91f |
@ -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
|
||||||
|
75
README.md
75
README.md
@ -6,11 +6,6 @@
|
|||||||
|
|
||||||
<h1 align="center">MeloNX</h1>
|
<h1 align="center">MeloNX</h1>
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
MeloNX enables Nintendo Switch game emulation on iOS using the Ryujinx iOS code base.
|
MeloNX enables Nintendo Switch game emulation on iOS using the Ryujinx iOS code base.
|
||||||
</p>
|
</p>
|
||||||
@ -28,11 +23,10 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
|
|||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
- MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but may have issues or not work at all.
|
- MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but may have issues or not work at all.
|
||||||
- MeloNX needs Xcode or a Paid Apple Developer Account. SideStore support may come soon (SideStore Side Issue)
|
- MeloNX cannot be Sideloaded normally and requires the use of the following Installation Guide(s).
|
||||||
- MeloNX needs JIT
|
- MeloNX requires JIT
|
||||||
- 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.
|
||||||
- Lowest Supported Device: iPhone XR
|
|
||||||
|
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
@ -54,14 +48,67 @@ MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the
|
|||||||
|
|
||||||
4. **Enable JIT**
|
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)
|
||||||
|
|
||||||
5. **Add Necessary Files**
|
5. **Add Necessary Files**
|
||||||
|
|
||||||
If having Issues installing firmware (Make sure your Keys are installed first)
|
If having Issues installing firmware (Make sure your Keys are installed first)
|
||||||
- If needed, install firmware and keys from **Ryujinx Desktop**.
|
- If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
|
||||||
- Copy the **bis** and **system** folders
|
- Copy the **bis** and **system** folders
|
||||||
|
|
||||||
### Xcode
|
### Free Developer Account (Experimental)
|
||||||
|
|
||||||
|
1. **Sideload MeloNX**
|
||||||
|
- Use [SideStore](https://sidestore.io/) or [AltStore](https://altstore.io/) (**NOT** AltStore PAL).
|
||||||
|
|
||||||
|
2. **Sideload the Entitlement App**
|
||||||
|
- Install [this app](https://github.com/hugeBlack/GetMoreRam/releases/download/nightly/Entitlement.ipa) using [SideStore](https://sidestore.io/) or [AltStore](https://altstore.io/) (**NOT** AltStore PAL).
|
||||||
|
|
||||||
|
3. **Sign In to Your Account**
|
||||||
|
- Open **Settings** in the entitlement app and sign in with your Apple ID.
|
||||||
|
|
||||||
|
4. **Refresh App IDs**
|
||||||
|
- Navigate to the **App IDs** page.
|
||||||
|
- Tap **Refresh** to update the list.
|
||||||
|
|
||||||
|
5. **Enable Increased Memory Limit**
|
||||||
|
- Select **MeloNX** (should be like "com.stossy11.MeloNX" or some variation) from the list.
|
||||||
|
- Tap **Add Increased Memory Limit**.
|
||||||
|
|
||||||
|
6. **Reinstall MeloNX**
|
||||||
|
- Delete the existing installation.
|
||||||
|
- Sideload the app again using SideStore or AltStore.
|
||||||
|
|
||||||
|
7. **Verify Increased Memory Limit**
|
||||||
|
- Open MeloNX and check if the **Increased Memory Limit** is enabled.
|
||||||
|
|
||||||
|
8. **Add Necessary Files**
|
||||||
|
|
||||||
|
If having Issues installing firmware (Make sure your keys are installed first)
|
||||||
|
- If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
|
||||||
|
- Copy the **bis** and **system** folders
|
||||||
|
|
||||||
|
9. **Enable JIT**
|
||||||
|
- Use your preferred method to enable Just-In-Time (JIT) compilation.
|
||||||
|
- We recommend using [JitStreamer](https://jkcoxson.com/jitstreamer)
|
||||||
|
|
||||||
|
|
||||||
|
### TrollStore
|
||||||
|
As Said in FAQ:
|
||||||
|
> MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but may have issues or not work at all.
|
||||||
|
|
||||||
|
1. **Install MeloNX with TrollStore**
|
||||||
|
|
||||||
|
2. **Add Necessary Files**
|
||||||
|
|
||||||
|
3. **Enable TrollStore JIT**
|
||||||
|
- MeloNX includes automatic JIT using the TrollStore URL Scheme
|
||||||
|
- Open MeloNX Settings
|
||||||
|
- Scroll down and enable the "TrollStore JIT" toggle
|
||||||
|
- Profit
|
||||||
|
|
||||||
|
|
||||||
|
### Free Developer Account (Xcode)
|
||||||
|
|
||||||
**NOTE: These Xcode builds are nightly and may have unfinished features.**
|
**NOTE: These Xcode builds are nightly and may have unfinished features.**
|
||||||
|
|
||||||
@ -70,8 +117,8 @@ If having Issues installing firmware (Make sure your Keys are installed first)
|
|||||||
|
|
||||||
2. **Add Necessary Files**
|
2. **Add Necessary Files**
|
||||||
|
|
||||||
If having Issues installing firmware (Make sure your Keys are installed first)
|
If having Issues installing firmware (Make sure your keys are installed first)
|
||||||
- If needed, install firmware and keys from **Ryujinx Desktop**.
|
- If needed, install firmware and keys from **Ryujinx Desktop** (or forks).
|
||||||
- Copy the **bis** and **system** folders
|
- Copy the **bis** and **system** folders
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@ -99,7 +146,7 @@ If having Issues installing firmware (Make sure your Keys are installed first)
|
|||||||
- **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 controllers.
|
||||||
Motion controls are not natively supported for now.
|
Motion controls are natively supported in most cases.
|
||||||
|
|
||||||
- **DLC & Modifications**
|
- **DLC & Modifications**
|
||||||
|
|
||||||
|
@ -41,4 +41,6 @@ ESCAPED_PATH=$(echo "$DOTNET_PATH" | sed 's/\//\\\//g')
|
|||||||
# Update the xcconfig file
|
# Update the xcconfig file
|
||||||
sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE"
|
sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE"
|
||||||
|
|
||||||
|
$DOTNET_PATH clean
|
||||||
|
|
||||||
echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH"
|
echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH"
|
||||||
|
@ -8,6 +8,6 @@
|
|||||||
// 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.5.0
|
VERSION = 1.7.0
|
||||||
|
|
||||||
DOTNET = /usr/local/share/dotnet/dotnet
|
DOTNET = /usr/local/share/dotnet/dotnet
|
||||||
|
@ -24,8 +24,6 @@
|
|||||||
/* End PBXAggregateTarget section */
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
256C91642D8126E300F9736D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 256C91632D8126E300F9736D /* Alamofire */; };
|
|
||||||
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
|
|
||||||
4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; };
|
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 */; };
|
||||||
@ -33,6 +31,13 @@
|
|||||||
/* 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 */;
|
||||||
@ -47,13 +52,6 @@
|
|||||||
remoteGlobalIDString = 4E80A98C2CD6F54500029585;
|
remoteGlobalIDString = 4E80A98C2CD6F54500029585;
|
||||||
remoteInfo = MeloNX;
|
remoteInfo = MeloNX;
|
||||||
};
|
};
|
||||||
4EE019E62D7CF7D600B7D583 /* 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 */;
|
||||||
@ -117,7 +115,7 @@
|
|||||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
|
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
|
||||||
CodeSignOnCopy,
|
CodeSignOnCopy,
|
||||||
);
|
);
|
||||||
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework" = (
|
"Dependencies/Dynamic Libraries/RyujinxHelper.framework" = (
|
||||||
CodeSignOnCopy,
|
CodeSignOnCopy,
|
||||||
RemoveHeadersOnCopy,
|
RemoveHeadersOnCopy,
|
||||||
);
|
);
|
||||||
@ -178,7 +176,7 @@
|
|||||||
"Dependencies/Dynamic Libraries/libavutil.dylib",
|
"Dependencies/Dynamic Libraries/libavutil.dylib",
|
||||||
"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/RyujinxKeyboard.framework",
|
"Dependencies/Dynamic Libraries/RyujinxHelper.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,
|
||||||
@ -204,9 +202,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
|
|
||||||
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
|
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
|
||||||
256C91642D8126E300F9736D /* Alamofire in Frameworks */,
|
|
||||||
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
|
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
|
||||||
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
|
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
@ -296,16 +292,14 @@
|
|||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
4EE019E72D7CF7D600B7D583 /* PBXTargetDependency */,
|
4E2953AC2D803BC9000497CD /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
4E80A98F2CD6F54500029585 /* MeloNX */,
|
4E80A98F2CD6F54500029585 /* MeloNX */,
|
||||||
);
|
);
|
||||||
name = MeloNX;
|
name = MeloNX;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
|
|
||||||
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
|
4EA5AE812D16807500AD0B9F /* SwiftSVG */,
|
||||||
256C91632D8126E300F9736D /* Alamofire */,
|
|
||||||
);
|
);
|
||||||
productName = MeloNX;
|
productName = MeloNX;
|
||||||
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
|
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
|
||||||
@ -365,7 +359,7 @@
|
|||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 1620;
|
LastSwiftUpdateCheck = 1620;
|
||||||
LastUpgradeCheck = 1610;
|
LastUpgradeCheck = 1620;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
4E80A98C2CD6F54500029585 = {
|
4E80A98C2CD6F54500029585 = {
|
||||||
CreatedOnToolsVersion = 16.1;
|
CreatedOnToolsVersion = 16.1;
|
||||||
@ -396,9 +390,7 @@
|
|||||||
mainGroup = 4E80A9842CD6F54500029585;
|
mainGroup = 4E80A9842CD6F54500029585;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
|
|
||||||
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
|
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */,
|
||||||
256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */,
|
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 56;
|
preferredProjectObjectVersion = 56;
|
||||||
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
|
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
|
||||||
@ -457,7 +449,7 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/native/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n";
|
shellScript = "cd ../..\nmv src/Ryujinx.Headless.SDL2/bin/Release/net8.0/ios-arm64/publish/Ryujinx.Headless.SDL2.dylib src/MeloNX/MeloNX/Dependencies/Dynamic\\ Libraries/Ryujinx.Headless.SDL2.dylib\n";
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
@ -486,6 +478,12 @@
|
|||||||
/* 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 */;
|
||||||
@ -496,11 +494,6 @@
|
|||||||
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
||||||
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
|
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
4EE019E72D7CF7D600B7D583 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
|
|
||||||
targetProxy = 4EE019E62D7CF7D600B7D583 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
|
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
|
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
|
||||||
@ -655,7 +648,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = 4TD3JXVDW7;
|
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_TESTABILITY = NO;
|
ENABLE_TESTABILITY = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@ -716,8 +709,26 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = s;
|
GCC_OPTIMIZATION_LEVEL = z;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = MeloNX/Info.plist;
|
INFOPLIST_FILE = MeloNX/Info.plist;
|
||||||
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
|
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
|
||||||
@ -840,13 +851,50 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(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 = xyz.belladev.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -864,7 +912,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = 4TD3JXVDW7;
|
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@ -925,8 +973,26 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = s;
|
GCC_OPTIMIZATION_LEVEL = z;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = MeloNX/Info.plist;
|
INFOPLIST_FILE = MeloNX/Info.plist;
|
||||||
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
|
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
|
||||||
@ -1049,13 +1115,50 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(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 = xyz.belladev.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -1251,22 +1354,6 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/Alamofire/Alamofire";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 5.10.2;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 1.5.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
|
4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/mchoe/SwiftSVG";
|
repositoryURL = "https://github.com/mchoe/SwiftSVG";
|
||||||
@ -1278,16 +1365,6 @@
|
|||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
256C91632D8126E300F9736D /* Alamofire */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 256C91622D8126E300F9736D /* XCRemoteSwiftPackageReference "Alamofire" */;
|
|
||||||
productName = Alamofire;
|
|
||||||
};
|
|
||||||
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;
|
|
||||||
productName = SwiftUIJoystick;
|
|
||||||
};
|
|
||||||
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
|
4EA5AE812D16807500AD0B9F /* SwiftSVG */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */;
|
package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */;
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "587a0e7c5c7d612a2c16a973e66df9a6a582b963cb51df7c89fd96cb28ef4a63",
|
"originHash" : "fedf09a893a63378a2e53f631cd833ae83a0c9ee7338eb8d153b04fd34aaf805",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
|
||||||
"identity" : "alamofire",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/Alamofire/Alamofire",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
|
|
||||||
"version" : "5.10.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "swiftsvg",
|
"identity" : "swiftsvg",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@ -18,15 +9,6 @@
|
|||||||
"branch" : "master",
|
"branch" : "master",
|
||||||
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
|
"revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swiftuijoystick",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/michael94ellis/SwiftUIJoystick",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65",
|
|
||||||
"version" : "1.5.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -1,114 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "1610"
|
|
||||||
version = "1.7">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "YES"
|
|
||||||
buildImplicitDependencies = "YES"
|
|
||||||
buildArchitectures = "Automatic">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
|
|
||||||
BuildableName = "MeloNX.app"
|
|
||||||
BlueprintName = "MeloNX"
|
|
||||||
ReferencedContainer = "container:MeloNX.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
|
||||||
</BuildAction>
|
|
||||||
<TestAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
shouldAutocreateTestPlan = "YES">
|
|
||||||
<Testables>
|
|
||||||
<TestableReference
|
|
||||||
skipped = "NO"
|
|
||||||
parallelizable = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "4E80A99C2CD6F54700029585"
|
|
||||||
BuildableName = "MeloNXTests.xctest"
|
|
||||||
BlueprintName = "MeloNXTests"
|
|
||||||
ReferencedContainer = "container:MeloNX.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</TestableReference>
|
|
||||||
<TestableReference
|
|
||||||
skipped = "NO"
|
|
||||||
parallelizable = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "4E80A9A62CD6F54700029585"
|
|
||||||
BuildableName = "MeloNXUITests.xctest"
|
|
||||||
BlueprintName = "MeloNXUITests"
|
|
||||||
ReferencedContainer = "container:MeloNX.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</TestableReference>
|
|
||||||
</Testables>
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
launchStyle = "0"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
enableGPUValidationMode = "1"
|
|
||||||
showGraphicsOverview = "Yes"
|
|
||||||
allowLocationSimulation = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
|
|
||||||
BuildableName = "MeloNX.app"
|
|
||||||
BlueprintName = "MeloNX"
|
|
||||||
ReferencedContainer = "container:MeloNX.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
|
|
||||||
BuildableName = "MeloNX.app"
|
|
||||||
BlueprintName = "MeloNX"
|
|
||||||
ReferencedContainer = "container:MeloNX.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
</ProfileAction>
|
|
||||||
<AnalyzeAction
|
|
||||||
buildConfiguration = "Debug">
|
|
||||||
</AnalyzeAction>
|
|
||||||
<ArchiveAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
revealArchiveInOrganizer = "YES">
|
|
||||||
<PreActions>
|
|
||||||
<ExecutionAction
|
|
||||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
|
||||||
<ActionContent
|
|
||||||
title = "Run Script"
|
|
||||||
scriptText = "REPO_DIR="$(cd "${SRCROOT}/../../" && pwd)" SCRIPT_PATH="$REPO_DIR/distribution/ios/set_current_version.sh" sh "${SCRIPT_PATH}" "
|
|
||||||
shellToInvoke = "/bin/bash">
|
|
||||||
</ActionContent>
|
|
||||||
</ExecutionAction>
|
|
||||||
</PreActions>
|
|
||||||
</ArchiveAction>
|
|
||||||
</Scheme>
|
|
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1610"
|
LastUpgradeVersion = "1620"
|
||||||
version = "1.7">
|
version = "2.0">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES"
|
buildImplicitDependencies = "YES"
|
||||||
@ -62,10 +62,13 @@
|
|||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
|
debugXPCServices = "NO"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
enableGPUValidationMode = "1"
|
enableGPUValidationMode = "1"
|
||||||
showGraphicsOverview = "Yes"
|
allowLocationSimulation = "YES"
|
||||||
allowLocationSimulation = "YES">
|
queueDebuggingEnabled = "No"
|
||||||
|
consoleMode = "0"
|
||||||
|
structuredConsoleMode = "2">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
@ -105,8 +108,17 @@
|
|||||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
<ActionContent
|
<ActionContent
|
||||||
title = "Run Script"
|
title = "Run Script"
|
||||||
scriptText = "REPO_DIR="$(cd "${SRCROOT}/../../" && pwd)" SCRIPT_PATH="$REPO_DIR/distribution/ios/set_current_version.sh" sh "${SCRIPT_PATH}" "
|
scriptText = "REPO_DIR="$(cd "${SRCROOT}/../../" && pwd)" SCRIPT_PATH="$REPO_DIR/distribution/ios/set_current_version.sh" echo "hi" sh "${SCRIPT_PATH}" "
|
||||||
shellToInvoke = "/bin/bash">
|
shellToInvoke = "/bin/bash">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4E80A98C2CD6F54500029585"
|
||||||
|
BuildableName = "MeloNX.app"
|
||||||
|
BlueprintName = "MeloNX"
|
||||||
|
ReferencedContainer = "container:MeloNX.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
</ActionContent>
|
</ActionContent>
|
||||||
</ExecutionAction>
|
</ExecutionAction>
|
||||||
</PreActions>
|
</PreActions>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
<ActionContent
|
<ActionContent
|
||||||
title = "Run Script"
|
title = "Run Script"
|
||||||
scriptText = "REPO_DIR="$(cd "${SRCROOT}/../../" && pwd)" SCRIPT_PATH="$REPO_DIR/distribution/ios/get_dotnet.sh" sh "${SCRIPT_PATH}" "
|
scriptText = "REPO_DIR="$(cd "${SRCROOT}/../../" && pwd)" SCRIPT_PATH="$REPO_DIR/distribution/ios/get_dotnet.sh" echo "Xcode is located at: $DEVELOPER_DIR" sh "${SCRIPT_PATH}" "
|
||||||
shellToInvoke = "/bin/bash">
|
shellToInvoke = "/bin/bash">
|
||||||
<EnvironmentBuildable>
|
<EnvironmentBuildable>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
|
@ -6,39 +6,121 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Network
|
||||||
|
import UIKit
|
||||||
|
|
||||||
func enableJITEB() {
|
func enableJITEB() {
|
||||||
guard let bundleID = Bundle.main.bundleIdentifier else {
|
if UserDefaults.standard.bool(forKey: "waitForVPN") {
|
||||||
|
waitForVPNConnection { connected in
|
||||||
|
if connected {
|
||||||
|
enableJITEBRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enableJITEBRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableJITEBRequest() {
|
||||||
|
let pid = Int(getpid())
|
||||||
|
print(pid)
|
||||||
|
|
||||||
|
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
|
||||||
|
var request = URLRequest(url: address)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
presentAlert(title: "Request Error", message: error.localizedDescription)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let address = URL(string: "http://[fd00::]:9172/launch_app/\(bundleID)")!
|
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: address) { data, response, error in
|
|
||||||
if error != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
showLaunchAppAlert(jsonData: data!, in: UIApplication.shared.windows.last!.rootViewController!)
|
if let data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||||
|
showLaunchAppAlert(jsonData: data, in: windowScene.windows.last!.rootViewController!)
|
||||||
|
} else {
|
||||||
|
fatalError("Unable to get Window")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task.resume()
|
task.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitForVPNConnection(timeout: TimeInterval = 30, interval: TimeInterval = 1, _ completion: @escaping (Bool) -> Void) {
|
||||||
|
let startTime = Date()
|
||||||
|
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
|
||||||
|
|
||||||
|
timer.schedule(deadline: .now(), repeating: interval)
|
||||||
|
|
||||||
|
timer.setEventHandler {
|
||||||
|
pingSite { connected in
|
||||||
|
if connected {
|
||||||
|
timer.cancel()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
} else if Date().timeIntervalSince(startTime) > timeout {
|
||||||
|
timer.cancel()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping (Bool) -> Void) {
|
||||||
|
guard let url = URL(string: host) else {
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = URLSessionConfiguration.default
|
||||||
|
config.timeoutIntervalForRequest = 2.0
|
||||||
|
config.timeoutIntervalForResource = 2.0
|
||||||
|
|
||||||
|
let session = URLSession(configuration: config)
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
|
||||||
|
let task = session.dataTask(with: request) { _, response, error in
|
||||||
|
if let error = error {
|
||||||
|
print("Ping failed: \(error.localizedDescription)")
|
||||||
|
completion(false)
|
||||||
|
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
||||||
|
completion(true)
|
||||||
|
} else {
|
||||||
|
let httpResponse = response as? HTTPURLResponse
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func presentAlert(title: String, message: String, completion: (() -> Void)? = nil) {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let lastWindow = windowScene.windows.last {
|
||||||
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
|
||||||
|
completion?()
|
||||||
|
})
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
lastWindow.rootViewController?.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct LaunchApp: Codable {
|
struct LaunchApp: Codable {
|
||||||
let ok: Bool
|
let success: Bool
|
||||||
let error: String?
|
let message: String
|
||||||
let launching: Bool
|
|
||||||
let position: Int?
|
|
||||||
let mounting: Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
|
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
|
||||||
@ -47,28 +129,23 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
|
|||||||
|
|
||||||
var message = ""
|
var message = ""
|
||||||
|
|
||||||
if let error = result.error {
|
if !result.success {
|
||||||
message = "Error: \(error)"
|
message += "\n\(result.message)"
|
||||||
} else if result.mounting {
|
|
||||||
message = "App is mounting..."
|
|
||||||
} else if result.launching {
|
|
||||||
message = "App is launching..."
|
|
||||||
} else {
|
|
||||||
message = "App launch status unknown."
|
|
||||||
}
|
|
||||||
|
|
||||||
if let position = result.position {
|
|
||||||
message += "\nPosition: \(position)"
|
|
||||||
}
|
|
||||||
|
|
||||||
let alert = UIAlertController(title: "Launch Status", message: message, preferredStyle: .alert)
|
let alert = UIAlertController(title: "JIT Error", message: message, preferredStyle: .alert)
|
||||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
viewController.present(alert, animated: true)
|
viewController.present(alert, animated: true)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
print("Hopefully JIT is enabled now...")
|
||||||
|
Ryujinx.shared.ryuIsJITEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
|
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))
|
||||||
|
|
||||||
|
36
src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift
Normal file
36
src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// EnableJIT.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 10/02/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
func enableJITStik() {
|
||||||
|
let bundleid = Bundle.main.bundleIdentifier ?? "Unknown"
|
||||||
|
|
||||||
|
let address = URL(string: "stikjit://enable-jit?bundle-id=\(bundleid)")!
|
||||||
|
var request = URLRequest(url: address)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
presentAlert(title: "Request Error", message: error.localizedDescription)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||||
|
// JIT, wow
|
||||||
|
} else {
|
||||||
|
fatalError("Unable to get Window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.resume()
|
||||||
|
}
|
@ -78,8 +78,6 @@ class NativeController: Hashable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open a game controller for the virtual joystick
|
|
||||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
|
||||||
controller = SDL_GameControllerOpen(Int32(instanceID))
|
controller = SDL_GameControllerOpen(Int32(instanceID))
|
||||||
|
|
||||||
if controller == nil {
|
if controller == nil {
|
||||||
|
@ -70,8 +70,6 @@ class VirtualController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open a game controller for the virtual joystick
|
|
||||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
|
||||||
controller = SDL_GameControllerOpen(Int32(instanceID))
|
controller = SDL_GameControllerOpen(Int32(instanceID))
|
||||||
|
|
||||||
if controller == nil {
|
if controller == nil {
|
||||||
@ -133,10 +131,8 @@ class VirtualController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
|
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
|
||||||
let scaleFactor = 32767.0 / 160
|
let scaledX = Int16(min(32767.0, max(-32768.0, x * 32767.0)))
|
||||||
|
let scaledY = Int16(min(32767.0, max(-32768.0, y * 32767.0)))
|
||||||
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
|
|
||||||
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
|
|
||||||
|
|
||||||
if stick == .right {
|
if stick == .right {
|
||||||
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
|
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
|
||||||
@ -191,6 +187,24 @@ enum VirtualControllerButton: Int {
|
|||||||
case dPadRight
|
case dPadRight
|
||||||
case leftTrigger
|
case leftTrigger
|
||||||
case rightTrigger
|
case rightTrigger
|
||||||
|
|
||||||
|
var isTrigger: Bool {
|
||||||
|
switch self {
|
||||||
|
case .leftTrigger, .rightTrigger, .leftShoulder, .rightShoulder:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSmall: Bool {
|
||||||
|
switch self {
|
||||||
|
case .back, .start, .guide:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ThumbstickType: Int {
|
enum ThumbstickType: Int {
|
||||||
|
@ -10,9 +10,7 @@ import Foundation
|
|||||||
|
|
||||||
class MTLHud {
|
class MTLHud {
|
||||||
|
|
||||||
var canMetalHud: Bool {
|
@Published var canMetalHud: Bool = false
|
||||||
return openMetalDylib()
|
|
||||||
}
|
|
||||||
|
|
||||||
var isEnabled: Bool {
|
var isEnabled: Bool {
|
||||||
if let getenv = getenv("MTL_HUD_ENABLED") {
|
if let getenv = getenv("MTL_HUD_ENABLED") {
|
||||||
@ -24,7 +22,17 @@ class MTLHud {
|
|||||||
static let shared = MTLHud()
|
static let shared = MTLHud()
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
openMetalDylib()
|
let _ = openMetalDylib() // i'm fixing the warnings just because you said i suck at coding Autumn (propenchiefer,
|
||||||
|
https://youtu.be/tc65SNOTMz4 7:23)
|
||||||
|
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
|
||||||
|
enable()
|
||||||
|
} else {
|
||||||
|
disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggle() {
|
||||||
|
print(UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED"))
|
||||||
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
|
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
|
||||||
enable()
|
enable()
|
||||||
} else {
|
} else {
|
||||||
@ -35,16 +43,15 @@ class MTLHud {
|
|||||||
func openMetalDylib() -> Bool {
|
func openMetalDylib() -> Bool {
|
||||||
let path = "/usr/lib/libMTLHud.dylib"
|
let path = "/usr/lib/libMTLHud.dylib"
|
||||||
|
|
||||||
// Load the dynamic library
|
|
||||||
if dlopen(path, RTLD_NOW) != nil {
|
if dlopen(path, RTLD_NOW) != nil {
|
||||||
// Library loaded successfully
|
|
||||||
print("Library loaded from \(path)")
|
print("Library loaded from \(path)")
|
||||||
|
canMetalHud = true
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
// Handle error
|
|
||||||
if let error = String(validatingUTF8: dlerror()) {
|
if let error = String(validatingUTF8: dlerror()) {
|
||||||
print("Error loading library: \(error)")
|
print("Error loading library: \(error)")
|
||||||
}
|
}
|
||||||
|
canMetalHud = false
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,13 +31,14 @@ struct iOSNav<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Ryujinx {
|
class Ryujinx : ObservableObject {
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
|
|
||||||
let virtualController = VirtualController()
|
let virtualController = VirtualController()
|
||||||
|
|
||||||
@Published var controllerMap: [Controller] = []
|
@Published var controllerMap: [Controller] = []
|
||||||
@Published var metalLayer: CAMetalLayer? = nil
|
@Published var metalLayer: CAMetalLayer? = nil
|
||||||
|
@Published var isPortrait = false
|
||||||
@Published var firmwareversion = "0"
|
@Published var firmwareversion = "0"
|
||||||
@Published var emulationUIView: MeloMTKView? = nil
|
@Published var emulationUIView: MeloMTKView? = nil
|
||||||
@Published var config: Ryujinx.Configuration? = nil
|
@Published var config: Ryujinx.Configuration? = nil
|
||||||
@ -45,6 +46,10 @@ class Ryujinx {
|
|||||||
|
|
||||||
@Published var defMLContentSize: CGFloat?
|
@Published var defMLContentSize: CGFloat?
|
||||||
|
|
||||||
|
var thread: Thread!
|
||||||
|
|
||||||
|
@Published var jitenabled = false
|
||||||
|
|
||||||
var shouldMetal: Bool {
|
var shouldMetal: Bool {
|
||||||
metalLayer == nil
|
metalLayer == nil
|
||||||
}
|
}
|
||||||
@ -145,7 +150,7 @@ class Ryujinx {
|
|||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
RunLoop.current.perform { [self] in
|
thread = Thread { [self] in
|
||||||
|
|
||||||
isRunning = true
|
isRunning = true
|
||||||
|
|
||||||
@ -178,6 +183,10 @@ class Ryujinx {
|
|||||||
Self.log("Emulation failed to start: \(error)")
|
Self.log("Emulation failed to start: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thread.qualityOfService = .userInteractive
|
||||||
|
thread.name = "MeloNX"
|
||||||
|
thread.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -192,6 +201,7 @@ class Ryujinx {
|
|||||||
self.metalLayer = nil
|
self.metalLayer = nil
|
||||||
|
|
||||||
stop_emulation()
|
stop_emulation()
|
||||||
|
thread.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
var running: Bool {
|
var running: Bool {
|
||||||
@ -264,6 +274,24 @@ class Ryujinx {
|
|||||||
// 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])
|
||||||
@ -360,7 +388,6 @@ class Ryujinx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fetchFirmwareVersion() -> String {
|
func fetchFirmwareVersion() -> String {
|
||||||
do {
|
|
||||||
let firmwareVersionPointer = installed_firmware_version()
|
let firmwareVersionPointer = installed_firmware_version()
|
||||||
if let pointer = firmwareVersionPointer {
|
if let pointer = firmwareVersionPointer {
|
||||||
let firmwareVersion = String(cString: pointer)
|
let firmwareVersion = String(cString: pointer)
|
||||||
@ -370,10 +397,6 @@ class Ryujinx {
|
|||||||
return firmwareVersion
|
return firmwareVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "0"
|
return "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,66 +524,145 @@ class Ryujinx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func repeatuntilfindLayer() {
|
|
||||||
Task { @MainActor in
|
|
||||||
while self.metalLayer == nil {
|
|
||||||
let layer = self.getMetalLayer(nil)
|
|
||||||
|
|
||||||
if layer != nil {
|
|
||||||
self.metalLayer = layer
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.sleep(forTimeInterval: 0.1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? {
|
|
||||||
var window = window
|
|
||||||
if window == nil {
|
|
||||||
window = SDL_GetWindowFromID(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var windowInfo = SDL_SysWMinfo()
|
|
||||||
SDL_GetWindowWMInfo(window, &windowInfo)
|
|
||||||
|
|
||||||
|
|
||||||
guard let uiWindow = windowInfo.info.uikit.window,
|
|
||||||
let rootView = uiWindow.takeUnretainedValue().rootViewController?.view else {
|
|
||||||
print("Unable to get root view")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findMetalLayer(in view: UIView) -> CAMetalLayer? {
|
|
||||||
if let metalLayer = view.layer as? CAMetalLayer {
|
|
||||||
return metalLayer
|
|
||||||
}
|
|
||||||
|
|
||||||
for subview in view.subviews {
|
|
||||||
if let metalLayer = findMetalLayer(in: subview) {
|
|
||||||
return metalLayer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if let existingLayer = findMetalLayer(in: rootView) {
|
|
||||||
print("Found Metal Layer")
|
|
||||||
return existingLayer
|
|
||||||
}
|
|
||||||
print("found nothing")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static func log(_ message: String) {
|
static func log(_ message: String) {
|
||||||
print("[Ryujinx] \(message)")
|
print("[Ryujinx] \(message)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func updateOrientation() -> Bool {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let window = windowScene.windows.first {
|
||||||
|
return (window.bounds.size.height > window.bounds.size.width)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ryuIsJITEnabled() {
|
||||||
|
jitenabled = isJITEnabled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public extension UIDevice {
|
||||||
|
static let modelName: String = {
|
||||||
|
var systemInfo = utsname()
|
||||||
|
uname(&systemInfo)
|
||||||
|
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||||
|
let identifier = machineMirror.children.reduce("") { identifier, element in
|
||||||
|
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||||
|
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity
|
||||||
|
#if os(iOS)
|
||||||
|
switch identifier {
|
||||||
|
case "iPod5,1": return "iPod touch (5th generation)"
|
||||||
|
case "iPod7,1": return "iPod touch (6th generation)"
|
||||||
|
case "iPod9,1": return "iPod touch (7th generation)"
|
||||||
|
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4"
|
||||||
|
case "iPhone4,1": return "iPhone 4s"
|
||||||
|
case "iPhone5,1", "iPhone5,2": return "iPhone 5"
|
||||||
|
case "iPhone5,3", "iPhone5,4": return "iPhone 5c"
|
||||||
|
case "iPhone6,1", "iPhone6,2": return "iPhone 5s"
|
||||||
|
case "iPhone7,2": return "iPhone 6"
|
||||||
|
case "iPhone7,1": return "iPhone 6 Plus"
|
||||||
|
case "iPhone8,1": return "iPhone 6s"
|
||||||
|
case "iPhone8,2": return "iPhone 6s Plus"
|
||||||
|
case "iPhone9,1", "iPhone9,3": return "iPhone 7"
|
||||||
|
case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus"
|
||||||
|
case "iPhone10,1", "iPhone10,4": return "iPhone 8"
|
||||||
|
case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus"
|
||||||
|
case "iPhone10,3", "iPhone10,6": return "iPhone X"
|
||||||
|
case "iPhone11,2": return "iPhone XS"
|
||||||
|
case "iPhone11,4", "iPhone11,6": return "iPhone XS Max"
|
||||||
|
case "iPhone11,8": return "iPhone XR"
|
||||||
|
case "iPhone12,1": return "iPhone 11"
|
||||||
|
case "iPhone12,3": return "iPhone 11 Pro"
|
||||||
|
case "iPhone12,5": return "iPhone 11 Pro Max"
|
||||||
|
case "iPhone13,1": return "iPhone 12 mini"
|
||||||
|
case "iPhone13,2": return "iPhone 12"
|
||||||
|
case "iPhone13,3": return "iPhone 12 Pro"
|
||||||
|
case "iPhone13,4": return "iPhone 12 Pro Max"
|
||||||
|
case "iPhone14,4": return "iPhone 13 mini"
|
||||||
|
case "iPhone14,5": return "iPhone 13"
|
||||||
|
case "iPhone14,2": return "iPhone 13 Pro"
|
||||||
|
case "iPhone14,3": return "iPhone 13 Pro Max"
|
||||||
|
case "iPhone14,7": return "iPhone 14"
|
||||||
|
case "iPhone14,8": return "iPhone 14 Plus"
|
||||||
|
case "iPhone15,2": return "iPhone 14 Pro"
|
||||||
|
case "iPhone15,3": return "iPhone 14 Pro Max"
|
||||||
|
case "iPhone15,4": return "iPhone 15"
|
||||||
|
case "iPhone15,5": return "iPhone 15 Plus"
|
||||||
|
case "iPhone16,1": return "iPhone 15 Pro"
|
||||||
|
case "iPhone16,2": return "iPhone 15 Pro Max"
|
||||||
|
case "iPhone17,3": return "iPhone 16"
|
||||||
|
case "iPhone17,4": return "iPhone 16 Plus"
|
||||||
|
case "iPhone17,1": return "iPhone 16 Pro"
|
||||||
|
case "iPhone17,2": return "iPhone 16 Pro Max"
|
||||||
|
case "iPhone17,5": return "iPhone 16e"
|
||||||
|
case "iPhone8,4": return "iPhone SE"
|
||||||
|
case "iPhone12,8": return "iPhone SE (2nd generation)"
|
||||||
|
case "iPhone14,6": return "iPhone SE (3rd generation)"
|
||||||
|
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2"
|
||||||
|
case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)"
|
||||||
|
case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)"
|
||||||
|
case "iPad6,11", "iPad6,12": return "iPad (5th generation)"
|
||||||
|
case "iPad7,5", "iPad7,6": return "iPad (6th generation)"
|
||||||
|
case "iPad7,11", "iPad7,12": return "iPad (7th generation)"
|
||||||
|
case "iPad11,6", "iPad11,7": return "iPad (8th generation)"
|
||||||
|
case "iPad12,1", "iPad12,2": return "iPad (9th generation)"
|
||||||
|
case "iPad13,18", "iPad13,19": return "iPad (10th generation)"
|
||||||
|
case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air"
|
||||||
|
case "iPad5,3", "iPad5,4": return "iPad Air 2"
|
||||||
|
case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)"
|
||||||
|
case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
|
||||||
|
case "iPad13,16", "iPad13,17": return "iPad Air (5th generation)"
|
||||||
|
case "iPad14,8", "iPad14,9": return "iPad Air (11-inch) (M2)"
|
||||||
|
case "iPad14,10", "iPad14,11": return "iPad Air (13-inch) (M2)"
|
||||||
|
case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini"
|
||||||
|
case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2"
|
||||||
|
case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3"
|
||||||
|
case "iPad5,1", "iPad5,2": return "iPad mini 4"
|
||||||
|
case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)"
|
||||||
|
case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)"
|
||||||
|
case "iPad16,1", "iPad16,2": return "iPad mini (A17 Pro)"
|
||||||
|
case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)"
|
||||||
|
case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)"
|
||||||
|
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)"
|
||||||
|
case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)"
|
||||||
|
case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)"
|
||||||
|
case "iPad14,3", "iPad14,4": return "iPad Pro (11-inch) (4th generation)"
|
||||||
|
case "iPad16,3", "iPad16,4": return "iPad Pro (11-inch) (M4)"
|
||||||
|
case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)"
|
||||||
|
case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)"
|
||||||
|
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)"
|
||||||
|
case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)"
|
||||||
|
case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)"
|
||||||
|
case "iPad14,5", "iPad14,6": return "iPad Pro (12.9-inch) (6th generation)"
|
||||||
|
case "iPad16,5", "iPad16,6": return "iPad Pro (13-inch) (M4)"
|
||||||
|
case "AppleTV5,3": return "Apple TV"
|
||||||
|
case "AppleTV6,2": return "Apple TV 4K"
|
||||||
|
case "AudioAccessory1,1": return "HomePod"
|
||||||
|
case "AudioAccessory5,1": return "HomePod mini"
|
||||||
|
case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))"
|
||||||
|
default: return identifier
|
||||||
|
}
|
||||||
|
#elseif os(tvOS)
|
||||||
|
switch identifier {
|
||||||
|
case "AppleTV5,3": return "Apple TV 4"
|
||||||
|
case "AppleTV6,2", "AppleTV11,1", "AppleTV14,1": return "Apple TV 4K"
|
||||||
|
case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))"
|
||||||
|
default: return identifier
|
||||||
|
}
|
||||||
|
#elseif os(visionOS)
|
||||||
|
switch identifier {
|
||||||
|
case "RealityDevice14,1": return "Apple Vision Pro"
|
||||||
|
default: return identifier
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapToDevice(identifier: identifier)
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -32,7 +32,7 @@ struct LaunchGameIntentDef: AppIntent {
|
|||||||
|
|
||||||
let ryujinx = Ryujinx.shared.games
|
let ryujinx = Ryujinx.shared.games
|
||||||
|
|
||||||
let name = findClosestGameName(input: gameName, games: ryujinx.flatMap(\.titleName))
|
let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName))
|
||||||
|
|
||||||
let urlString = "melonx://game?name=\(name ?? gameName)"
|
let urlString = "melonx://game?name=\(name ?? gameName)"
|
||||||
print(urlString)
|
print(urlString)
|
||||||
|
@ -63,22 +63,15 @@ public struct Game: Identifiable, Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createImage(from gameInfo: GameInfo) -> UIImage? {
|
func createImage(from gameInfo: GameInfo) -> UIImage? {
|
||||||
// Access the struct
|
|
||||||
let gameInfoValue = gameInfo
|
let gameInfoValue = gameInfo
|
||||||
|
|
||||||
// Get the image data
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the ImageData byte array to Swift's Data
|
|
||||||
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
|
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
|
||||||
|
|
||||||
// Create a UIImage (or NSImage on macOS)
|
|
||||||
print(imageData)
|
|
||||||
|
|
||||||
return UIImage(data: imageData)
|
return UIImage(data: imageData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
// Created by Bella on 12/03/2025.
|
// Created by Bella on 12/03/2025.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
struct LatestVersionResponse: Codable {
|
struct LatestVersionResponse: Codable {
|
||||||
let version_number: String
|
let version_number: String
|
||||||
let version_number_stripped: String
|
let version_number_stripped: String
|
||||||
|
@ -6,12 +6,10 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
// import SDL2
|
|
||||||
import GameController
|
import GameController
|
||||||
import Darwin
|
import Darwin
|
||||||
import UIKit
|
import UIKit
|
||||||
import MetalKit
|
import MetalKit
|
||||||
// import SDL
|
|
||||||
|
|
||||||
struct MoltenVKSettings: Codable, Hashable {
|
struct MoltenVKSettings: Codable, Hashable {
|
||||||
let string: String
|
let string: String
|
||||||
@ -19,6 +17,8 @@ struct MoltenVKSettings: Codable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
// Games
|
// Games
|
||||||
@State private var game: Game?
|
@State private var game: Game?
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
// JIT
|
// JIT
|
||||||
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
|
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
|
||||||
|
@AppStorage("stikJIT") var stikJIT: Bool = false
|
||||||
|
|
||||||
// Other Configuration
|
// Other Configuration
|
||||||
@State var isMK8: Bool = false
|
@State var isMK8: Bool = false
|
||||||
@ -53,145 +54,155 @@ 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
|
||||||
@State var jitNotEnabled = false
|
@StateObject var ryujinx = Ryujinx.shared
|
||||||
|
|
||||||
|
// MARK: - SDL
|
||||||
|
var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
init() {
|
init() {
|
||||||
var defaultConfig = loadSettings()
|
var defaultConfig = loadSettings()
|
||||||
if defaultConfig == nil {
|
if defaultConfig == nil {
|
||||||
saveSettings(config: .init(gamepath: ""))
|
saveSettings(config: .init(gamepath: ""))
|
||||||
|
|
||||||
defaultConfig = loadSettings()
|
defaultConfig = loadSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_config = State(initialValue: defaultConfig!)
|
_config = State(initialValue: defaultConfig!)
|
||||||
|
|
||||||
let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings.
|
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"),
|
||||||
MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
|
MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_LOG_LEVEL", value: "0"),
|
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"),
|
MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"),
|
||||||
// MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"),
|
|
||||||
// Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64)
|
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"),
|
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"),
|
||||||
]
|
]
|
||||||
|
|
||||||
_settings = State(initialValue: defaultSettings)
|
_settings = State(initialValue: defaultSettings)
|
||||||
|
|
||||||
|
print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
|
||||||
|
|
||||||
initializeSDL()
|
initializeSDL()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if game != nil, !jitNotEnabled {
|
if game != nil && (ryujinx.jitenabled || ignoreJIT) {
|
||||||
// This is when the game starts to stop the animation
|
gameView
|
||||||
ZStack {
|
} else if game != nil && !ryujinx.jitenabled {
|
||||||
if #available(iOS 16, *) {
|
jitErrorView
|
||||||
EmulationView(startgame: $game)
|
|
||||||
.persistentSystemOverlays(.hidden)
|
|
||||||
} else {
|
} else {
|
||||||
EmulationView(startgame: $game)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isLoading {
|
|
||||||
ZStack {
|
|
||||||
Color.black
|
|
||||||
.opacity(0.8)
|
|
||||||
emulationView
|
|
||||||
.ignoresSafeArea(.all)
|
|
||||||
}
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
.ignoresSafeArea(.all)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if game != nil, ignoreJIT {
|
|
||||||
ZStack {
|
|
||||||
if #available(iOS 16, *) {
|
|
||||||
EmulationView(startgame: $game)
|
|
||||||
.persistentSystemOverlays(.hidden)
|
|
||||||
} else {
|
|
||||||
EmulationView(startgame: $game)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isLoading {
|
|
||||||
ZStack {
|
|
||||||
Color.black
|
|
||||||
.opacity(0.8)
|
|
||||||
emulationView
|
|
||||||
.ignoresSafeArea(.all)
|
|
||||||
}
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
.ignoresSafeArea(.all)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if game != nil {
|
|
||||||
Text("")
|
|
||||||
.sheet(isPresented: $jitNotEnabled) {
|
|
||||||
JITPopover() {
|
|
||||||
jitNotEnabled = false
|
|
||||||
}
|
|
||||||
.interactiveDismissDisabled()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This is the main menu view that includes the Settings and the Game Selector
|
|
||||||
mainMenuView
|
mainMenuView
|
||||||
.onAppear() {
|
|
||||||
quits = false
|
|
||||||
|
|
||||||
loadSettings()
|
|
||||||
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
|
|
||||||
}
|
|
||||||
.onOpenURL() { url in
|
|
||||||
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
|
||||||
components.host == "game" {
|
|
||||||
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
|
||||||
|
|
||||||
game = Ryujinx.shared.games.first(where: { $0.titleId == text })
|
|
||||||
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
|
||||||
game = Ryujinx.shared.games.first(where: { $0.titleName == text })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func initControllerObservers() {
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
forName: .GCControllerDidConnect,
|
|
||||||
object: nil,
|
|
||||||
queue: .main) { notification in
|
|
||||||
if let controller = notification.object as? GCController {
|
|
||||||
print("Controller connected: \(controller.productCategory)")
|
|
||||||
nativeControllers[controller] = .init(controller)
|
|
||||||
refreshControllersList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
forName: .GCControllerDidDisconnect,
|
|
||||||
object: nil,
|
|
||||||
queue: .main) { notification in
|
|
||||||
if let controller = notification.object as? GCController {
|
|
||||||
print("Controller disconnected: \(controller.productCategory)")
|
|
||||||
nativeControllers[controller]?.cleanup()
|
|
||||||
nativeControllers[controller] = nil
|
|
||||||
refreshControllersList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - View Components
|
// MARK: - View Components
|
||||||
|
|
||||||
|
private var gameView: some View {
|
||||||
|
ZStack {
|
||||||
|
if #available(iOS 16, *) {
|
||||||
|
EmulationView(startgame: $game)
|
||||||
|
.persistentSystemOverlays(.hidden)
|
||||||
|
} else {
|
||||||
|
EmulationView(startgame: $game)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isLoading {
|
||||||
|
ZStack {
|
||||||
|
Color.black.opacity(0.8)
|
||||||
|
emulationView.ignoresSafeArea(.all)
|
||||||
|
}
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.ignoresSafeArea(.all)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var jitErrorView: some View {
|
||||||
|
Text("")
|
||||||
|
.sheet(isPresented:Binding(
|
||||||
|
get: { !ryujinx.jitenabled },
|
||||||
|
set: { newValue in
|
||||||
|
ryujinx.jitenabled = newValue
|
||||||
|
|
||||||
|
ryujinx.ryuIsJITEnabled()
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
JITPopover() {
|
||||||
|
ryujinx.jitenabled = false
|
||||||
|
}
|
||||||
|
.interactiveDismissDisabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mainMenuView: some View {
|
||||||
|
MainTabView(
|
||||||
|
startemu: $game,
|
||||||
|
config: $config,
|
||||||
|
MVKconfig: $settings,
|
||||||
|
controllersList: $controllersList,
|
||||||
|
currentControllers: $currentControllers,
|
||||||
|
onscreencontroller: $onscreencontroller
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
quits = false
|
||||||
|
let _ = loadSettings()
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
|
||||||
|
refreshControllersList()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
print(MTLHud.shared.isEnabled)
|
||||||
|
|
||||||
|
initControllerObservers()
|
||||||
|
|
||||||
|
Air.play(AnyView(
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "gamecontroller")
|
||||||
|
.font(.system(size: 300))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
|
||||||
|
Text("Select Game")
|
||||||
|
.font(.system(size: 150))
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
checkJitStatus()
|
||||||
|
}
|
||||||
|
.onOpenURL { url in
|
||||||
|
handleDeepLink(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var emulationView: some View {
|
private var emulationView: some View {
|
||||||
GeometryReader { screenGeometry in
|
GeometryReader { screenGeometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
|
gameLoadingContent(screenGeometry: screenGeometry)
|
||||||
|
|
||||||
|
HStack{
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
if showlogsloading {
|
||||||
|
LogFileView(isfps: true)
|
||||||
|
.frame(alignment: .topLeading)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
|
private func gameLoadingContent(screenGeometry: GeometryProxy) -> some View {
|
||||||
HStack(spacing: screenGeometry.size.width * 0.04) {
|
HStack(spacing: screenGeometry.size.width * 0.04) {
|
||||||
if let icon = game?.icon {
|
if let icon = game?.icon {
|
||||||
Image(uiImage: icon)
|
Image(uiImage: icon)
|
||||||
@ -209,6 +220,18 @@ struct ContentView: View {
|
|||||||
.font(.system(size: min(screenGeometry.size.width * 0.04, 32)))
|
.font(.system(size: min(screenGeometry.size.width * 0.04, 32)))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
loadingProgressBar(screenGeometry: screenGeometry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, screenGeometry.size.width * 0.06)
|
||||||
|
.padding(.vertical, screenGeometry.size.height * 0.05)
|
||||||
|
.position(
|
||||||
|
x: screenGeometry.size.width / 2,
|
||||||
|
y: screenGeometry.size.height * 0.5
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadingProgressBar(screenGeometry: GeometryProxy) -> some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
|
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
|
||||||
|
|
||||||
@ -234,20 +257,14 @@ struct ContentView: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
isAnimating = true
|
isAnimating = true
|
||||||
|
|
||||||
setupEmulation()
|
setupEmulation()
|
||||||
|
|
||||||
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
|
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
|
||||||
if get_current_fps() != 0 {
|
if get_current_fps() != 0 {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
|
||||||
isAnimating = false
|
isAnimating = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,59 +273,42 @@ struct ContentView: View {
|
|||||||
.frame(height: min(screenGeometry.size.height * 0.015, 12))
|
.frame(height: min(screenGeometry.size.height * 0.015, 12))
|
||||||
.frame(width: min(screenGeometry.size.width * 0.35, 350))
|
.frame(width: min(screenGeometry.size.width * 0.35, 350))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.padding(.horizontal, screenGeometry.size.width * 0.06)
|
|
||||||
.padding(.vertical, screenGeometry.size.height * 0.05)
|
|
||||||
.position(
|
|
||||||
x: screenGeometry.size.width / 2,
|
|
||||||
y: screenGeometry.size.height * 0.5
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if showlogsloading {
|
|
||||||
LogFileView(isfps: true)
|
|
||||||
.frame(alignment: .topLeading)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mainMenuView: some View {
|
|
||||||
MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
|
|
||||||
.onAppear() {
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
|
|
||||||
refreshControllersList()
|
|
||||||
}
|
|
||||||
|
|
||||||
Air.play(AnyView(
|
|
||||||
VStack {
|
|
||||||
Image(systemName: "gamecontroller")
|
|
||||||
.font(.system(size: 300))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
|
|
||||||
Text("Select Game")
|
|
||||||
.font(.system(size: 150))
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
))
|
|
||||||
|
|
||||||
jitNotEnabled = !isJITEnabled()
|
|
||||||
if jitNotEnabled {
|
|
||||||
useTrollStore ? askForJIT() : jitStreamerEB ? enableJITEB() : print("no JIT")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
|
||||||
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; // Initialises SDL2 for Events, Game Controller, Joystick, Audio and Video.
|
|
||||||
private func initializeSDL() {
|
private func initializeSDL() {
|
||||||
setMoltenVKSettings()
|
setMoltenVKSettings()
|
||||||
SDL_SetMainReady() // Sets SDL Ready
|
SDL_SetMainReady()
|
||||||
SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true
|
SDL_iPhoneSetEventPump(SDL_TRUE)
|
||||||
SDL_Init(SdlInitFlags) // Initialises SDL2
|
SDL_Init(sdlInitFlags)
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func initControllerObservers() {
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: .GCControllerDidConnect,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { notification in
|
||||||
|
if let controller = notification.object as? GCController {
|
||||||
|
print("Controller connected: \(controller.productCategory)")
|
||||||
|
nativeControllers[controller] = .init(controller)
|
||||||
|
refreshControllersList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: .GCControllerDidDisconnect,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { notification in
|
||||||
|
if let controller = notification.object as? GCController {
|
||||||
|
print("Controller disconnected: \(controller.productCategory)")
|
||||||
|
nativeControllers[controller]?.cleanup()
|
||||||
|
nativeControllers[controller] = nil
|
||||||
|
refreshControllersList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func setupEmulation() {
|
private func setupEmulation() {
|
||||||
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
|
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
|
||||||
|
|
||||||
@ -318,9 +318,9 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func refreshControllersList() {
|
private func refreshControllersList() {
|
||||||
controllersList = Ryujinx.shared.getConnectedControllers()
|
controllersList = ryujinx.getConnectedControllers()
|
||||||
|
|
||||||
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
|
if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
|
||||||
self.onscreencontroller = onscreen
|
self.onscreencontroller = onscreen
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,8 +330,7 @@ struct ContentView: View {
|
|||||||
currentControllers = []
|
currentControllers = []
|
||||||
|
|
||||||
if controllersList.count == 1 {
|
if controllersList.count == 1 {
|
||||||
let controller = controllersList[0]
|
currentControllers.append(controllersList[0])
|
||||||
currentControllers.append(controller)
|
|
||||||
} else if (controllersList.count - 1) >= 1 {
|
} else if (controllersList.count - 1) >= 1 {
|
||||||
for controller in controllersList {
|
for controller in controllersList {
|
||||||
if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
|
if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
|
||||||
@ -341,44 +340,70 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private func start(displayid: UInt32) {
|
private func start(displayid: UInt32) {
|
||||||
guard let game else { return }
|
guard let game else { return }
|
||||||
|
|
||||||
config.gamepath = game.fileURL.path
|
config.gamepath = game.fileURL.path
|
||||||
config.inputids = Array(Set(currentControllers.map(\.id)))
|
config.inputids = Array(Set(currentControllers.map(\.id)))
|
||||||
|
|
||||||
if mVKPreFillBuffer {
|
configureEnvironmentVariables()
|
||||||
let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2")
|
|
||||||
setenv(setting.string, setting.value, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if syncqsubmits {
|
|
||||||
let setting = MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "2")
|
|
||||||
setenv(setting.string, setting.value, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.inputids.isEmpty {
|
if config.inputids.isEmpty {
|
||||||
config.inputids.append("0")
|
config.inputids.append("0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try Ryujinx.shared.start(with: config)
|
try ryujinx.start(with: config)
|
||||||
} catch {
|
} catch {
|
||||||
print("Error: \(error.localizedDescription)")
|
print("Error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func configureEnvironmentVariables() {
|
||||||
|
if mVKPreFillBuffer {
|
||||||
|
mVKPreFillBuffer = false
|
||||||
|
// setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if syncqsubmits {
|
||||||
|
setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "2", 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sets MoltenVK Environment Variables
|
|
||||||
private func setMoltenVKSettings() {
|
private func setMoltenVKSettings() {
|
||||||
settings.forEach { setting in
|
settings.forEach { setting in
|
||||||
setenv(setting.string, setting.value, 1)
|
setenv(setting.string, setting.value, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func checkJitStatus() {
|
||||||
|
ryujinx.ryuIsJITEnabled()
|
||||||
|
if jitStreamerEB {
|
||||||
|
jitStreamerEB = false // byee jitstreamer eb
|
||||||
|
}
|
||||||
|
if !ryujinx.jitenabled {
|
||||||
|
if useTrollStore {
|
||||||
|
askForJIT()
|
||||||
|
} else if stikJIT {
|
||||||
|
enableJITStik()
|
||||||
|
} else if jitStreamerEB {
|
||||||
|
enableJITEB()
|
||||||
|
} else {
|
||||||
|
print("no JIT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleDeepLink(_ url: URL) {
|
||||||
|
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
|
components.host == "game" {
|
||||||
|
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
||||||
|
game = ryujinx.games.first(where: { $0.titleId == text })
|
||||||
|
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
||||||
|
game = ryujinx.games.first(where: { $0.titleName == text })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Array {
|
extension Array {
|
||||||
|
@ -7,105 +7,172 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import GameController
|
import GameController
|
||||||
import SwiftUIJoystick
|
|
||||||
import CoreMotion
|
import CoreMotion
|
||||||
|
|
||||||
struct ControllerView: View {
|
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 {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
Group {
|
||||||
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
|
let isPad = UIDevice.current.userInterfaceIdiom == .pad
|
||||||
VStack {
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
|
||||||
HStack {
|
|
||||||
VStack {
|
|
||||||
ShoulderButtonsViewLeft()
|
|
||||||
ZStack {
|
|
||||||
Joystick()
|
|
||||||
DPadView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
|
||||||
ShoulderButtonsViewRight()
|
|
||||||
ZStack {
|
|
||||||
Joystick(iscool: true) // hope this works
|
|
||||||
ABXYView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
ButtonView(button: .start) // Adding the + button
|
|
||||||
.padding(.horizontal, 40)
|
|
||||||
ButtonView(button: .back) // Adding the - button
|
|
||||||
.padding(.horizontal, 40)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if isPortrait && !isPad {
|
||||||
|
portraitLayout
|
||||||
} else {
|
} else {
|
||||||
// could be landscape
|
landscapeLayout
|
||||||
VStack {
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
|
||||||
HStack {
|
|
||||||
|
|
||||||
// gotta fuckin add + and - now
|
|
||||||
VStack {
|
|
||||||
ShoulderButtonsViewLeft()
|
|
||||||
ZStack {
|
|
||||||
Joystick()
|
|
||||||
DPadView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HStack {
|
|
||||||
// Spacer()
|
|
||||||
VStack {
|
|
||||||
// Spacer()
|
|
||||||
ButtonView(button: .back) // Adding the - button
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
|
||||||
// Spacer()
|
|
||||||
ButtonView(button: .start) // Adding the + button
|
|
||||||
}
|
|
||||||
// Spacer()
|
|
||||||
}
|
|
||||||
VStack {
|
|
||||||
ShoulderButtonsViewRight()
|
|
||||||
ZStack {
|
|
||||||
Joystick(iscool: true) // hope this work s
|
|
||||||
ABXYView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
// .padding(.bottom, geometry.size.height / 11) // also extremally broken (
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
.onChange(of: verticalSizeClass) { _ in
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
.onAppear(perform: updateOrientation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Layouts
|
||||||
|
private var portraitLayout: some View {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
HStack(spacing: 30) {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
ShoulderButtonsViewLeft()
|
||||||
|
ZStack {
|
||||||
|
JoystickController(showBackground: $hideDpad)
|
||||||
|
if !hideDpad {
|
||||||
|
DPadView()
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: hideDpad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
ShoulderButtonsViewRight()
|
||||||
|
ZStack {
|
||||||
|
JoystickController(iscool: true, showBackground: $hideABXY)
|
||||||
|
if !hideABXY {
|
||||||
|
ABXYView()
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: hideABXY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 60) {
|
||||||
|
HStack {
|
||||||
|
ButtonView(button: .leftStick)
|
||||||
|
.padding()
|
||||||
|
ButtonView(button: .start)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
ButtonView(button: .back)
|
||||||
|
ButtonView(button: .rightStick)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var landscapeLayout: some View {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
ShoulderButtonsViewLeft()
|
||||||
|
ZStack {
|
||||||
|
JoystickController(showBackground: $hideDpad)
|
||||||
|
if !hideDpad {
|
||||||
|
DPadView()
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: hideDpad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
centerButtons
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
ShoulderButtonsViewRight()
|
||||||
|
ZStack {
|
||||||
|
JoystickController(iscool: true, showBackground: $hideABXY)
|
||||||
|
if !hideABXY {
|
||||||
|
ABXYView()
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: hideABXY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var centerButtons: some View {
|
||||||
|
Group {
|
||||||
|
if stickButton {
|
||||||
|
VStack {
|
||||||
|
HStack(spacing: 50) {
|
||||||
|
ButtonView(button: .leftStick)
|
||||||
|
.padding()
|
||||||
|
Spacer()
|
||||||
|
ButtonView(button: .rightStick)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.padding(.top, 30)
|
||||||
|
|
||||||
|
HStack(spacing: 50) {
|
||||||
|
ButtonView(button: .back)
|
||||||
|
Spacer()
|
||||||
|
ButtonView(button: .start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
} else {
|
||||||
|
HStack(spacing: 50) {
|
||||||
|
ButtonView(button: .back)
|
||||||
|
Spacer()
|
||||||
|
ButtonView(button: .start)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Methods
|
||||||
|
|
||||||
|
private func updateOrientation() {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let window = windowScene.windows.first {
|
||||||
|
isPortrait = window.bounds.size.height > window.bounds.size.width
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct ShoulderButtonsViewLeft: View {
|
struct ShoulderButtonsViewLeft: View {
|
||||||
@State var width: CGFloat = 160
|
@State private var width: CGFloat = 160
|
||||||
@State var height: CGFloat = 20
|
@State private var height: CGFloat = 20
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack(spacing: 20) {
|
||||||
ButtonView(button: .leftTrigger)
|
ButtonView(button: .leftTrigger)
|
||||||
.padding(.horizontal)
|
|
||||||
ButtonView(button: .leftShoulder)
|
ButtonView(button: .leftShoulder)
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.frame(width: width, height: height)
|
.frame(width: width, height: height)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
width *= 1.2
|
width *= 1.2
|
||||||
height *= 1.2
|
height *= 1.2
|
||||||
@ -118,19 +185,17 @@ struct ShoulderButtonsViewLeft: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ShoulderButtonsViewRight: View {
|
struct ShoulderButtonsViewRight: View {
|
||||||
@State var width: CGFloat = 160
|
@State private var width: CGFloat = 160
|
||||||
@State var height: CGFloat = 20
|
@State private var height: CGFloat = 20
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack(spacing: 20) {
|
||||||
ButtonView(button: .rightShoulder)
|
ButtonView(button: .rightShoulder)
|
||||||
.padding(.horizontal)
|
|
||||||
ButtonView(button: .rightTrigger)
|
ButtonView(button: .rightTrigger)
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.frame(width: width, height: height)
|
.frame(width: width, height: height)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
width *= 1.2
|
width *= 1.2
|
||||||
height *= 1.2
|
height *= 1.2
|
||||||
@ -143,21 +208,21 @@ struct ShoulderButtonsViewRight: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct DPadView: View {
|
struct DPadView: View {
|
||||||
@State var size: CGFloat = 145
|
@State private var size: CGFloat = 145
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack(spacing: 7) {
|
||||||
ButtonView(button: .dPadUp)
|
ButtonView(button: .dPadUp)
|
||||||
HStack {
|
HStack(spacing: 22) {
|
||||||
ButtonView(button: .dPadLeft)
|
ButtonView(button: .dPadLeft)
|
||||||
Spacer(minLength: 20)
|
Spacer(minLength: 22)
|
||||||
ButtonView(button: .dPadRight)
|
ButtonView(button: .dPadRight)
|
||||||
}
|
}
|
||||||
ButtonView(button: .dPadDown)
|
ButtonView(button: .dPadDown)
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
size *= 1.2
|
size *= 1.2
|
||||||
}
|
}
|
||||||
@ -168,22 +233,21 @@ struct DPadView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ABXYView: View {
|
struct ABXYView: View {
|
||||||
@State var size: CGFloat = 145
|
@State private var size: CGFloat = 145
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack(spacing: 7) {
|
||||||
ButtonView(button: .X)
|
ButtonView(button: .X)
|
||||||
HStack {
|
HStack(spacing: 22) {
|
||||||
ButtonView(button: .Y)
|
ButtonView(button: .Y)
|
||||||
Spacer(minLength: 20)
|
Spacer(minLength: 22)
|
||||||
ButtonView(button: .A)
|
ButtonView(button: .A)
|
||||||
}
|
}
|
||||||
ButtonView(button: .B)
|
ButtonView(button: .B)
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
size *= 1.2
|
size *= 1.2
|
||||||
}
|
}
|
||||||
@ -195,47 +259,102 @@ struct ABXYView: View {
|
|||||||
|
|
||||||
struct ButtonView: View {
|
struct ButtonView: View {
|
||||||
var button: VirtualControllerButton
|
var button: VirtualControllerButton
|
||||||
@State var width: CGFloat = 45
|
@State private var width: CGFloat = 45
|
||||||
@State var height: CGFloat = 45
|
@State private var height: CGFloat = 45
|
||||||
@State var isPressed = false
|
@State private var isPressed = false
|
||||||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
@State private var debounceTimer: Timer?
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Image(systemName: buttonText)
|
Image(systemName: buttonText)
|
||||||
.resizable()
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
.frame(width: width, height: height)
|
.frame(width: width, height: height)
|
||||||
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
|
.foregroundColor(true ? Color.white.opacity(0.5) : Color.black.opacity(0.5))
|
||||||
.opacity(isPressed ? 0.4 : 0.7)
|
.background(
|
||||||
|
Group {
|
||||||
|
if !button.isTrigger && button != .leftStick && button != .rightStick {
|
||||||
|
Circle()
|
||||||
|
.fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
|
||||||
|
.frame(width: width * 1.25, height: height * 1.25)
|
||||||
|
} else if button == .leftStick || button == .rightStick {
|
||||||
|
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))
|
||||||
|
} else if button.isTrigger {
|
||||||
|
Image(systemName: "" + String(turntobutton(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(
|
.gesture(
|
||||||
DragGesture(minimumDistance: 0)
|
DragGesture(minimumDistance: 0)
|
||||||
.onChanged { _ in
|
.onChanged { _ in
|
||||||
if !self.isPressed {
|
handleButtonPress()
|
||||||
self.isPressed = true
|
|
||||||
Ryujinx.shared.virtualController.setButtonState(1, for: button)
|
|
||||||
Haptics.shared.play(.heavy)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
.onEnded { _ in
|
||||||
self.isPressed = false
|
handleButtonRelease()
|
||||||
Ryujinx.shared.virtualController.setButtonState(0, for: button)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder {
|
print(String(buttonText.dropFirst(2)))
|
||||||
width = 65
|
configureSizeForButton()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func turntobutton(_ string: String) -> String {
|
||||||
|
var sting = string
|
||||||
|
if string.hasPrefix("zl") || string.hasPrefix("zr") {
|
||||||
|
sting = String(string.dropFirst(3))
|
||||||
|
} else {
|
||||||
|
sting = String(string.dropFirst(2))
|
||||||
|
}
|
||||||
|
sting = sting.replacingOccurrences(of: "rectangle", with: "button")
|
||||||
|
sting = sting.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
|
||||||
|
|
||||||
if button == .back || button == .start || button == .guide {
|
return sting
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
width = 35
|
||||||
height = 35
|
height = 35
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adjust for iPad
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
width *= 1.2
|
width *= 1.2
|
||||||
height *= 1.2
|
height *= 1.2
|
||||||
@ -244,9 +363,6 @@ struct ButtonView: View {
|
|||||||
width *= CGFloat(controllerScale)
|
width *= CGFloat(controllerScale)
|
||||||
height *= CGFloat(controllerScale)
|
height *= CGFloat(controllerScale)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private var buttonText: String {
|
private var buttonText: String {
|
||||||
switch button {
|
switch button {
|
||||||
@ -258,6 +374,10 @@ struct ButtonView: View {
|
|||||||
return "x.circle.fill"
|
return "x.circle.fill"
|
||||||
case .Y:
|
case .Y:
|
||||||
return "y.circle.fill"
|
return "y.circle.fill"
|
||||||
|
case .leftStick:
|
||||||
|
return "l.joystick.press.down.fill"
|
||||||
|
case .rightStick:
|
||||||
|
return "r.joystick.press.down.fill"
|
||||||
case .dPadUp:
|
case .dPadUp:
|
||||||
return "arrowtriangle.up.circle.fill"
|
return "arrowtriangle.up.circle.fill"
|
||||||
case .dPadDown:
|
case .dPadDown:
|
||||||
@ -267,7 +387,7 @@ struct ButtonView: View {
|
|||||||
case .dPadRight:
|
case .dPadRight:
|
||||||
return "arrowtriangle.right.circle.fill"
|
return "arrowtriangle.right.circle.fill"
|
||||||
case .leftTrigger:
|
case .leftTrigger:
|
||||||
return"zl.rectangle.roundedtop.fill"
|
return "zl.rectangle.roundedtop.fill"
|
||||||
case .rightTrigger:
|
case .rightTrigger:
|
||||||
return "zr.rectangle.roundedtop.fill"
|
return "zr.rectangle.roundedtop.fill"
|
||||||
case .leftShoulder:
|
case .leftShoulder:
|
||||||
@ -275,16 +395,11 @@ struct ButtonView: View {
|
|||||||
case .rightShoulder:
|
case .rightShoulder:
|
||||||
return "r.rectangle.roundedbottom.fill"
|
return "r.rectangle.roundedbottom.fill"
|
||||||
case .start:
|
case .start:
|
||||||
return "plus.circle.fill" // System symbol for +
|
return "plus.circle.fill"
|
||||||
case .back:
|
case .back:
|
||||||
return "minus.circle.fill" // System symbol for -
|
return "minus.circle.fill"
|
||||||
case .guide:
|
case .guide:
|
||||||
return "house.circle.fill"
|
return "house.circle.fill"
|
||||||
// This should be all the cases
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// Joystick.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 21/03/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct Joystick: View {
|
||||||
|
@Binding var position: CGPoint
|
||||||
|
@State var joystickSize: CGFloat
|
||||||
|
var boundarySize: CGFloat
|
||||||
|
|
||||||
|
@State private var offset: CGSize = .zero
|
||||||
|
@Binding var showBackground: Bool
|
||||||
|
|
||||||
|
let sensitivity: CGFloat = 1.5
|
||||||
|
|
||||||
|
var dragGesture: some Gesture {
|
||||||
|
DragGesture()
|
||||||
|
.onChanged { value in
|
||||||
|
withAnimation(.easeIn) {
|
||||||
|
showBackground = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let translation = value.translation
|
||||||
|
let distance = sqrt(translation.width * translation.width + translation.height * translation.height)
|
||||||
|
let maxRadius = (boundarySize - joystickSize) / 2
|
||||||
|
let extendedRadius = maxRadius + (joystickSize / 2)
|
||||||
|
|
||||||
|
if distance <= extendedRadius {
|
||||||
|
offset = translation
|
||||||
|
} else {
|
||||||
|
let angle = atan2(translation.height, translation.width)
|
||||||
|
offset = CGSize(width: cos(angle) * extendedRadius, height: sin(angle) * extendedRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
position = CGPoint(
|
||||||
|
x: max(-1, min(1, (offset.width / extendedRadius) * sensitivity)),
|
||||||
|
y: max(-1, min(1, (offset.height / extendedRadius) * sensitivity))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
offset = .zero
|
||||||
|
position = .zero
|
||||||
|
withAnimation(.easeOut) {
|
||||||
|
showBackground = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.clear.opacity(0))
|
||||||
|
.frame(width: boundarySize, height: boundarySize)
|
||||||
|
|
||||||
|
if showBackground {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.gray.opacity(0.4))
|
||||||
|
.frame(width: boundarySize, height: boundarySize)
|
||||||
|
.animation(.easeInOut(duration: 0.05), value: showBackground)
|
||||||
|
.transition(.scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.fill(Color.white.opacity(0.5))
|
||||||
|
.frame(width: joystickSize, height: joystickSize)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(Color.gray.opacity(0.3))
|
||||||
|
.frame(width: joystickSize * 1.25, height: joystickSize * 1.25)
|
||||||
|
)
|
||||||
|
.offset(offset)
|
||||||
|
.gesture(dragGesture)
|
||||||
|
}
|
||||||
|
.frame(width: boundarySize, height: boundarySize)
|
||||||
|
.onChange(of: showBackground) { newValue in
|
||||||
|
if newValue {
|
||||||
|
joystickSize *= 1.4
|
||||||
|
} else {
|
||||||
|
joystickSize = (boundarySize * 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,13 +7,13 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftUIJoystick
|
|
||||||
|
|
||||||
public struct Joystick: View {
|
struct JoystickController: View {
|
||||||
@State var iscool: Bool? = nil
|
@State var iscool: Bool? = nil
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@ObservedObject public var joystickMonitor = JoystickMonitor()
|
@Binding var showBackground: Bool
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
@State var position: CGPoint = CGPoint(x: 0, y: 0)
|
||||||
var dragDiameter: CGFloat {
|
var dragDiameter: CGFloat {
|
||||||
var selfs = CGFloat(160)
|
var selfs = CGFloat(160)
|
||||||
selfs *= controllerScale
|
selfs *= controllerScale
|
||||||
@ -23,24 +23,11 @@ public struct Joystick: View {
|
|||||||
|
|
||||||
return selfs
|
return selfs
|
||||||
}
|
}
|
||||||
private let shape: JoystickShape = .circle
|
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack{
|
VStack {
|
||||||
JoystickBuilder(
|
Joystick(position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground)
|
||||||
monitor: self.joystickMonitor,
|
.onChange(of: position) { newValue in
|
||||||
width: self.dragDiameter,
|
|
||||||
shape: .circle,
|
|
||||||
background: {
|
|
||||||
Text("")
|
|
||||||
.hidden()
|
|
||||||
},
|
|
||||||
foreground: {
|
|
||||||
Circle().fill(Color.gray)
|
|
||||||
.opacity(0.7)
|
|
||||||
},
|
|
||||||
locksInPlace: false)
|
|
||||||
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
|
|
||||||
let scaledX = Float(newValue.x)
|
let scaledX = Float(newValue.x)
|
||||||
let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/
|
let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/
|
||||||
print("Joystick Position: (\(scaledX), \(scaledY))")
|
print("Joystick Position: (\(scaledX), \(scaledY))")
|
||||||
|
@ -15,7 +15,7 @@ struct MetalView: UIViewRepresentable {
|
|||||||
func makeUIView(context: Context) -> UIView {
|
func makeUIView(context: Context) -> UIView {
|
||||||
|
|
||||||
if Ryujinx.shared.emulationUIView == nil {
|
if Ryujinx.shared.emulationUIView == nil {
|
||||||
var view = MeloMTKView()
|
let view = MeloMTKView()
|
||||||
|
|
||||||
guard let metalLayer = view.layer as? CAMetalLayer else {
|
guard let metalLayer = view.layer as? CAMetalLayer else {
|
||||||
fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer")
|
fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer")
|
||||||
@ -34,13 +34,19 @@ struct MetalView: UIViewRepresentable {
|
|||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
let uiview = UIView()
|
if Double(UIDevice.current.systemVersion)! < 17.0 {
|
||||||
|
|
||||||
uiview.layer.addSublayer(Ryujinx.shared.metalLayer!)
|
let uiview = MTKView()
|
||||||
|
let layer = Ryujinx.shared.metalLayer!
|
||||||
|
|
||||||
uiview.contentScaleFactor = Ryujinx.shared.metalLayer!.contentsScale
|
layer.frame = uiview.bounds
|
||||||
|
|
||||||
|
uiview.layer.addSublayer(layer)
|
||||||
|
|
||||||
return uiview
|
return uiview
|
||||||
|
} else {
|
||||||
|
return Ryujinx.shared.emulationUIView!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIView, context: Context) {
|
func updateUIView(_ uiView: UIView, context: Context) {
|
||||||
|
@ -10,7 +10,7 @@ import MetalKit
|
|||||||
|
|
||||||
struct TouchView: UIViewRepresentable {
|
struct TouchView: UIViewRepresentable {
|
||||||
func makeUIView(context: Context) -> UIView {
|
func makeUIView(context: Context) -> UIView {
|
||||||
var view = MeloMTKView()
|
let view = MeloMTKView()
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,9 @@ struct GameLibraryView: View {
|
|||||||
@State var isViewingGameInfo: Bool = false
|
@State var isViewingGameInfo: Bool = false
|
||||||
@State var isSelectingGameUpdate: Bool = false
|
@State var isSelectingGameUpdate: Bool = false
|
||||||
@State var isSelectingGameDLC: Bool = false
|
@State var isSelectingGameDLC: Bool = false
|
||||||
|
@StateObject var ryujinx = Ryujinx.shared
|
||||||
@State var gameInfo: Game?
|
@State var gameInfo: Game?
|
||||||
|
@State var gameRequirements: [GameRequirements] = []
|
||||||
var games: Binding<[Game]> {
|
var games: Binding<[Game]> {
|
||||||
Binding(
|
Binding(
|
||||||
get: { Ryujinx.shared.games },
|
get: { Ryujinx.shared.games },
|
||||||
@ -78,7 +80,7 @@ struct GameLibraryView: View {
|
|||||||
if !isSearching && !realRecentGames.isEmpty {
|
if !isSearching && !realRecentGames.isEmpty {
|
||||||
Section {
|
Section {
|
||||||
ForEach(realRecentGames) { game in
|
ForEach(realRecentGames) { game in
|
||||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo)
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
removeFromRecentGames(game)
|
removeFromRecentGames(game)
|
||||||
@ -93,14 +95,14 @@ struct GameLibraryView: View {
|
|||||||
|
|
||||||
Section {
|
Section {
|
||||||
ForEach(filteredGames) { game in
|
ForEach(filteredGames) { game in
|
||||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo)
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Others")
|
Text("Others")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ForEach(filteredGames) { game in
|
ForEach(filteredGames) { game in
|
||||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,6 +114,15 @@ struct GameLibraryView: View {
|
|||||||
|
|
||||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||||
firmwareversion = (firmware == "" ? "0" : firmware)
|
firmwareversion = (firmware == "" ? "0" : firmware)
|
||||||
|
|
||||||
|
pullGameCompatibility() { game in
|
||||||
|
switch game {
|
||||||
|
case .success(let sucees):
|
||||||
|
gameRequirements = sucees
|
||||||
|
case .failure(_):
|
||||||
|
print("uhohh stinki")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
|
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
|
||||||
switch result {
|
switch result {
|
||||||
@ -203,6 +214,13 @@ struct GameLibraryView: View {
|
|||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
|
if ryujinx.jitenabled {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: startemu) { game in
|
.onChange(of: startemu) { game in
|
||||||
guard let game else { return }
|
guard let game else { return }
|
||||||
@ -258,7 +276,7 @@ struct GameLibraryView: View {
|
|||||||
let fileExtension = (url.pathExtension as NSString).utf8String
|
let fileExtension = (url.pathExtension as NSString).utf8String
|
||||||
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
||||||
|
|
||||||
var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
||||||
|
|
||||||
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
|
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
|
||||||
|
|
||||||
@ -359,7 +377,6 @@ extension Game: Codable {
|
|||||||
version = try container.decode(String.self, forKey: .version)
|
version = try container.decode(String.self, forKey: .version)
|
||||||
fileURL = try container.decode(URL.self, forKey: .fileURL)
|
fileURL = try container.decode(URL.self, forKey: .fileURL)
|
||||||
|
|
||||||
// Initialize other properties
|
|
||||||
self.containerFolder = fileURL.deletingLastPathComponent()
|
self.containerFolder = fileURL.deletingLastPathComponent()
|
||||||
self.fileType = .item
|
self.fileType = .item
|
||||||
}
|
}
|
||||||
@ -378,10 +395,11 @@ extension Game: Codable {
|
|||||||
struct GameListRow: View {
|
struct GameListRow: View {
|
||||||
let game: Game
|
let game: Game
|
||||||
@Binding var startemu: Game?
|
@Binding var startemu: Game?
|
||||||
@Binding var games: [Game] // Add this binding
|
@Binding var games: [Game]
|
||||||
@Binding var isViewingGameInfo: Bool
|
@Binding var isViewingGameInfo: Bool
|
||||||
@Binding var isSelectingGameUpdate: Bool
|
@Binding var isSelectingGameUpdate: Bool
|
||||||
@Binding var isSelectingGameDLC: Bool
|
@Binding var isSelectingGameDLC: Bool
|
||||||
|
@Binding var gameRequirements: [GameRequirements]
|
||||||
@Binding var gameInfo: Game?
|
@Binding var gameInfo: Game?
|
||||||
@State var gametoDelete: Game?
|
@State var gametoDelete: Game?
|
||||||
@State var showGameDeleteConfirmation: Bool = false
|
@State var showGameDeleteConfirmation: Bool = false
|
||||||
@ -425,8 +443,31 @@ struct GameListRow: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
if let game = gameRequirements.first(where: { $0.game_id == game.titleId }) {
|
||||||
|
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
||||||
|
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
Capsule().fill(game.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.green : Color.red)
|
||||||
|
.frame(width: 70 / 1.5, height: 35 / 1.5)
|
||||||
|
.overlay {
|
||||||
|
Text(game.device_memory)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
}
|
||||||
|
|
||||||
|
Capsule().fill(game.color)
|
||||||
|
.frame(width: 70 / 1.5, height: 35 / 1.5)
|
||||||
|
.overlay {
|
||||||
|
Text(game.compatibility)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.font(.system(size: 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Image(systemName: "play.circle.fill")
|
Image(systemName: "play.circle.fill")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
@ -492,6 +533,7 @@ struct GameListRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func deleteGame(game: Game) {
|
private func deleteGame(game: Game) {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
do {
|
do {
|
||||||
@ -502,3 +544,66 @@ struct GameListRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct GameRequirements: Codable {
|
||||||
|
var game_id: String
|
||||||
|
var compatibility: String
|
||||||
|
var device_memory: String
|
||||||
|
var memoryInt: Int {
|
||||||
|
var devicemem = device_memory
|
||||||
|
devicemem.removeLast(2)
|
||||||
|
print(devicemem)
|
||||||
|
return Int(devicemem) ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var color: Color {
|
||||||
|
switch compatibility {
|
||||||
|
case "Perfect":
|
||||||
|
return .green
|
||||||
|
case "Playable":
|
||||||
|
return .yellow
|
||||||
|
case "Menu":
|
||||||
|
return .orange
|
||||||
|
case "Boots":
|
||||||
|
return .red
|
||||||
|
case "Nothing":
|
||||||
|
return .black
|
||||||
|
default:
|
||||||
|
return .clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pullGameCompatibility(completion: @escaping (Result<[GameRequirements], Error>) -> Void) {
|
||||||
|
if let cachedData = GameCompatibiliryCache.shared.getCachedData() {
|
||||||
|
completion(.success(cachedData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let url = URL(string: "https://melonx.org/api/game_entries") else {
|
||||||
|
completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
completion(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = data else {
|
||||||
|
completion(.failure(NSError(domain: "No data", code: 0, userInfo: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let decodedData = try JSONDecoder().decode([GameRequirements].self, from: data)
|
||||||
|
GameCompatibiliryCache.shared.setCachedData(decodedData)
|
||||||
|
completion(.success(decodedData))
|
||||||
|
} catch {
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
@ -37,6 +37,8 @@ struct JITPopover: View {
|
|||||||
if isJIT {
|
if isJIT {
|
||||||
dismiss()
|
dismiss()
|
||||||
onJITEnabled()
|
onJITEnabled()
|
||||||
|
|
||||||
|
Ryujinx.shared.ryuIsJITEnabled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,13 +96,14 @@ struct LogFileView: View {
|
|||||||
|
|
||||||
private func startLogFileWatching() {
|
private func startLogFileWatching() {
|
||||||
showingLogs = true
|
showingLogs = true
|
||||||
self.readLatestLogFile()
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
||||||
if showingLogs {
|
if showingLogs {
|
||||||
self.readLatestLogFile()
|
self.readLatestLogFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
if isfps {
|
if isfps {
|
||||||
|
sleep(1)
|
||||||
if get_current_fps() != 0 {
|
if get_current_fps() != 0 {
|
||||||
stopLogFileWatching()
|
stopLogFileWatching()
|
||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
|
@ -19,6 +19,7 @@ struct SettingsView: View {
|
|||||||
@AppStorage("useTrollStore") var useTrollStore: Bool = false
|
@AppStorage("useTrollStore") var useTrollStore: Bool = false
|
||||||
|
|
||||||
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
|
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
|
||||||
|
@AppStorage("stikJIT") var stikJIT: Bool = false
|
||||||
|
|
||||||
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
||||||
|
|
||||||
@ -48,11 +49,17 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
@AppStorage("showlogsgame") var showlogsgame: Bool = false
|
@AppStorage("showlogsgame") var showlogsgame: Bool = false
|
||||||
|
|
||||||
|
@AppStorage("stick-button") var stickButton = false
|
||||||
|
@AppStorage("waitForVPN") var waitForVPN = false
|
||||||
|
|
||||||
|
@AppStorage("HideButtons") var hideButtonsJoy = false
|
||||||
|
|
||||||
@State private var showResolutionInfo = false
|
@State private var showResolutionInfo = false
|
||||||
@State private var showAnisotropicInfo = false
|
@State private var showAnisotropicInfo = false
|
||||||
@State private var showControllerInfo = false
|
@State private var showControllerInfo = false
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
@AppStorage("portal") var gamepo = false
|
@AppStorage("portal") var gamepo = false
|
||||||
|
@StateObject var ryujinx = Ryujinx.shared
|
||||||
|
|
||||||
var filteredMemoryModes: [(String, String)] {
|
var filteredMemoryModes: [(String, String)] {
|
||||||
guard !searchText.isEmpty else { return memoryManagerModes }
|
guard !searchText.isEmpty else { return memoryManagerModes }
|
||||||
@ -183,7 +190,7 @@ struct SettingsView: View {
|
|||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
Toggle(isOn: $performacehud) {
|
Toggle(isOn: $performacehud) {
|
||||||
labelWithIcon("Performance Overlay", iconName: "speedometer")
|
labelWithIcon("Custom Performance Overlay", iconName: "speedometer")
|
||||||
}
|
}
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
} header: {
|
} header: {
|
||||||
@ -286,6 +293,11 @@ struct SettingsView: View {
|
|||||||
}.tint(.blue)
|
}.tint(.blue)
|
||||||
|
|
||||||
|
|
||||||
|
Toggle(isOn: $stickButton) {
|
||||||
|
labelWithIcon("Show Stick Buttons", iconName: "l.joystick.press.down")
|
||||||
|
}.tint(.blue)
|
||||||
|
|
||||||
|
|
||||||
Toggle(isOn: $ryuDemo) {
|
Toggle(isOn: $ryuDemo) {
|
||||||
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
|
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
|
||||||
}
|
}
|
||||||
@ -446,14 +458,29 @@ struct SettingsView: View {
|
|||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
|
|
||||||
if #available(iOS 17.0.1, *) {
|
if #available(iOS 17.0.1, *) {
|
||||||
|
// You will stay in our hearts, JitStreamer EB. one of the first public JIT enablers, that didn't need a computer after initial install
|
||||||
|
/*
|
||||||
Toggle(isOn: $jitStreamerEB) {
|
Toggle(isOn: $jitStreamerEB) {
|
||||||
labelWithIcon("JitStreamer EB", iconName: "bolt.heart")
|
labelWithIcon("JitStreamer EB", iconName: "bolt.heart")
|
||||||
}
|
}
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button {
|
Button {
|
||||||
if let mainWindow = UIApplication.shared.windows.last {
|
waitForVPN.toggle()
|
||||||
let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best iOS developers of all time jkcoxson <3", preferredStyle: .alert)
|
} 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
|
let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
|
||||||
UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
|
UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
|
||||||
@ -469,6 +496,32 @@ struct SettingsView: View {
|
|||||||
Text("About")
|
Text("About")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Toggle(isOn: $stikJIT) {
|
||||||
|
labelWithIcon("StikJIT", iconName: "bolt.heart")
|
||||||
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let mainWindow = windowScene.windows.last {
|
||||||
|
let alertController = UIAlertController(title: "About StikJIT", message: "StikJIT is a really amazing iOS Application to Enable JIT on the go on-device, made by the best, most kind, helpful and nice developers of all time jkcoxson and Blu <3", preferredStyle: .alert)
|
||||||
|
|
||||||
|
let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
|
||||||
|
UIApplication.shared.open(URL(string: "https://github.com/0-Blu/StikJIT")!)
|
||||||
|
}
|
||||||
|
alertController.addAction(learnMoreButton)
|
||||||
|
|
||||||
|
let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil)
|
||||||
|
alertController.addAction(doneButton)
|
||||||
|
|
||||||
|
mainWindow.rootViewController?.present(alertController, animated: true)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("About")
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Toggle(isOn: $useTrollStore) {
|
Toggle(isOn: $useTrollStore) {
|
||||||
labelWithIcon("TrollStore JIT", iconName: "troll.svg")
|
labelWithIcon("TrollStore JIT", iconName: "troll.svg")
|
||||||
@ -481,7 +534,8 @@ struct SettingsView: View {
|
|||||||
}.tint(.blue)
|
}.tint(.blue)
|
||||||
.contextMenu() {
|
.contextMenu() {
|
||||||
Button {
|
Button {
|
||||||
if let mainWindow = UIApplication.shared.windows.last {
|
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 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)
|
let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil)
|
||||||
@ -529,27 +583,29 @@ struct SettingsView: View {
|
|||||||
Section {
|
Section {
|
||||||
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
||||||
let model = getDeviceModel()
|
let model = getDeviceModel()
|
||||||
let deviceType = model.hasPrefix("iPad") ? "iPadOS" :
|
|
||||||
model.hasPrefix("iPhone") ? "iOS" :
|
|
||||||
"macOS"
|
|
||||||
|
|
||||||
let iconName = model.hasPrefix("iPad") ? "ipad.landscape" :
|
let iconName = model.hasPrefix("iPad") ? "ipad.landscape" :
|
||||||
model.hasPrefix("iPhone") ? "iphone" :
|
model.hasPrefix("iPhone") ? "iphone" :
|
||||||
"macwindow"
|
"macwindow"
|
||||||
|
|
||||||
labelWithIcon("JIT Acquisition: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
|
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("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
|
||||||
|
|
||||||
labelWithIcon("Device: \(getDeviceModel())", iconName: iconName)
|
labelWithIcon("Device: \(UIDevice.modelName)", iconName: iconName)
|
||||||
|
|
||||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||||
labelWithIcon("Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill")
|
labelWithIcon("Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill")
|
||||||
} else {
|
} else {
|
||||||
labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill")
|
labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill")
|
||||||
}
|
}
|
||||||
|
|
||||||
labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo")
|
|
||||||
|
labelWithIcon("\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", iconName: "applelogo")
|
||||||
|
|
||||||
} header: {
|
} header: {
|
||||||
Text("Information")
|
Text("Information")
|
||||||
@ -564,10 +620,6 @@ struct SettingsView: View {
|
|||||||
Section {
|
Section {
|
||||||
DisclosureGroup {
|
DisclosureGroup {
|
||||||
|
|
||||||
Toggle(isOn: $mVKPreFillBuffer) {
|
|
||||||
labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape")
|
|
||||||
}.tint(.blue)
|
|
||||||
|
|
||||||
Toggle(isOn: $config.dfsIntegrityChecks) {
|
Toggle(isOn: $config.dfsIntegrityChecks) {
|
||||||
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
|
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
|
||||||
}.tint(.blue)
|
}.tint(.blue)
|
||||||
@ -577,7 +629,16 @@ struct SettingsView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
Text("\(String(Int(getpagesize())))")
|
Text("\(String(Int(getpagesize())))")
|
||||||
.foregroundColor(.secondary)
|
.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) {
|
Toggle(isOn: $ignoreJIT) {
|
||||||
@ -617,7 +678,7 @@ struct SettingsView: View {
|
|||||||
.textCase(nil)
|
.textCase(nil)
|
||||||
.headerProminence(.increased)
|
.headerProminence(.increased)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). \n \n\(gamepo ? "the cake is a lie" : "")")
|
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" : "")")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -626,6 +687,8 @@ struct SettingsView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
mVKPreFillBuffer = false
|
||||||
|
|
||||||
if let configs = loadSettings() {
|
if let configs = loadSettings() {
|
||||||
self.config = configs
|
self.config = configs
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// MeloNXUpdateSheet.swift
|
// MeloNXUpdateSheet.swift
|
||||||
// MeloNX
|
// MeloNX
|
||||||
//
|
//
|
||||||
// Created by Bella on 12/03/2025.
|
// Created by Stossy11 and Bella on 12/03/2025.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@ -58,7 +58,3 @@ struct MeloNXUpdateSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
|
||||||
MeloNXUpdateSheet(updateInfo: LatestVersionResponse.example1, isPresented: .constant(true))
|
|
||||||
}
|
|
@ -1,168 +0,0 @@
|
|||||||
//
|
|
||||||
// GameDLCManagerSheet.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by XITRIX on 16/02/2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import UniformTypeIdentifiers
|
|
||||||
|
|
||||||
struct DownloadableContentNca: Codable, Hashable {
|
|
||||||
var fullPath: String
|
|
||||||
var titleId: UInt
|
|
||||||
var enabled: Bool
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case fullPath = "path"
|
|
||||||
case titleId = "title_id"
|
|
||||||
case enabled = "is_enabled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DownloadableContentContainer: Codable, Hashable {
|
|
||||||
var containerPath: String
|
|
||||||
var downloadableContentNcaList: [DownloadableContentNca]
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case containerPath = "path"
|
|
||||||
case downloadableContentNcaList = "dlc_nca_list"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DLCManagerSheet: View {
|
|
||||||
@Binding var game: Game!
|
|
||||||
@State private var isSelectingGameDLC = false
|
|
||||||
@State private var dlcs: [DownloadableContentContainer] = []
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
let withIndex = dlcs.enumerated().map { $0 }
|
|
||||||
List(withIndex, id: \.element.containerPath) { index, dlc in
|
|
||||||
Button(action: {
|
|
||||||
let toggle = dlcs[index].downloadableContentNcaList.first?.enabled ?? true
|
|
||||||
dlcs[index].downloadableContentNcaList.mutableForEach { $0.enabled = !toggle }
|
|
||||||
Self.saveDlcs(game, dlc: dlcs)
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Text((dlc.containerPath as NSString).lastPathComponent)
|
|
||||||
.foregroundStyle(Color(uiColor: .label))
|
|
||||||
Spacer()
|
|
||||||
if dlc.downloadableContentNcaList.first?.enabled == true {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
.font(.system(size: 24))
|
|
||||||
} else {
|
|
||||||
Image(systemName: "circle")
|
|
||||||
.foregroundStyle(Color(uiColor: .secondaryLabel))
|
|
||||||
.font(.system(size: 24))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contextMenu {
|
|
||||||
Button {
|
|
||||||
let path = URL.documentsDirectory.appendingPathComponent(dlc.containerPath)
|
|
||||||
try? FileManager.default.removeItem(atPath: path.path)
|
|
||||||
dlcs.remove(at: index)
|
|
||||||
Self.saveDlcs(game, dlc: dlcs)
|
|
||||||
} label: {
|
|
||||||
Text("Remove DLC")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle("\(game.titleName) DLCs")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
Button("Add", systemImage: "plus") {
|
|
||||||
isSelectingGameDLC = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
dlcs = Self.loadDlc(game)
|
|
||||||
}
|
|
||||||
.fileImporter(isPresented: $isSelectingGameDLC, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let urls):
|
|
||||||
for url in urls {
|
|
||||||
guard url.startAccessingSecurityScopedResource() else {
|
|
||||||
print("Failed to access security-scoped resource")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer { url.stopAccessingSecurityScopedResource() }
|
|
||||||
|
|
||||||
do {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
||||||
let dlcDirectory = documentsDirectory.appendingPathComponent("dlc")
|
|
||||||
let romDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
|
|
||||||
|
|
||||||
if !fileManager.fileExists(atPath: dlcDirectory.path) {
|
|
||||||
try fileManager.createDirectory(at: dlcDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fileManager.fileExists(atPath: romDlcDirectory.path) {
|
|
||||||
try fileManager.createDirectory(at: romDlcDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: url.path)
|
|
||||||
guard !dlcContent.isEmpty else { return }
|
|
||||||
|
|
||||||
let destinationURL = romDlcDirectory.appendingPathComponent(url.lastPathComponent)
|
|
||||||
try? fileManager.copyItem(at: url, to: destinationURL)
|
|
||||||
|
|
||||||
let container = DownloadableContentContainer(
|
|
||||||
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
|
|
||||||
downloadableContentNcaList: dlcContent
|
|
||||||
)
|
|
||||||
dlcs.append(container)
|
|
||||||
|
|
||||||
Self.saveDlcs(game, dlc: dlcs)
|
|
||||||
} catch {
|
|
||||||
print("Error copying game file: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .failure(let err):
|
|
||||||
print("File import failed: \(err.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension DLCManagerSheet {
|
|
||||||
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
|
|
||||||
let jsonURL = dlcJsonPath(for: game)
|
|
||||||
guard let data = try? Data(contentsOf: jsonURL),
|
|
||||||
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
|
|
||||||
else { return [] }
|
|
||||||
|
|
||||||
result = result.filter { container in
|
|
||||||
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
|
|
||||||
return FileManager.default.fileExists(atPath: path.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
|
|
||||||
guard let data = try? JSONEncoder().encode(dlc) else { return }
|
|
||||||
try? data.write(to: dlcJsonPath(for: game))
|
|
||||||
}
|
|
||||||
|
|
||||||
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
|
|
||||||
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
|
|
||||||
}
|
|
||||||
|
|
||||||
static func dlcJsonPath(for game: Game) -> URL {
|
|
||||||
URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game.titleId).appendingPathComponent("dlc.json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extension URL {
|
|
||||||
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
|
|
||||||
static var documentsDirectory: URL {
|
|
||||||
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
||||||
return documentDirectory
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,201 +0,0 @@
|
|||||||
//
|
|
||||||
// GameUpdateManagerSheet.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 16/02/2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import UniformTypeIdentifiers
|
|
||||||
|
|
||||||
struct UpdateManagerSheet: View {
|
|
||||||
@State private var items: [String] = []
|
|
||||||
@State private var paths: [URL] = []
|
|
||||||
@State private var selectedItem: String? = nil
|
|
||||||
@Binding var game: Game?
|
|
||||||
@State private var isSelectingGameUpdate = false
|
|
||||||
@State private var jsonURL: URL? = nil
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationView {
|
|
||||||
List(paths, id: \..self, selection: $selectedItem) { item in
|
|
||||||
Button(action: {
|
|
||||||
selectItem(item.lastPathComponent)
|
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Text(item.lastPathComponent)
|
|
||||||
.foregroundStyle(Color(uiColor: .label))
|
|
||||||
Spacer()
|
|
||||||
if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
.font(.system(size: 24))
|
|
||||||
} else {
|
|
||||||
Image(systemName: "circle")
|
|
||||||
.foregroundStyle(Color(uiColor: .secondaryLabel))
|
|
||||||
.font(.system(size: 24))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contextMenu {
|
|
||||||
Button {
|
|
||||||
removeUpdate(item)
|
|
||||||
} label: {
|
|
||||||
Text("Remove Update")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
|
|
||||||
|
|
||||||
loadJSON(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
|
|
||||||
}
|
|
||||||
.navigationTitle("\(game!.titleName) Updates")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
Button("Add", systemImage: "plus") {
|
|
||||||
isSelectingGameUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item]) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let url):
|
|
||||||
guard url.startAccessingSecurityScopedResource() else {
|
|
||||||
print("Failed to access security-scoped resource")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer { url.stopAccessingSecurityScopedResource() }
|
|
||||||
|
|
||||||
let gameInfo = game!
|
|
||||||
|
|
||||||
do {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
||||||
let updatedDirectory = documentsDirectory.appendingPathComponent("updates")
|
|
||||||
let romUpdatedDirectory = updatedDirectory.appendingPathComponent(gameInfo.titleId)
|
|
||||||
|
|
||||||
if !fileManager.fileExists(atPath: updatedDirectory.path) {
|
|
||||||
try fileManager.createDirectory(at: updatedDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fileManager.fileExists(atPath: romUpdatedDirectory.path) {
|
|
||||||
try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
|
|
||||||
try? fileManager.copyItem(at: url, to: destinationURL)
|
|
||||||
|
|
||||||
items.append("updates/" + gameInfo.titleId + "/" + url.lastPathComponent)
|
|
||||||
selectItem(url.lastPathComponent)
|
|
||||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
|
||||||
loadJSON(jsonURL!)
|
|
||||||
} catch {
|
|
||||||
print("Error copying game file: \(error)")
|
|
||||||
}
|
|
||||||
case .failure(let err):
|
|
||||||
print("File import failed: \(err.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeUpdate(_ game: URL) {
|
|
||||||
let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)"
|
|
||||||
paths.removeAll { $0 == game }
|
|
||||||
items.removeAll { $0 == gameString }
|
|
||||||
|
|
||||||
if selectedItem == gameString {
|
|
||||||
selectedItem = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try FileManager.default.removeItem(at: game)
|
|
||||||
} catch {
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
saveJSON(selectedItem: selectedItem ?? "")
|
|
||||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveJSON(selectedItem: String?) {
|
|
||||||
guard let jsonURL = jsonURL else { return }
|
|
||||||
do {
|
|
||||||
let jsonDict = ["paths": items, "selected": selectedItem ?? self.selectedItem ?? ""] as [String: Any]
|
|
||||||
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
|
||||||
try newData.write(to: jsonURL)
|
|
||||||
} catch {
|
|
||||||
print("Failed to update JSON: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadJSON(_ json: URL) {
|
|
||||||
self.jsonURL = json
|
|
||||||
|
|
||||||
guard let jsonURL else { return }
|
|
||||||
|
|
||||||
do {
|
|
||||||
let data = try Data(contentsOf: jsonURL)
|
|
||||||
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
|
||||||
let list = jsonDict["paths"] as? [String]
|
|
||||||
{
|
|
||||||
|
|
||||||
let filteredList = list.filter { relativePath in
|
|
||||||
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
|
|
||||||
return FileManager.default.fileExists(atPath: path.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
let urls: [URL] = filteredList.map { relativePath in
|
|
||||||
URL.documentsDirectory.appendingPathComponent(relativePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
items = filteredList
|
|
||||||
paths = urls
|
|
||||||
selectedItem = jsonDict["selected"] as? String
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print("Failed to read JSON: \(error)")
|
|
||||||
createDefaultJSON()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDefaultJSON() {
|
|
||||||
guard let jsonURL = jsonURL else { return }
|
|
||||||
let defaultData: [String: Any] = ["selected": "", "paths": []]
|
|
||||||
do {
|
|
||||||
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
|
|
||||||
try newData.write(to: jsonURL)
|
|
||||||
items = []
|
|
||||||
selectedItem = ""
|
|
||||||
} catch {
|
|
||||||
print("Failed to create default JSON: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectItem(_ item: String) {
|
|
||||||
let newSelection = "updates/\(game!.titleId)/\(item)"
|
|
||||||
|
|
||||||
guard let jsonURL else { return }
|
|
||||||
|
|
||||||
do {
|
|
||||||
let data = try Data(contentsOf: jsonURL)
|
|
||||||
var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
|
|
||||||
|
|
||||||
if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection {
|
|
||||||
jsonDict["selected"] = ""
|
|
||||||
selectedItem = ""
|
|
||||||
} else {
|
|
||||||
jsonDict["selected"] = "\(newSelection)"
|
|
||||||
selectedItem = newSelection
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonDict["paths"] = items
|
|
||||||
|
|
||||||
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
|
||||||
try newData.write(to: jsonURL)
|
|
||||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
|
||||||
} catch {
|
|
||||||
print("Failed to update JSON: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,308 @@
|
|||||||
|
//
|
||||||
|
// GameDLCManagerSheet.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by XITRIX on 16/02/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
// MARK: - Models
|
||||||
|
struct DownloadableContentNca: Codable, Hashable {
|
||||||
|
var fullPath: String
|
||||||
|
var titleId: UInt
|
||||||
|
var enabled: Bool
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case fullPath = "path"
|
||||||
|
case titleId = "title_id"
|
||||||
|
case enabled = "is_enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DownloadableContentContainer: Codable, Hashable, Identifiable {
|
||||||
|
var id: String { containerPath }
|
||||||
|
var containerPath: String
|
||||||
|
var downloadableContentNcaList: [DownloadableContentNca]
|
||||||
|
|
||||||
|
var filename: String {
|
||||||
|
(containerPath as NSString).lastPathComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEnabled: Bool {
|
||||||
|
downloadableContentNcaList.first?.enabled == true
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case containerPath = "path"
|
||||||
|
case downloadableContentNcaList = "dlc_nca_list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - View
|
||||||
|
struct DLCManagerSheet: View {
|
||||||
|
// MARK: - Properties
|
||||||
|
@Binding var game: Game!
|
||||||
|
@State private var isSelectingGameDLC = false
|
||||||
|
@State private var dlcs: [DownloadableContentContainer] = []
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
var body: some View {
|
||||||
|
iOSNav {
|
||||||
|
List {
|
||||||
|
if dlcs.isEmpty {
|
||||||
|
emptyStateView
|
||||||
|
} else {
|
||||||
|
ForEach(dlcs) { dlc in
|
||||||
|
dlcRow(dlc)
|
||||||
|
}
|
||||||
|
.onDelete(perform: removeDLCs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("\(game.titleName) DLCs")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Done") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
isSelectingGameDLC = true
|
||||||
|
} label: {
|
||||||
|
Label("Add DLC", systemImage: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fileImporter(
|
||||||
|
isPresented: $isSelectingGameDLC,
|
||||||
|
allowedContentTypes: [.item],
|
||||||
|
allowsMultipleSelection: true,
|
||||||
|
onCompletion: handleFileImport
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Views
|
||||||
|
private var emptyStateView: some View {
|
||||||
|
Group {
|
||||||
|
if #available(iOS 17, *) {
|
||||||
|
ContentUnavailableView(
|
||||||
|
"No DLCs Found",
|
||||||
|
systemImage: "puzzlepiece.extension",
|
||||||
|
description: Text("Tap the + button to add game DLCs.")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "puzzlepiece.extension")
|
||||||
|
.font(.system(size: 64))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text("No DLCs Found")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
Text("Tap the + button to add game DLCs.")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.listRowInsets(EdgeInsets())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
|
||||||
|
Button {
|
||||||
|
toggleDLC(dlc)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(dlc.filename)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
|
||||||
|
.foregroundStyle(dlc.isEnabled ? .primary : .secondary)
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.swipeActions(edge: .trailing) {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
|
||||||
|
removeDLC(at: IndexSet(integer: index))
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Functions
|
||||||
|
private func loadData() {
|
||||||
|
dlcs = Self.loadDlc(game)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleDLC(_ dlc: DownloadableContentContainer) {
|
||||||
|
guard let index = dlcs.firstIndex(where: { $0.id == dlc.id }) else { return }
|
||||||
|
|
||||||
|
let toggle = !dlcs[index].isEnabled
|
||||||
|
dlcs[index].downloadableContentNcaList = dlcs[index].downloadableContentNcaList.map { nca in
|
||||||
|
var mutableNca = nca
|
||||||
|
mutableNca.enabled = toggle
|
||||||
|
return mutableNca
|
||||||
|
}
|
||||||
|
|
||||||
|
Self.saveDlcs(game, dlc: dlcs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeDLCs(at offsets: IndexSet) {
|
||||||
|
offsets.forEach { removeDLC(at: IndexSet(integer: $0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeDLC(at indexSet: IndexSet) {
|
||||||
|
guard let index = indexSet.first else { return }
|
||||||
|
|
||||||
|
let dlcToRemove = dlcs[index]
|
||||||
|
let path = URL.documentsDirectory.appendingPathComponent(dlcToRemove.containerPath)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: path)
|
||||||
|
dlcs.remove(at: index)
|
||||||
|
Self.saveDlcs(game, dlc: dlcs)
|
||||||
|
} catch {
|
||||||
|
print("Failed to remove DLC: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleFileImport(result: Result<[URL], Error>) {
|
||||||
|
switch result {
|
||||||
|
case .success(let urls):
|
||||||
|
for url in urls {
|
||||||
|
importDLC(from: url)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
print("File import failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func importDLC(from url: URL) {
|
||||||
|
guard url.startAccessingSecurityScopedResource() else {
|
||||||
|
print("Failed to access security-scoped resource")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { url.stopAccessingSecurityScopedResource() }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let dlcDirectory = URL.documentsDirectory.appendingPathComponent("dlc")
|
||||||
|
let gameDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
|
||||||
|
|
||||||
|
try fileManager.createDirectory(at: gameDlcDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
// Copy the DLC file
|
||||||
|
let destinationURL = gameDlcDirectory.appendingPathComponent(url.lastPathComponent)
|
||||||
|
try? fileManager.removeItem(at: destinationURL)
|
||||||
|
try fileManager.copyItem(at: url, to: destinationURL)
|
||||||
|
|
||||||
|
// Fetch DLC metadata from Ryujinx
|
||||||
|
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: destinationURL.path)
|
||||||
|
guard !dlcContent.isEmpty else {
|
||||||
|
print("No valid DLC content found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let newDlcContainer = DownloadableContentContainer(
|
||||||
|
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
|
||||||
|
downloadableContentNcaList: dlcContent
|
||||||
|
)
|
||||||
|
|
||||||
|
dlcs.append(newDlcContainer)
|
||||||
|
Self.saveDlcs(game, dlc: dlcs)
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print("Error importing DLC: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
private extension DLCManagerSheet {
|
||||||
|
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
|
||||||
|
let jsonURL = dlcJsonPath(for: game)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
guard FileManager.default.fileExists(atPath: jsonURL.path),
|
||||||
|
let data = try? Data(contentsOf: jsonURL),
|
||||||
|
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
|
||||||
|
else { return [] }
|
||||||
|
|
||||||
|
result = result.filter { container in
|
||||||
|
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
|
||||||
|
return FileManager.default.fileExists(atPath: path.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
print("Error loading DLCs: \(error)")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
|
||||||
|
do {
|
||||||
|
let data = try JSONEncoder().encode(dlc)
|
||||||
|
try data.write(to: dlcJsonPath(for: game))
|
||||||
|
} catch {
|
||||||
|
print("Error saving DLCs: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
|
||||||
|
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func dlcJsonPath(for game: Game) -> URL {
|
||||||
|
URL.documentsDirectory
|
||||||
|
.appendingPathComponent("games")
|
||||||
|
.appendingPathComponent(game.titleId)
|
||||||
|
.appendingPathComponent("dlc.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Array Extension
|
||||||
|
extension Array where Element: AnyObject {
|
||||||
|
mutating func mutableForEach(_ body: (inout Element) -> Void) {
|
||||||
|
for index in indices {
|
||||||
|
var element = self[index]
|
||||||
|
body(&element)
|
||||||
|
self[index] = element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - URL Extension
|
||||||
|
extension URL {
|
||||||
|
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
|
||||||
|
static var documentsDirectory: URL {
|
||||||
|
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
|
return documentDirectory
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,299 @@
|
|||||||
|
//
|
||||||
|
// GameUpdateManagerSheet.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 16/02/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
struct UpdateManagerSheet: View {
|
||||||
|
// MARK: - Properties
|
||||||
|
@State private var updates: [UpdateItem] = []
|
||||||
|
@Binding var game: Game?
|
||||||
|
@State private var isSelectingGameUpdate = false
|
||||||
|
@State private var jsonURL: URL? = nil
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
// MARK: - Models
|
||||||
|
class UpdateItem: Identifiable, ObservableObject {
|
||||||
|
let id = UUID()
|
||||||
|
let url: URL
|
||||||
|
let filename: String
|
||||||
|
let path: String
|
||||||
|
|
||||||
|
@Published var isSelected: Bool = false
|
||||||
|
|
||||||
|
init(url: URL, filename: String, path: String, isSelected: Bool = false) {
|
||||||
|
self.url = url
|
||||||
|
self.filename = filename
|
||||||
|
self.path = path
|
||||||
|
self.isSelected = isSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
|
var body: some View {
|
||||||
|
iOSNav {
|
||||||
|
List {
|
||||||
|
if updates.isEmpty {
|
||||||
|
emptyStateView
|
||||||
|
} else {
|
||||||
|
ForEach(updates) { update in
|
||||||
|
updateRow(update)
|
||||||
|
}
|
||||||
|
.onDelete(perform: removeUpdates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("\(game?.titleName ?? "Game") Updates")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Done") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
isSelectingGameUpdate = true
|
||||||
|
} label: {
|
||||||
|
Label("Add Update", systemImage: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item], onCompletion: handleFileImport)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Views
|
||||||
|
private var emptyStateView: some View {
|
||||||
|
Group {
|
||||||
|
if #available(iOS 17, *) {
|
||||||
|
ContentUnavailableView(
|
||||||
|
"No Updates Found",
|
||||||
|
systemImage: "arrow.down.circle",
|
||||||
|
description: Text("Tap the + button to add game updates.")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "arrow.down.circle")
|
||||||
|
.font(.system(size: 64))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text("No Updates Found")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
Text("Tap the + button to add game updates.")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.listRowInsets(EdgeInsets())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateRow(_ update: UpdateItem) -> some View {
|
||||||
|
Button {
|
||||||
|
toggleSelection(update)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(update.filename)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
|
||||||
|
.foregroundStyle(update.isSelected ? .primary : .secondary)
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.swipeActions(edge: .trailing) {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
if let index = updates.firstIndex(where: { $0.path == update.path }) {
|
||||||
|
removeUpdate(at: IndexSet(integer: index))
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Functions
|
||||||
|
private func loadData() {
|
||||||
|
guard let game = game else { return }
|
||||||
|
|
||||||
|
let documentsDirectory = URL.documentsDirectory
|
||||||
|
jsonURL = documentsDirectory
|
||||||
|
.appendingPathComponent("games")
|
||||||
|
.appendingPathComponent(game.titleId)
|
||||||
|
.appendingPathComponent("updates.json")
|
||||||
|
|
||||||
|
loadJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadJSON() {
|
||||||
|
guard let jsonURL = jsonURL else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
if !FileManager.default.fileExists(atPath: jsonURL.path) {
|
||||||
|
createDefaultJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = try Data(contentsOf: jsonURL)
|
||||||
|
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||||
|
let paths = jsonDict["paths"] as? [String],
|
||||||
|
let selected = jsonDict["selected"] as? String {
|
||||||
|
|
||||||
|
let filteredPaths = paths.filter { relativePath in
|
||||||
|
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
|
||||||
|
return FileManager.default.fileExists(atPath: path.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
updates = filteredPaths.map { relativePath in
|
||||||
|
let url = URL.documentsDirectory.appendingPathComponent(relativePath)
|
||||||
|
return UpdateItem(
|
||||||
|
url: url,
|
||||||
|
filename: url.lastPathComponent,
|
||||||
|
path: relativePath,
|
||||||
|
isSelected: selected == relativePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Failed to read JSON: \(error)")
|
||||||
|
createDefaultJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createDefaultJSON() {
|
||||||
|
guard let jsonURL = jsonURL else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
let defaultData: [String: Any] = ["selected": "", "paths": []]
|
||||||
|
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
|
||||||
|
try newData.write(to: jsonURL)
|
||||||
|
updates = []
|
||||||
|
} catch {
|
||||||
|
print("Failed to create default JSON: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleFileImport(result: Result<URL, Error>) {
|
||||||
|
switch result {
|
||||||
|
case .success(let selectedURL):
|
||||||
|
guard let game = game,
|
||||||
|
selectedURL.startAccessingSecurityScopedResource() else {
|
||||||
|
print("Failed to access security-scoped resource")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer { selectedURL.stopAccessingSecurityScopedResource() }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let updatesDirectory = URL.documentsDirectory.appendingPathComponent("updates")
|
||||||
|
let gameUpdatesDirectory = updatesDirectory.appendingPathComponent(game.titleId)
|
||||||
|
|
||||||
|
// Create directories if needed
|
||||||
|
try fileManager.createDirectory(at: gameUpdatesDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
// Copy the file
|
||||||
|
let destinationURL = gameUpdatesDirectory.appendingPathComponent(selectedURL.lastPathComponent)
|
||||||
|
try? fileManager.removeItem(at: destinationURL) // Remove if exists
|
||||||
|
try fileManager.copyItem(at: selectedURL, to: destinationURL)
|
||||||
|
|
||||||
|
// Add to updates
|
||||||
|
let relativePath = "updates/\(game.titleId)/\(selectedURL.lastPathComponent)"
|
||||||
|
let newUpdate = UpdateItem(
|
||||||
|
url: destinationURL,
|
||||||
|
filename: selectedURL.lastPathComponent,
|
||||||
|
path: relativePath
|
||||||
|
)
|
||||||
|
|
||||||
|
updates.append(newUpdate)
|
||||||
|
toggleSelection(newUpdate)
|
||||||
|
|
||||||
|
// Reload games
|
||||||
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
|
} catch {
|
||||||
|
print("Error copying update file: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
print("File import failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleSelection(_ update: UpdateItem) {
|
||||||
|
print("toggle selection \(update.path)")
|
||||||
|
|
||||||
|
updates = updates.map { item in
|
||||||
|
var mutableItem = item
|
||||||
|
mutableItem.isSelected = item.path == update.path && !update.isSelected
|
||||||
|
print(mutableItem.isSelected)
|
||||||
|
print(update.isSelected)
|
||||||
|
return mutableItem
|
||||||
|
}
|
||||||
|
|
||||||
|
print(updates)
|
||||||
|
|
||||||
|
saveJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeUpdates(at offsets: IndexSet) {
|
||||||
|
offsets.forEach { removeUpdate(at: IndexSet(integer: $0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeUpdate(at indexSet: IndexSet) {
|
||||||
|
guard let index = indexSet.first else { return }
|
||||||
|
|
||||||
|
let updateToRemove = updates[index]
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Remove the file
|
||||||
|
try FileManager.default.removeItem(at: updateToRemove.url)
|
||||||
|
|
||||||
|
// Remove from updates array
|
||||||
|
updates.remove(at: index)
|
||||||
|
|
||||||
|
// Save changes
|
||||||
|
saveJSON()
|
||||||
|
|
||||||
|
// Reload games
|
||||||
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
|
} catch {
|
||||||
|
print("Failed to remove update: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveJSON() {
|
||||||
|
guard let jsonURL = jsonURL else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let paths = updates.map { $0.path }
|
||||||
|
let selected = updates.first(where: { $0.isSelected })?.path ?? ""
|
||||||
|
|
||||||
|
let jsonDict = ["paths": paths, "selected": selected] as [String: Any]
|
||||||
|
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
||||||
|
try newData.write(to: jsonURL)
|
||||||
|
} catch {
|
||||||
|
print("Failed to update JSON: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,39 +8,41 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import Alamofire
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
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 {
|
||||||
@Environment(\.scenePhase) var scenePhase
|
|
||||||
@State var finished = false
|
|
||||||
|
|
||||||
// Variables for the update system :)
|
@State var showed = false
|
||||||
|
@Environment(\.scenePhase) var scenePhase
|
||||||
|
@State var alert: UIAlertController? = nil
|
||||||
|
|
||||||
@State var showOutOfDateSheet = false
|
@State var showOutOfDateSheet = false
|
||||||
@State var updateInfo: LatestVersionResponse? = nil
|
@State var updateInfo: LatestVersionResponse? = nil
|
||||||
|
|
||||||
|
@State var finished = false
|
||||||
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
|
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))!
|
||||||
|
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))!
|
||||||
|
method_exchangeImplementations(origMethod, fixMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
VStack {
|
|
||||||
if finishedStorage {
|
if finishedStorage {
|
||||||
ContentView()
|
ContentView()
|
||||||
} else {
|
|
||||||
SetupView(finished: $finished)
|
|
||||||
.onChange(of: finished) { newValue in
|
|
||||||
withAnimation {
|
|
||||||
withAnimation {
|
|
||||||
finishedStorage = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
checkLatestVersion()
|
checkLatestVersion()
|
||||||
}
|
}
|
||||||
// this seems like a weird way to show the sheet but, from my history this is the most reliable way for the content to actually show in the sheet, otherwise its blank
|
|
||||||
.sheet(isPresented: Binding(
|
.sheet(isPresented: Binding(
|
||||||
get: { showOutOfDateSheet && updateInfo != nil },
|
get: { showOutOfDateSheet && updateInfo != nil },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
@ -54,55 +56,60 @@ struct MeloNXApp: App {
|
|||||||
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
|
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
SetupView(finished: $finished)
|
||||||
|
.onChange(of: finished) { newValue in
|
||||||
|
withAnimation {
|
||||||
|
withAnimation {
|
||||||
|
finishedStorage = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sends a GET request to the MeloNXSite API and compares the version it returns to the current app version
|
|
||||||
func checkLatestVersion() {
|
func checkLatestVersion() {
|
||||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
||||||
let strippedAppVersion = appVersion.replacingOccurrences(of: ".", with: "")
|
let strippedAppVersion = appVersion.replacingOccurrences(of: ".", with: "")
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// no this isnt a public ip address silly viewers (i know damn well someone thought this was my real ip), this is local :PP
|
let urlString = "http://192.168.178.116:8000/api/latest_release"
|
||||||
let url = "http://192.168.178.116:8000/api/latest_release"
|
|
||||||
#else
|
#else
|
||||||
// dont spam this :pray:
|
let urlString = "https://melonx.org/api/latest_release"
|
||||||
let url = "https://melonx.org/api/latest_release"
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// actually sends the request
|
guard let url = URL(string: urlString) else {
|
||||||
AF.request(url).responseDecodable(of: LatestVersionResponse.self) { response in
|
print("Invalid URL")
|
||||||
switch response.result {
|
return
|
||||||
case .success(let latestVersionResponse):
|
}
|
||||||
|
|
||||||
|
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
print("Error checking for new version: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = data else {
|
||||||
|
print("No data received")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let latestVersionResponse = try JSONDecoder().decode(LatestVersionResponse.self, from: data)
|
||||||
let latestAPIVersionStripped = latestVersionResponse.version_number_stripped
|
let latestAPIVersionStripped = latestVersionResponse.version_number_stripped
|
||||||
if Int(strippedAppVersion) ?? 0 < Int(latestAPIVersionStripped) ?? 0 {
|
|
||||||
|
if Int(strippedAppVersion) ?? 0 > Int(latestAPIVersionStripped) ?? 0 {
|
||||||
|
DispatchQueue.main.async {
|
||||||
updateInfo = latestVersionResponse
|
updateInfo = latestVersionResponse
|
||||||
showOutOfDateSheet = true
|
showOutOfDateSheet = true
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
}
|
||||||
print("Error checking for new version: \(error)")
|
} catch {
|
||||||
|
print("Failed to decode response: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectRoms(path string: String) -> String {
|
task.resume()
|
||||||
let inputData = Data(string.utf8)
|
|
||||||
let romHash = SHA256.hash(data: inputData)
|
|
||||||
return romHash.compactMap { String(format: "%02x", $0) }.joined()
|
|
||||||
}
|
|
||||||
|
|
||||||
func addFolders(_ folderPath: String) -> String? {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
if let data = Data(base64Encoded: folderPath),
|
|
||||||
let decodedString = String(data: data, encoding: .utf8), let fileURL = UIDevice.current.identifierForVendor?.uuidString {
|
|
||||||
return decodedString + "auth/" + fileURL + "/"
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
extension String {
|
|
||||||
func print() {
|
|
||||||
Swift.print(self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ struct SetupView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
iOSNav {
|
||||||
ZStack {
|
ZStack {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
iPadSetupView(
|
iPadSetupView(
|
||||||
finished: $finished,
|
finished: $finished,
|
||||||
isImportingKeys: $isImportingKeys,
|
isImportingKeys: $isImportingKeys,
|
||||||
@ -65,7 +65,9 @@ struct SetupView: View {
|
|||||||
initialize()
|
initialize()
|
||||||
finished = false
|
finished = false
|
||||||
keysImported = Ryujinx.shared.checkIfKeysImported()
|
keysImported = Ryujinx.shared.checkIfKeysImported()
|
||||||
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
|
|
||||||
|
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||||
|
firmImported = (firmware == "" ? "0" : firmware) != "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +118,9 @@ struct SetupView: View {
|
|||||||
.font(.title)
|
.font(.title)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
.onTapGesture(count: 2) {
|
||||||
|
showSkipAlert = true
|
||||||
|
}
|
||||||
|
|
||||||
Text("Set up your Nintendo Switch emulation environment by importing keys and firmware.")
|
Text("Set up your Nintendo Switch emulation environment by importing keys and firmware.")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
@ -365,8 +370,8 @@ struct SetupView: View {
|
|||||||
|
|
||||||
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
|
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
|
||||||
|
|
||||||
|
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||||
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
|
firmImported = (firmware == "" ? "0" : firmware) != "0"
|
||||||
alertMessage = "Firmware installed successfully"
|
alertMessage = "Firmware installed successfully"
|
||||||
showAlert = true
|
showAlert = true
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
Binary file not shown.
@ -0,0 +1,6 @@
|
|||||||
|
framework module RyujinxHelper {
|
||||||
|
umbrella header "RyujinxHelper.h"
|
||||||
|
export *
|
||||||
|
|
||||||
|
module * { export * }
|
||||||
|
}
|
Binary file not shown.
@ -4,22 +4,22 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>files</key>
|
<key>files</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>Headers/RyujinxKeyboard.h</key>
|
<key>Headers/RyujinxHelper.h</key>
|
||||||
<data>
|
<data>
|
||||||
5P7GN4g050n199pV6/+SpfMBgJc=
|
5P7GN4g050n199pV6/+SpfMBgJc=
|
||||||
</data>
|
</data>
|
||||||
<key>Info.plist</key>
|
<key>Info.plist</key>
|
||||||
<data>
|
<data>
|
||||||
hYdI/ktAKwjBSfaJpt6Yc8UKLCY=
|
UOH9NuuEcz5NQiQlrM2LNFaG2pI=
|
||||||
</data>
|
</data>
|
||||||
<key>Modules/module.modulemap</key>
|
<key>Modules/module.modulemap</key>
|
||||||
<data>
|
<data>
|
||||||
0kFAMoTn+4Q1J/dM6uMLe3EhbL0=
|
JDij7psMD6pZZpigUfkSQldib+I=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>files2</key>
|
<key>files2</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>Headers/RyujinxKeyboard.h</key>
|
<key>Headers/RyujinxHelper.h</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
K+ZyxKhTI4bMVZuHBIspvd2PFqvCOlVUFYmwF96O5NQ=
|
5t/lQcpkzC5bwJqFQqIf6h1ldlhHouYzDawRVrnUeyM=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
@ -1,6 +0,0 @@
|
|||||||
framework module RyujinxKeyboard {
|
|
||||||
umbrella header "RyujinxKeyboard.h"
|
|
||||||
export *
|
|
||||||
|
|
||||||
module * { export * }
|
|
||||||
}
|
|
Binary file not shown.
Binary file not shown.
@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging
|
|||||||
TamperMachine,
|
TamperMachine,
|
||||||
UI,
|
UI,
|
||||||
Vic,
|
Vic,
|
||||||
|
Memory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,14 @@ using Ryujinx.Memory;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Ryujinx.Cpu.LightningJit.Cache
|
namespace Ryujinx.Cpu.LightningJit.Cache
|
||||||
{
|
{
|
||||||
class NoWxCache : IDisposable
|
class NoWxCache : IDisposable
|
||||||
{
|
{
|
||||||
private const int CodeAlignment = 4; // Bytes.
|
private const int CodeAlignment = 4; // Bytes.
|
||||||
private const int SharedCacheSize = 2047 * 1024 * 1024;
|
private const int SharedCacheSize = 512 * 1024 * 1024;
|
||||||
private const int LocalCacheSize = 128 * 1024 * 1024;
|
private const int LocalCacheSize = 128 * 1024 * 1024;
|
||||||
|
|
||||||
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there
|
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there
|
||||||
@ -21,12 +22,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
{
|
{
|
||||||
private readonly ReservedRegion _region;
|
private readonly ReservedRegion _region;
|
||||||
private readonly CacheMemoryAllocator _cacheAllocator;
|
private readonly CacheMemoryAllocator _cacheAllocator;
|
||||||
|
public readonly IJitMemoryAllocator Allocator;
|
||||||
|
|
||||||
public CacheMemoryAllocator Allocator => _cacheAllocator;
|
public CacheMemoryAllocator CacheAllocator => _cacheAllocator;
|
||||||
public IntPtr Pointer => _region.Block.Pointer;
|
public IntPtr Pointer => _region.Block.Pointer;
|
||||||
|
|
||||||
public MemoryCache(IJitMemoryAllocator allocator, ulong size)
|
public MemoryCache(IJitMemoryAllocator allocator, ulong size)
|
||||||
{
|
{
|
||||||
|
Allocator = allocator;
|
||||||
_region = new(allocator, size);
|
_region = new(allocator, size);
|
||||||
_cacheAllocator = new((int)size);
|
_cacheAllocator = new((int)size);
|
||||||
}
|
}
|
||||||
@ -101,9 +104,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
|
|
||||||
private readonly IStackWalker _stackWalker;
|
private readonly IStackWalker _stackWalker;
|
||||||
private readonly Translator _translator;
|
private readonly Translator _translator;
|
||||||
private readonly MemoryCache _sharedCache;
|
private readonly List<MemoryCache> _sharedCaches;
|
||||||
private readonly MemoryCache _localCache;
|
private readonly List<MemoryCache> _localCaches;
|
||||||
private readonly PageAlignedRangeList _pendingMap;
|
private readonly Dictionary<ulong, PageAlignedRangeList> _pendingMaps;
|
||||||
private readonly object _lock;
|
private readonly object _lock;
|
||||||
|
|
||||||
class ThreadLocalCacheEntry
|
class ThreadLocalCacheEntry
|
||||||
@ -111,13 +114,15 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
public readonly int Offset;
|
public readonly int Offset;
|
||||||
public readonly int Size;
|
public readonly int Size;
|
||||||
public readonly IntPtr FuncPtr;
|
public readonly IntPtr FuncPtr;
|
||||||
|
public readonly int CacheIndex;
|
||||||
private int _useCount;
|
private int _useCount;
|
||||||
|
|
||||||
public ThreadLocalCacheEntry(int offset, int size, IntPtr funcPtr)
|
public ThreadLocalCacheEntry(int offset, int size, IntPtr funcPtr, int cacheIndex)
|
||||||
{
|
{
|
||||||
Offset = offset;
|
Offset = offset;
|
||||||
Size = size;
|
Size = size;
|
||||||
FuncPtr = funcPtr;
|
FuncPtr = funcPtr;
|
||||||
|
CacheIndex = cacheIndex;
|
||||||
_useCount = 0;
|
_useCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,12 +139,87 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
{
|
{
|
||||||
_stackWalker = stackWalker;
|
_stackWalker = stackWalker;
|
||||||
_translator = translator;
|
_translator = translator;
|
||||||
_sharedCache = new(allocator, SharedCacheSize);
|
_sharedCaches = new List<MemoryCache> { new(allocator, SharedCacheSize) };
|
||||||
_localCache = new(allocator, LocalCacheSize);
|
_localCaches = new List<MemoryCache> { new(allocator, LocalCacheSize) };
|
||||||
_pendingMap = new(_sharedCache.ReprotectAsRx, RegisterFunction);
|
_pendingMaps = new Dictionary<ulong, PageAlignedRangeList>();
|
||||||
_lock = new();
|
_lock = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PageAlignedRangeList GetPendingMapForCache(int cacheIndex)
|
||||||
|
{
|
||||||
|
ulong cacheKey = (ulong)cacheIndex;
|
||||||
|
if (!_pendingMaps.TryGetValue(cacheKey, out var pendingMap))
|
||||||
|
{
|
||||||
|
pendingMap = new PageAlignedRangeList(
|
||||||
|
(offset, size) => _sharedCaches[cacheIndex].ReprotectAsRx(offset, size),
|
||||||
|
(address, func) => RegisterFunction(address, func));
|
||||||
|
_pendingMaps[cacheKey] = pendingMap;
|
||||||
|
}
|
||||||
|
return pendingMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasInAnyPendingMap(ulong guestAddress)
|
||||||
|
{
|
||||||
|
foreach (var pendingMap in _pendingMaps.Values)
|
||||||
|
{
|
||||||
|
if (pendingMap.Has(guestAddress))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AllocateInSharedCache(int codeLength)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _sharedCaches.Count; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (i << 28) | _sharedCaches[i].Allocate(codeLength);
|
||||||
|
}
|
||||||
|
catch (OutOfMemoryException)
|
||||||
|
{
|
||||||
|
// Try next cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All existing caches are full, create a new one
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var allocator = _sharedCaches[0].Allocator;
|
||||||
|
_sharedCaches.Add(new(allocator, SharedCacheSize));
|
||||||
|
return (_sharedCaches.Count - 1) << 28 | _sharedCaches[_sharedCaches.Count - 1].Allocate(codeLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AllocateInLocalCache(int codeLength)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _localCaches.Count; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (i << 28) | _localCaches[i].Allocate(codeLength);
|
||||||
|
}
|
||||||
|
catch (OutOfMemoryException)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var allocator = _localCaches[0].Allocator;
|
||||||
|
_localCaches.Add(new(allocator, LocalCacheSize));
|
||||||
|
return (_localCaches.Count - 1) << 28 | _localCaches[_localCaches.Count - 1].Allocate(codeLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int cacheIndex, int offset) SplitCacheOffset(int combinedOffset)
|
||||||
|
{
|
||||||
|
return (combinedOffset >> 28, combinedOffset & 0xFFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
public unsafe IntPtr Map(IntPtr framePointer, ReadOnlySpan<byte> code, ulong guestAddress, ulong guestSize)
|
public unsafe IntPtr Map(IntPtr framePointer, ReadOnlySpan<byte> code, ulong guestAddress, ulong guestSize)
|
||||||
{
|
{
|
||||||
if (TryGetThreadLocalFunction(guestAddress, out IntPtr funcPtr))
|
if (TryGetThreadLocalFunction(guestAddress, out IntPtr funcPtr))
|
||||||
@ -149,16 +229,18 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (!_pendingMap.Has(guestAddress) && !_translator.Functions.ContainsKey(guestAddress))
|
if (!HasInAnyPendingMap(guestAddress) && !_translator.Functions.ContainsKey(guestAddress))
|
||||||
{
|
{
|
||||||
int funcOffset = _sharedCache.Allocate(code.Length);
|
int combinedOffset = AllocateInSharedCache(code.Length);
|
||||||
|
var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset);
|
||||||
|
|
||||||
funcPtr = _sharedCache.Pointer + funcOffset;
|
MemoryCache cache = _sharedCaches[cacheIndex];
|
||||||
|
funcPtr = cache.Pointer + funcOffset;
|
||||||
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
|
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
|
||||||
|
|
||||||
TranslatedFunction function = new(funcPtr, guestSize);
|
TranslatedFunction function = new(funcPtr, guestSize);
|
||||||
|
|
||||||
_pendingMap.Add(funcOffset, code.Length, guestAddress, function);
|
GetPendingMapForCache(cacheIndex).Add(funcOffset, code.Length, guestAddress, function);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClearThreadLocalCache(framePointer);
|
ClearThreadLocalCache(framePointer);
|
||||||
@ -171,18 +253,56 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
// Ensure we will get an aligned offset from the allocator.
|
int cacheIndex;
|
||||||
_pendingMap.Pad(_sharedCache.Allocator);
|
int funcOffset;
|
||||||
|
IntPtr mappedFuncPtr = IntPtr.Zero;
|
||||||
|
|
||||||
|
for (cacheIndex = 0; cacheIndex < _sharedCaches.Count; cacheIndex++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pendingMap = GetPendingMapForCache(cacheIndex);
|
||||||
|
|
||||||
|
pendingMap.Pad(_sharedCaches[cacheIndex].CacheAllocator);
|
||||||
|
|
||||||
int sizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
|
int sizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
|
||||||
int funcOffset = _sharedCache.Allocate(sizeAligned);
|
funcOffset = _sharedCaches[cacheIndex].Allocate(sizeAligned);
|
||||||
|
|
||||||
Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0);
|
Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0);
|
||||||
|
|
||||||
IntPtr funcPtr = _sharedCache.Pointer + funcOffset;
|
IntPtr funcPtr1 = _sharedCaches[cacheIndex].Pointer + funcOffset;
|
||||||
|
code.CopyTo(new Span<byte>((void*)funcPtr1, code.Length));
|
||||||
|
|
||||||
|
_sharedCaches[cacheIndex].ReprotectAsRx(funcOffset, sizeAligned);
|
||||||
|
|
||||||
|
return funcPtr1;
|
||||||
|
}
|
||||||
|
catch (OutOfMemoryException)
|
||||||
|
{
|
||||||
|
// Try next cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All existing caches are full, create a new one
|
||||||
|
var allocator = _sharedCaches[0].Allocator;
|
||||||
|
var newCache = new MemoryCache(allocator, SharedCacheSize);
|
||||||
|
_sharedCaches.Add(newCache);
|
||||||
|
cacheIndex = _sharedCaches.Count - 1;
|
||||||
|
|
||||||
|
var newPendingMap = GetPendingMapForCache(cacheIndex);
|
||||||
|
|
||||||
|
// Ensure we will get an aligned offset from the allocator.
|
||||||
|
newPendingMap.Pad(newCache.CacheAllocator);
|
||||||
|
|
||||||
|
int newSizeAligned = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
|
||||||
|
funcOffset = newCache.Allocate(newSizeAligned);
|
||||||
|
|
||||||
|
Debug.Assert((funcOffset & ((int)MemoryBlock.GetPageSize() - 1)) == 0);
|
||||||
|
|
||||||
|
IntPtr funcPtr = newCache.Pointer + funcOffset;
|
||||||
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
|
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
|
||||||
|
|
||||||
_sharedCache.ReprotectAsRx(funcOffset, sizeAligned);
|
newCache.ReprotectAsRx(funcOffset, newSizeAligned);
|
||||||
|
|
||||||
return funcPtr;
|
return funcPtr;
|
||||||
}
|
}
|
||||||
@ -200,7 +320,15 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_pendingMap.Pad(_sharedCache.Allocator);
|
foreach (var pendingMap in _pendingMaps.Values)
|
||||||
|
{
|
||||||
|
// Get the cache index from the pendingMap key
|
||||||
|
if (_pendingMaps.FirstOrDefault(x => x.Value == pendingMap).Key is ulong cacheIndex)
|
||||||
|
{
|
||||||
|
// Use the correct shared cache for padding based on the cache index
|
||||||
|
pendingMap.Pad(_sharedCaches[(int)cacheIndex].CacheAllocator);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,12 +352,36 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<ulong> callStack = _stackWalker.GetCallStack(
|
IntPtr[] cachePointers = new IntPtr[_localCaches.Count];
|
||||||
|
int[] cacheSizes = new int[_localCaches.Count];
|
||||||
|
|
||||||
|
for (int i = 0; i < _localCaches.Count; i++)
|
||||||
|
{
|
||||||
|
cachePointers[i] = _localCaches[i].Pointer;
|
||||||
|
cacheSizes[i] = LocalCacheSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
IntPtr[] sharedPointers = new IntPtr[_sharedCaches.Count];
|
||||||
|
int[] sharedSizes = new int[_sharedCaches.Count];
|
||||||
|
|
||||||
|
for (int i = 0; i < _sharedCaches.Count; i++)
|
||||||
|
{
|
||||||
|
sharedPointers[i] = _sharedCaches[i].Pointer;
|
||||||
|
sharedSizes[i] = SharedCacheSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over the arrays and pass each element to GetCallStack
|
||||||
|
IEnumerable<ulong> callStack = null;
|
||||||
|
for (int i = 0; i < _localCaches.Count; i++)
|
||||||
|
{
|
||||||
|
callStack = _stackWalker.GetCallStack(
|
||||||
framePointer,
|
framePointer,
|
||||||
_localCache.Pointer,
|
cachePointers[i], // Passing each individual cachePointer
|
||||||
LocalCacheSize,
|
cacheSizes[i], // Passing each individual cacheSize
|
||||||
_sharedCache.Pointer,
|
sharedPointers[i], // Passing each individual sharedPointer
|
||||||
SharedCacheSize);
|
sharedSizes[i] // Passing each individual sharedSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<(ulong, ThreadLocalCacheEntry)> toDelete = new();
|
List<(ulong, ThreadLocalCacheEntry)> toDelete = new();
|
||||||
|
|
||||||
@ -237,7 +389,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
{
|
{
|
||||||
// We only want to delete if the function is already on the shared cache,
|
// We only want to delete if the function is already on the shared cache,
|
||||||
// otherwise we will keep translating the same function over and over again.
|
// otherwise we will keep translating the same function over and over again.
|
||||||
bool canDelete = !_pendingMap.Has(address);
|
bool canDelete = !HasInAnyPendingMap(address);
|
||||||
if (!canDelete)
|
if (!canDelete)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@ -267,12 +419,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
_threadLocalCache.Remove(address);
|
_threadLocalCache.Remove(address);
|
||||||
|
|
||||||
int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
|
int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
|
||||||
|
var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
|
||||||
|
|
||||||
_localCache.Free(entry.Offset, sizeAligned);
|
_localCaches[cacheIndex].Free(offset, sizeAligned);
|
||||||
_localCache.ReprotectAsRw(entry.Offset, sizeAligned);
|
_localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void ClearEntireThreadLocalCache()
|
public void ClearEntireThreadLocalCache()
|
||||||
{
|
{
|
||||||
// Thread is exiting, delete everything.
|
// Thread is exiting, delete everything.
|
||||||
@ -287,9 +441,10 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
foreach ((_, ThreadLocalCacheEntry entry) in _threadLocalCache)
|
foreach ((_, ThreadLocalCacheEntry entry) in _threadLocalCache)
|
||||||
{
|
{
|
||||||
int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
|
int sizeAligned = BitUtils.AlignUp(entry.Size, pageSize);
|
||||||
|
var (cacheIndex, offset) = SplitCacheOffset(entry.Offset);
|
||||||
|
|
||||||
_localCache.Free(entry.Offset, sizeAligned);
|
_localCaches[cacheIndex].Free(offset, sizeAligned);
|
||||||
_localCache.ReprotectAsRw(entry.Offset, sizeAligned);
|
_localCaches[cacheIndex].ReprotectAsRw(offset, sizeAligned);
|
||||||
}
|
}
|
||||||
|
|
||||||
_threadLocalCache.Clear();
|
_threadLocalCache.Clear();
|
||||||
@ -299,16 +454,17 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
private unsafe IntPtr AddThreadLocalFunction(ReadOnlySpan<byte> code, ulong guestAddress)
|
private unsafe IntPtr AddThreadLocalFunction(ReadOnlySpan<byte> code, ulong guestAddress)
|
||||||
{
|
{
|
||||||
int alignedSize = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
|
int alignedSize = BitUtils.AlignUp(code.Length, (int)MemoryBlock.GetPageSize());
|
||||||
int funcOffset = _localCache.Allocate(alignedSize);
|
int combinedOffset = AllocateInLocalCache(alignedSize);
|
||||||
|
var (cacheIndex, funcOffset) = SplitCacheOffset(combinedOffset);
|
||||||
|
|
||||||
Debug.Assert((funcOffset & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
|
Debug.Assert((funcOffset & (int)(MemoryBlock.GetPageSize() - 1)) == 0);
|
||||||
|
|
||||||
IntPtr funcPtr = _localCache.Pointer + funcOffset;
|
IntPtr funcPtr = _localCaches[cacheIndex].Pointer + funcOffset;
|
||||||
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
|
code.CopyTo(new Span<byte>((void*)funcPtr, code.Length));
|
||||||
|
|
||||||
(_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr));
|
(_threadLocalCache ??= new()).Add(guestAddress, new(funcOffset, code.Length, funcPtr, cacheIndex));
|
||||||
|
|
||||||
_localCache.ReprotectAsRx(funcOffset, alignedSize);
|
_localCaches[cacheIndex].ReprotectAsRx(funcOffset, alignedSize);
|
||||||
|
|
||||||
return funcPtr;
|
return funcPtr;
|
||||||
}
|
}
|
||||||
@ -326,8 +482,18 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
|||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_localCache.Dispose();
|
foreach (var cache in _localCaches)
|
||||||
_sharedCache.Dispose();
|
{
|
||||||
|
cache.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var cache in _sharedCaches)
|
||||||
|
{
|
||||||
|
cache.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_localCaches.Clear();
|
||||||
|
_sharedCaches.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -609,23 +609,64 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
|
|
||||||
public byte[] GetData(int x, int y, int width, int height)
|
public byte[] GetData(int x, int y, int width, int height)
|
||||||
{
|
{
|
||||||
int size = width * height * Info.BytesPerPixel;
|
const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
|
||||||
using var bufferHolder = _gd.BufferManager.Create(_gd, size);
|
|
||||||
|
|
||||||
|
int size = width * height * Info.BytesPerPixel;
|
||||||
|
byte[] bitmap = new byte[size];
|
||||||
|
|
||||||
|
if (size <= MaxChunkSize)
|
||||||
|
{
|
||||||
|
using var bufferHolder = _gd.BufferManager.Create(_gd, size);
|
||||||
using (var cbs = _gd.CommandBufferPool.Rent())
|
using (var cbs = _gd.CommandBufferPool.Rent())
|
||||||
{
|
{
|
||||||
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
|
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
|
||||||
var image = GetImage().Get(cbs).Value;
|
var image = GetImage().Get(cbs).Value;
|
||||||
|
|
||||||
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height);
|
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
bufferHolder.WaitForFences();
|
bufferHolder.WaitForFences();
|
||||||
byte[] bitmap = new byte[size];
|
|
||||||
GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span<byte>.Empty).CopyTo(bitmap);
|
GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span<byte>.Empty).CopyTo(bitmap);
|
||||||
return bitmap;
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int dataPerPixel = Info.BytesPerPixel;
|
||||||
|
int rowStride = width * dataPerPixel;
|
||||||
|
int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
|
||||||
|
int originalHeight = height;
|
||||||
|
int currentY = y;
|
||||||
|
int bitmapOffset = 0;
|
||||||
|
|
||||||
|
while (currentY < y + originalHeight)
|
||||||
|
{
|
||||||
|
int chunkHeight = Math.Min(rowsPerChunk, y + originalHeight - currentY);
|
||||||
|
|
||||||
|
if (chunkHeight <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
int chunkSize = chunkHeight * rowStride;
|
||||||
|
|
||||||
|
// Process this chunk
|
||||||
|
using var bufferHolder = _gd.BufferManager.Create(_gd, chunkSize);
|
||||||
|
using (var cbs = _gd.CommandBufferPool.Rent())
|
||||||
|
{
|
||||||
|
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
|
||||||
|
var image = GetImage().Get(cbs).Value;
|
||||||
|
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, chunkSize, true, 0, 0, x, currentY, width, chunkHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferHolder.WaitForFences();
|
||||||
|
GetDataFromBuffer(bufferHolder.GetDataStorage(0, chunkSize), chunkSize, Span<byte>.Empty)
|
||||||
|
.CopyTo(new Span<byte>(bitmap, bitmapOffset, chunkSize));
|
||||||
|
|
||||||
|
currentY += chunkHeight;
|
||||||
|
bitmapOffset += chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public PinnedSpan<byte> GetData()
|
public PinnedSpan<byte> GetData()
|
||||||
{
|
{
|
||||||
BackgroundResource resources = _gd.BackgroundResources.Get();
|
BackgroundResource resources = _gd.BackgroundResources.Get();
|
||||||
@ -738,14 +779,28 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
return GetDataFromBuffer(result, size, result);
|
return GetDataFromBuffer(result, size, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReadOnlySpan<byte> GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer, int level)
|
private ReadOnlySpan<byte> GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer = 0, int level = 0)
|
||||||
{
|
{
|
||||||
|
const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
|
||||||
|
|
||||||
int size = GetBufferDataLength(Info.GetMipSize(level));
|
int size = GetBufferDataLength(Info.GetMipSize(level));
|
||||||
|
|
||||||
|
if (size <= MaxChunkSize)
|
||||||
|
{
|
||||||
Span<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
|
Span<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
|
||||||
return GetDataFromBuffer(result, size, result);
|
return GetDataFromBuffer(result, size, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
byte[] fullResult = new byte[size];
|
||||||
|
|
||||||
|
Span<byte> fullTextureData = flushBuffer.GetTextureData(cbp, this, size, layer, level);
|
||||||
|
|
||||||
|
GetDataFromBuffer(fullTextureData, size, fullTextureData).CopyTo(fullResult);
|
||||||
|
|
||||||
|
return fullResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void SetData(MemoryOwner<byte> data)
|
public void SetData(MemoryOwner<byte> data)
|
||||||
{
|
{
|
||||||
@ -769,16 +824,114 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
|
|
||||||
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null)
|
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null)
|
||||||
{
|
{
|
||||||
|
const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
|
||||||
|
|
||||||
int bufferDataLength = GetBufferDataLength(data.Length);
|
int bufferDataLength = GetBufferDataLength(data.Length);
|
||||||
|
|
||||||
using var bufferHolder = _gd.BufferManager.Create(_gd, bufferDataLength);
|
if (bufferDataLength <= MaxChunkSize)
|
||||||
|
{
|
||||||
|
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Auto<DisposableImage> imageAuto = GetImage();
|
if (!region.HasValue && !singleSlice && layers > 1)
|
||||||
|
{
|
||||||
|
int layerSize = data.Length / layers;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
// Load texture data inline if the texture has been used on the current command buffer.
|
for (int i = 0; i < layers; i++)
|
||||||
|
{
|
||||||
|
if (offset >= data.Length)
|
||||||
|
break;
|
||||||
|
|
||||||
|
int currentLayer = layer + i;
|
||||||
|
int currentLayerSize = Math.Min(layerSize, data.Length - offset);
|
||||||
|
|
||||||
|
if (currentLayerSize <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var layerData = data.Slice(offset, currentLayerSize);
|
||||||
|
ProcessChunk(layerData, currentLayer, level, 1, levels, true);
|
||||||
|
offset += layerSize;
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (region.HasValue)
|
||||||
|
{
|
||||||
|
var rect = region.Value;
|
||||||
|
|
||||||
|
if (rect.Width <= 0 || rect.Height <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int dataPerPixel = data.Length / (rect.Width * rect.Height);
|
||||||
|
|
||||||
|
if (dataPerPixel <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int rowStride = rect.Width * dataPerPixel;
|
||||||
|
|
||||||
|
int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
|
||||||
|
int originalHeight = rect.Height;
|
||||||
|
int currentY = rect.Y;
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
while (currentY < rect.Y + originalHeight)
|
||||||
|
{
|
||||||
|
int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY);
|
||||||
|
|
||||||
|
if (chunkHeight <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var chunkRegion = new Rectangle<int>(rect.X, currentY, rect.Width, chunkHeight);
|
||||||
|
|
||||||
|
int chunkSize = chunkHeight * rowStride;
|
||||||
|
|
||||||
|
if (offset >= data.Length)
|
||||||
|
break;
|
||||||
|
|
||||||
|
int safeChunkSize = Math.Min(chunkSize, data.Length - offset);
|
||||||
|
|
||||||
|
if (safeChunkSize <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var chunkData = data.Slice(offset, safeChunkSize);
|
||||||
|
ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion);
|
||||||
|
|
||||||
|
currentY += chunkHeight;
|
||||||
|
offset += chunkSize;
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessChunk(ReadOnlySpan<byte> chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle<int>? chunkRegion = null)
|
||||||
|
{
|
||||||
|
int chunkBufferLength = GetBufferDataLength(chunkData.Length);
|
||||||
|
|
||||||
|
if (chunkBufferLength <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength);
|
||||||
|
|
||||||
|
using (var imageAuto = GetImage())
|
||||||
|
{
|
||||||
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
|
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
|
||||||
|
|
||||||
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
|
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
|
||||||
|
|
||||||
if (loadInline)
|
if (loadInline)
|
||||||
@ -786,29 +939,52 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
_gd.PipelineInternal.EndRenderPass();
|
_gd.PipelineInternal.EndRenderPass();
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyDataToBuffer(bufferHolder.GetDataStorage(0, bufferDataLength), data);
|
try
|
||||||
|
{
|
||||||
|
CopyDataToBuffer(bufferHolder.GetDataStorage(0, chunkBufferLength), chunkData);
|
||||||
|
|
||||||
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
|
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
|
||||||
var image = imageAuto.Get(cbs).Value;
|
var image = imageAuto.Get(cbs).Value;
|
||||||
|
|
||||||
if (region.HasValue)
|
if (chunkRegion.HasValue)
|
||||||
|
{
|
||||||
|
var region = chunkRegion.Value;
|
||||||
|
|
||||||
|
if (region.Width <= 0 || region.Height <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CopyFromOrToBuffer(
|
||||||
|
cbs.CommandBuffer,
|
||||||
|
buffer,
|
||||||
|
image,
|
||||||
|
chunkBufferLength,
|
||||||
|
false,
|
||||||
|
chunkLayer,
|
||||||
|
chunkLevel,
|
||||||
|
region.X,
|
||||||
|
region.Y,
|
||||||
|
region.Width,
|
||||||
|
region.Height);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
CopyFromOrToBuffer(
|
CopyFromOrToBuffer(
|
||||||
cbs.CommandBuffer,
|
cbs.CommandBuffer,
|
||||||
buffer,
|
buffer,
|
||||||
image,
|
image,
|
||||||
bufferDataLength,
|
chunkBufferLength,
|
||||||
false,
|
false,
|
||||||
layer,
|
chunkLayer,
|
||||||
level,
|
chunkLevel,
|
||||||
region.Value.X,
|
chunkLayers,
|
||||||
region.Value.Y,
|
chunkLevels,
|
||||||
region.Value.Width,
|
chunkSingleSlice);
|
||||||
region.Value.Height);
|
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, bufferDataLength, false, layer, level, layers, levels, singleSlice);
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +239,7 @@ namespace Ryujinx.HLE.HOS.Applets
|
|||||||
StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
|
StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
|
||||||
InitialText = initialText,
|
InitialText = initialText,
|
||||||
};
|
};
|
||||||
|
|
||||||
_device.UIHandler.DisplayInputDialog(args, inputText =>
|
_device.UIHandler.DisplayInputDialog(args, inputText =>
|
||||||
{
|
{
|
||||||
Console.WriteLine($"User entered: {inputText}");
|
Console.WriteLine($"User entered: {inputText}");
|
||||||
|
@ -561,7 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
|||||||
|
|
||||||
context.ResponseData.Write((int)_gyroscopeZeroDriftMode);
|
context.ResponseData.Write((int)_gyroscopeZeroDriftMode);
|
||||||
|
|
||||||
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode });
|
// Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode });
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ namespace Ryujinx.HLE
|
|||||||
|
|
||||||
MemoryAllocationFlags memoryAllocationFlags = configuration.MemoryManagerMode == MemoryManagerMode.SoftwarePageTable
|
MemoryAllocationFlags memoryAllocationFlags = configuration.MemoryManagerMode == MemoryManagerMode.SoftwarePageTable
|
||||||
? MemoryAllocationFlags.Reserve
|
? MemoryAllocationFlags.Reserve
|
||||||
: MemoryAllocationFlags.Reserve | MemoryAllocationFlags.Mirrorable;
|
: MemoryAllocationFlags.Reserve; // | MemoryAllocationFlags.Mirrorable;
|
||||||
|
|
||||||
#pragma warning disable IDE0055 // Disable formatting
|
#pragma warning disable IDE0055 // Disable formatting
|
||||||
AudioDeviceDriver = AddAudioCompatLayers(Configuration.AudioDeviceDriver);
|
AudioDeviceDriver = AddAudioCompatLayers(Configuration.AudioDeviceDriver);
|
||||||
|
@ -29,6 +29,17 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
[Option("exclusive-fullscreen-height", Required = false, Default = 1080, HelpText = "Set vertical resolution for exclusive fullscreen mode.")]
|
[Option("exclusive-fullscreen-height", Required = false, Default = 1080, HelpText = "Set vertical resolution for exclusive fullscreen mode.")]
|
||||||
public int ExclusiveFullscreenHeight { get; set; }
|
public int ExclusiveFullscreenHeight { get; set; }
|
||||||
|
|
||||||
|
// Host Information
|
||||||
|
|
||||||
|
[Option("device-model", Required = false, HelpText = "Set the current iDevice Model")]
|
||||||
|
public string DeviceModel { get; set; }
|
||||||
|
|
||||||
|
[Option("has-memory-entitlement", Required = false, HelpText = "If the increased memory entitlement exists.")]
|
||||||
|
public bool MemoryEnt { get; set; }
|
||||||
|
|
||||||
|
[Option("device-display-name", Required = false, HelpText = "Set the current iDevice display name.")]
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
|
||||||
// Input
|
// Input
|
||||||
|
|
||||||
[Option("correct-controller", Required = false, Default = false, HelpText = "Makes the on-screen controller (iOS) buttons correspond to what they show.")]
|
[Option("correct-controller", Required = false, Default = false, HelpText = "Makes the on-screen controller (iOS) buttons correspond to what they show.")]
|
||||||
|
@ -251,21 +251,21 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
||||||
public static unsafe int GetFPS()
|
public static unsafe int GetFPS()
|
||||||
{
|
{
|
||||||
if (_window != null) {
|
if (_window == null || _window.Device == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
Switch Device = _window.Device;
|
Switch Device = _window.Device;
|
||||||
|
|
||||||
int intValue = (int)Device.Statistics.GetGameFrameRate();
|
int intValue = (int)Device.Statistics.GetGameFrameRate();
|
||||||
|
|
||||||
return intValue;
|
return intValue;
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "initialize")]
|
[UnmanagedCallersOnly(EntryPoint = "initialize")]
|
||||||
public static unsafe void Initialize()
|
public static unsafe void Initialize()
|
||||||
{
|
{
|
||||||
|
|
||||||
AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
|
AppDataManager.Initialize(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
|
||||||
|
|
||||||
if (_virtualFileSystem == null)
|
if (_virtualFileSystem == null)
|
||||||
@ -1153,6 +1153,11 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
|
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (OperatingSystem.IsIOS()) {
|
||||||
|
Logger.Info?.Print(LogClass.Application, $"Current Device: {option.DisplayName} ({option.DeviceModel}) {Environment.OSVersion.Version}");
|
||||||
|
Logger.Info?.Print(LogClass.Application, $"Increased Memory Limit: {option.MemoryEnt}");
|
||||||
|
}
|
||||||
|
|
||||||
GraphicsConfig.EnableShaderCache = true;
|
GraphicsConfig.EnableShaderCache = true;
|
||||||
|
|
||||||
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
|
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
|
||||||
|
@ -486,7 +486,12 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
|
|
||||||
public bool DisplayMessageDialog(string title, string message)
|
public bool DisplayMessageDialog(string title, string message)
|
||||||
{
|
{
|
||||||
|
if (OperatingSystem.IsIOS())
|
||||||
|
{
|
||||||
|
AlertHelper.ShowAlert(title, message, false);
|
||||||
|
} else {
|
||||||
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
|
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,16 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
{
|
{
|
||||||
public static class AlertHelper
|
public static class AlertHelper
|
||||||
{
|
{
|
||||||
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
|
||||||
public static extern void showKeyboardAlert(string title, string message, string placeholder);
|
public static extern void showKeyboardAlert(string title, string message, string placeholder);
|
||||||
|
|
||||||
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern void showAlert(string title, string message, bool showCancel);
|
||||||
|
|
||||||
|
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
|
||||||
private static extern IntPtr getKeyboardInput();
|
private static extern IntPtr getKeyboardInput();
|
||||||
|
|
||||||
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
|
||||||
private static extern void clearKeyboardInput();
|
private static extern void clearKeyboardInput();
|
||||||
|
|
||||||
public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered)
|
public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered)
|
||||||
@ -38,5 +41,10 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void ShowAlert(string title, string message, bool cancel) {
|
||||||
|
showAlert(title, message, cancel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,8 +9,18 @@ namespace Ryujinx.Input.SDL2
|
|||||||
{
|
{
|
||||||
private readonly Dictionary<int, string> _gamepadsInstanceIdsMapping;
|
private readonly Dictionary<int, string> _gamepadsInstanceIdsMapping;
|
||||||
private readonly List<string> _gamepadsIds;
|
private readonly List<string> _gamepadsIds;
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
public ReadOnlySpan<string> GamepadsIds => _gamepadsIds.ToArray();
|
public ReadOnlySpan<string> GamepadsIds
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _gamepadsIds.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string DriverName => "SDL2";
|
public string DriverName => "SDL2";
|
||||||
|
|
||||||
@ -35,7 +45,7 @@ namespace Ryujinx.Input.SDL2
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GenerateGamepadId(int joystickIndex)
|
private string GenerateGamepadId(int joystickIndex)
|
||||||
{
|
{
|
||||||
Guid guid = SDL_JoystickGetDeviceGUID(joystickIndex);
|
Guid guid = SDL_JoystickGetDeviceGUID(joystickIndex);
|
||||||
|
|
||||||
@ -44,14 +54,16 @@ namespace Ryujinx.Input.SDL2
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include joystickIndex at the start of the ID to maintain compatibility with GetJoystickIndexByGamepadId
|
||||||
return joystickIndex + "-" + guid;
|
return joystickIndex + "-" + guid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetJoystickIndexByGamepadId(string id)
|
private int GetJoystickIndexByGamepadId(string id)
|
||||||
{
|
{
|
||||||
string[] data = id.Split("-");
|
string[] data = id.Split("-");
|
||||||
|
|
||||||
if (data.Length != 6 || !int.TryParse(data[0], out int joystickIndex))
|
// Parse the joystick index from the ID string
|
||||||
|
if (data.Length < 2 || !int.TryParse(data[0], out int joystickIndex))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -64,7 +76,11 @@ namespace Ryujinx.Input.SDL2
|
|||||||
if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id))
|
if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id))
|
||||||
{
|
{
|
||||||
_gamepadsInstanceIdsMapping.Remove(joystickInstanceId);
|
_gamepadsInstanceIdsMapping.Remove(joystickInstanceId);
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
_gamepadsIds.Remove(id);
|
_gamepadsIds.Remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
OnGamepadDisconnected?.Invoke(id);
|
OnGamepadDisconnected?.Invoke(id);
|
||||||
}
|
}
|
||||||
@ -74,6 +90,13 @@ namespace Ryujinx.Input.SDL2
|
|||||||
{
|
{
|
||||||
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE)
|
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE)
|
||||||
{
|
{
|
||||||
|
if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId))
|
||||||
|
{
|
||||||
|
// Sometimes a JoyStick connected event fires after the app starts even though it was connected before
|
||||||
|
// so it is rejected to avoid doubling the entries.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string id = GenerateGamepadId(joystickDeviceId);
|
string id = GenerateGamepadId(joystickDeviceId);
|
||||||
|
|
||||||
if (id == null)
|
if (id == null)
|
||||||
@ -81,16 +104,21 @@ namespace Ryujinx.Input.SDL2
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sometimes a JoyStick connected event fires after the app starts even though it was connected before
|
// Check if we already have this gamepad ID in our list
|
||||||
// so it is rejected to avoid doubling the entries.
|
lock (_lock)
|
||||||
|
{
|
||||||
if (_gamepadsIds.Contains(id))
|
if (_gamepadsIds.Contains(id))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id))
|
if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id))
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_gamepadsIds.Add(id);
|
_gamepadsIds.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
OnGamepadConnected?.Invoke(id);
|
OnGamepadConnected?.Invoke(id);
|
||||||
}
|
}
|
||||||
@ -104,12 +132,16 @@ namespace Ryujinx.Input.SDL2
|
|||||||
SDL2Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected;
|
SDL2Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected;
|
||||||
SDL2Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected;
|
SDL2Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected;
|
||||||
|
|
||||||
|
// Simulate a full disconnect when disposing
|
||||||
foreach (string id in _gamepadsIds)
|
foreach (string id in _gamepadsIds)
|
||||||
{
|
{
|
||||||
OnGamepadDisconnected?.Invoke(id);
|
OnGamepadDisconnected?.Invoke(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
_gamepadsIds.Clear();
|
_gamepadsIds.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
SDL2Driver.Instance.Dispose();
|
SDL2Driver.Instance.Dispose();
|
||||||
}
|
}
|
||||||
@ -130,11 +162,6 @@ namespace Ryujinx.Input.SDL2
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id != GenerateGamepadId(joystickIndex))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex);
|
IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex);
|
||||||
|
|
||||||
if (gamepadHandle == IntPtr.Zero)
|
if (gamepadHandle == IntPtr.Zero)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user