forked from MeloNX/MeloNX
Compare commits
3 Commits
XC-ios-ht
...
hypervisor
Author | SHA1 | Date | |
---|---|---|---|
|
687f89ee58 | ||
|
c7e0d498bb | ||
|
5e9126a4bf |
@ -1,21 +0,0 @@
|
||||
name: Notify API on Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
notify-api:
|
||||
runs-on: debian-trixie
|
||||
steps:
|
||||
- name: Send API Call for New Release
|
||||
run: |
|
||||
curl -X POST http://melonx.org/api/new_release \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${{ secrets.MELONX_GITEA_API_KEY }}" \
|
||||
-d '{
|
||||
"version_number": "${{ github.event.release.tag_name }}",
|
||||
"download_link": "${{ github.event.release.html_url }}",
|
||||
"changelog": "${{ github.event.release.body }}",
|
||||
"is_latest": true
|
||||
}'
|
64
.gitignore
vendored
64
.gitignore
vendored
@ -176,67 +176,3 @@ PublishProfiles/
|
||||
# Glade backup files
|
||||
*.glade~
|
||||
src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib
|
||||
|
||||
# SWIFT GITIGNORE
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
94
Compile.md
94
Compile.md
@ -1,81 +1,33 @@
|
||||
# Compiling MeloNX on macOS
|
||||
# How to compile MeloNX using macOS
|
||||
|
||||
## Prerequisites
|
||||
- [.NET 8.0](<https://dotnet.microsoft.com/en-us/download/dotnet/8.0>)
|
||||
- A computer with macOS
|
||||
|
||||
Before you begin, ensure you have the following installed:
|
||||
## Compiling
|
||||
1. Clone the Git Repo and build Ryujinx
|
||||
```
|
||||
git clone https://github.com/melonx-emu/melonx/tree/XC-ios-ht
|
||||
cd melonx
|
||||
./compile.sh -x
|
||||
```
|
||||
|
||||
- [**.NET 8.0**](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
||||
- [**Xcode**](https://apps.apple.com/de/app/xcode/id497799835?l=en-GB&mt=12$0)
|
||||
- A Mac running **macOS**
|
||||
2. Open the Xcode project, stored at MeloNX/src/MeloNX
|
||||
|
||||
## Compilation Steps
|
||||
3. Make sure `Ryujinx.SDL2.Headless.dylib` is set to `Embed & Sign` in the General settings for the Xcode project
|
||||
|
||||
4. Signing & Capabilities
|
||||
Change your 'Team' to your Developer Account (free or paid) and change Bundle Identifier to
|
||||
`com.*your name*.MeloNX`
|
||||
|
||||
### 1. Clone the Repository and Build Ryujinx
|
||||
6. Build and Run
|
||||
`CMD+R`
|
||||
|
||||
Open a terminal and run:
|
||||
7. Check the [post-setup guide](<https://github.com/melonx-emu/melonx/tree/XC-ios-ht/postsetup.md>)
|
||||
|
||||
```sh
|
||||
git clone https://git.743378673.xyz/MeloNX/MeloNX.git
|
||||
cd MeloNX
|
||||
./compile.sh
|
||||
## If you don't have a paid developer account
|
||||
Make sure these entitlements are removed if you don't have a paid Apple Developer account
|
||||
```
|
||||
You may need to run this command if compilation fails, then run the `./compile.sh` command again (You will need to put in your user password. Your password will not be shown at all.)
|
||||
Extended Virtual Addressing
|
||||
Increased Debugging Memory Limit
|
||||
```
|
||||
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
|
||||
```
|
||||
|
||||
|
||||
However, if you only need to update MeloNX, make sure you have cd into the directory then run this then skip to step 5
|
||||
```
|
||||
git pull
|
||||
./compile.sh
|
||||
```
|
||||
|
||||
### 2. Open the Xcode Project
|
||||
|
||||
Navigate to the **Xcode project file** located at:
|
||||
|
||||
```
|
||||
src/MeloNX/MeloNX.xcodeproj
|
||||
```
|
||||
|
||||
Double-click to open it in **Xcode**.
|
||||
|
||||
### 3. Configure the Project Settings
|
||||
|
||||
- In **Xcode**, select the **MeloNX** project.
|
||||
- Under the **General** tab, find `Ryujinx.Headless.SDL2.dylib`.
|
||||
- Set its **Embed setting** to **"Embed & Sign"**.
|
||||
|
||||
### 4. Configure Signing & Capabilities
|
||||
|
||||
- In **Xcode**, go to **Signing & Capabilities**.
|
||||
- Set the **Team** to your **Apple Developer account** (free or paid).
|
||||
- Change the **Bundle Identifier** to:
|
||||
|
||||
```
|
||||
com.<your-name>.MeloNX
|
||||
```
|
||||
|
||||
*(Replace `<your-name>` with your actual name or identifier.)*
|
||||
|
||||
### 5. Connect Your Device
|
||||
|
||||
Ensure your **iPhone/iPad** is **connected** and **selected** (Next to MeloNX with the arrow) in Xcode.
|
||||
- You may need to install the iOS SDK. it will say next to MeloNX with the arrow saying "iOS XX Not Installed (GET)"
|
||||
- You will be need to press GET and wait for it to finish downloading and installing
|
||||
- Then you will be able to select your device and Build and Run.
|
||||
|
||||
Make Sure you do **NOT** select the Simulator. (Which is the Generic names and the ones with the non-coloured icons, e.g. "iPhone 16 Pro")
|
||||
|
||||
### 6. Build and Run
|
||||
|
||||
Click the **Run (▶️) button** in Xcode to compile and launch MeloNX.
|
||||
- When running on your device, Click the **Spray Can Button** below the Run button
|
||||
- Right Click where it says "> MeloNX PID XXXX"
|
||||
- Press Detach in the Context Menu.
|
||||
|
||||
---
|
||||
|
||||
Now you're all set! 🚀 If you encounter issues, please join the discord at https://melonx.org
|
||||
```
|
14
LICENSE.txt
14
LICENSE.txt
@ -1,15 +1,9 @@
|
||||
MeloNX License
|
||||
MIT License
|
||||
|
||||
Copyright (c) MeloNX Team and Contributors
|
||||
Copyright (c) Ryujinx Team and Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person (except anyone who has previously attempted or is currently attempting to merge MeloNX with Pomelo) obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. Every file is under this license, and all copies must be redistributed under the same license.
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Anyone who attempts or has attempted to merge MeloNX with Pomelo, or otherwise use this source code in conjunction with Pomelo, is prohibited from using, copying, modifying, or distributing the source code without first obtaining explicit, written permission from Stossy11.
|
||||
|
||||
Additionally, the names of the developers or contributors to this project may not be used to endorse or promote products derived from this software without specific, prior written permission from the respective developer(s).
|
||||
|
||||
Ryujinx is licensed under the MIT License. Copyright (c) Ryujinx contributors. All rights to Ryujinx are held by its respective copyright holders, and its use is subject to the terms of the MIT License.
|
115
README.md
115
README.md
@ -1,123 +1,22 @@
|
||||
<p align="center">
|
||||
<a href="https://melonx.org">
|
||||
<img src="https://melonx.org/static/imgs/MeloNX.svg" alt="MeloNX Logo" width="120">
|
||||
<a href="https://github.com/MeloNX-Emu/MeloNX">
|
||||
<img src="https://github.com/MeloNX-Emu/melonx-emu.github.io/blob/main/favicon.png?raw=true" alt="MeloNX Logo" width="120">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">MeloNX</h1>
|
||||
|
||||
|
||||
|
||||
<p align="center">
|
||||
MeloNX enables Nintendo Switch game emulation on iOS using the Ryujinx iOS code base.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
MeloNX is an iOS Nintendo Switch emulator based on Ryujinx, written primarily in C#. Designed to bring accurate performance and a user-friendly interface to iOS, MeloNX makes Switch games accessible on Apple devices.
|
||||
Developed from the ground up, MeloNX is open-source and available on Github under the <a href="https://github.com/MeloNX-Emu/MeloNX/blob/master/LICENSE.txt" target="_blank">MeloNX license (Based on MIT)</a>. <br
|
||||
Developed from the ground up, MeloNX is open-source and available on Github under the <a href="https://github.com/MeloNX-Emu/MeloNX/blob/master/LICENSE.txt" target="_blank">MIT license</a>. <br
|
||||
</p>
|
||||
|
||||
# Compatibility
|
||||
## Compatibility
|
||||
|
||||
MeloNX works on iPhone XS/XR and later and iPad 8th Gen and later. Check out the Compatibility on the <a href="https://melonx.org/compatibility/" target="_blank">website</a>.
|
||||
MeloNX works on iPhone X and later and iPad 7th Gen and later. A lot of games work.
|
||||
|
||||
# Usage
|
||||
## Usage
|
||||
|
||||
## FAQ
|
||||
- MeloNX is made for iOS 17+, on iOS 15 - 16 MeloNX can be installed but will have issues or may not work at all.
|
||||
- MeloNX needs Xcode or a Paid Apple Developer Account. SideStore support may come soon (SideStore Side Issue)
|
||||
- MeloNX needs JIT
|
||||
- Recommended Device: iPhone 15 Pro or newer.
|
||||
- Low-End Recommended Device**: iPhone 13 Pro.
|
||||
- Lowest Supported Device: iPhone XR
|
||||
|
||||
|
||||
## How to install
|
||||
|
||||
### Paid Developer Account
|
||||
|
||||
1. **Sideload the App**
|
||||
- Use any sideloading tool that supports Apple IDs.
|
||||
|
||||
2. **Enable Entitlements**
|
||||
- Visit [Apple Developer Identifiers](https://developer.apple.com/account/resources/identifiers).
|
||||
- Locate **MeloNX** and enable the following entitlements:
|
||||
- `Increased Memory Limit`
|
||||
- `Increased Debugging Memory Limit`
|
||||
|
||||
3. **Reinstall the App**
|
||||
- Delete the existing installation.
|
||||
- Sideload the app again with the updated entitlements.
|
||||
|
||||
4. **Enable JIT**
|
||||
- Use your preferred method to enable Just-In-Time (JIT) compilation.
|
||||
|
||||
5. **Add Necessary Files**
|
||||
|
||||
If having Issues installing firmware (Make sure your Keys are installed first)
|
||||
- If needed, install firmware and keys from **Ryujinx Desktop**.
|
||||
- Copy the **bis** and **system** folders
|
||||
|
||||
### Xcode
|
||||
|
||||
**NOTE: These Xcode builds are nightly and may have unfinished features.**
|
||||
|
||||
1. **Compile Guide**
|
||||
- Visit the [guide here](https://git.743378673.xyz/MeloNX/MeloNX/src/branch/XC-ios-ht/Compile.md).
|
||||
|
||||
2. **Add Necessary Files**
|
||||
|
||||
If having Issues installing firmware (Make sure your Keys are installed first)
|
||||
- If needed, install firmware and keys from **Ryujinx Desktop**.
|
||||
- Copy the **bis** and **system** folders
|
||||
|
||||
## Features
|
||||
|
||||
- **Audio**
|
||||
|
||||
Audio output is entirely supported, audio input (microphone) isn't supported.
|
||||
We use C# wrappers for [OpenAL](https://openal-soft.org/), and [SDL2](https://www.libsdl.org/) & [libsoundio](http://libsound.io/) as fallbacks.
|
||||
|
||||
- **CPU**
|
||||
|
||||
The CPU emulator, ARMeilleure, emulates an ARMv8 CPU and currently has support for most 64-bit ARMv8 and some of the ARMv7 (and older) instructions, including partial 32-bit support.
|
||||
It translates the ARM code to a custom IR, performs a few optimizations, and turns that into x86 code.
|
||||
There are three memory manager options available depending on the user's preference, leveraging both software-based (slower) and host-mapped modes (much faster).
|
||||
The fastest option (host, unchecked) is set by default.
|
||||
Ryujinx also features an optional Profiled Persistent Translation Cache, which essentially caches translated functions so that they do not need to be translated every time the game loads.
|
||||
The net result is a significant reduction in load times (the amount of time between launching a game and arriving at the title screen) for nearly every game.
|
||||
NOTE: This feature is enabled by default, You must launch the game at least twice to the title screen or beyond before performance improvements are unlocked on the third launch!
|
||||
These improvements are permanent and do not require any extra launches going forward.
|
||||
|
||||
- **GPU**
|
||||
|
||||
The GPU emulator emulates the Switch's Maxwell GPU using Metal (via MoltenVK) APIs through a custom build of OpenTK or Silk.NET respectively.
|
||||
|
||||
- **Input**
|
||||
|
||||
We currently have support for keyboard, touch input, JoyCon input support, and nearly all controllers.
|
||||
Motion controls are natively supported in most cases; for dual-JoyCon motion support, DS4Windows or BetterJoy are currently required.
|
||||
In all scenarios, you can set up everything inside the input configuration menu.
|
||||
|
||||
- **DLC & Modifications**
|
||||
|
||||
MeloNX supports DLC + Game Update Add-ons.
|
||||
Mods (romfs, exefs, and runtime mods such as cheats) are supported;
|
||||
|
||||
- **Configuration**
|
||||
|
||||
The emulator has settings for enabling or disabling some logging, remapping controllers, and more.
|
||||
|
||||
## License
|
||||
|
||||
This software is licensed under the terms of the [MeloNX license (Based on MIT License)](LICENSE.txt).
|
||||
This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3.
|
||||
See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY.md) for more details.
|
||||
|
||||
## Credits
|
||||
|
||||
- [Ryujinx](https://github.com/ryujinx-mirror/ryujinx) is used for the base of this emulator. (link is to ryujinx-mirror since they were supportive)
|
||||
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
|
||||
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
|
||||
- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
|
||||
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.
|
||||
To run MeloNX on your iOS device, at least 4GB of RAM is recommended to ensure stability. For full instructions, refer to our [Setup Guide](https://github.com/MeloNX-Emu/MeloNX/wiki/Setup-Guide).
|
||||
|
25
repo.json
25
repo.json
@ -1,25 +0,0 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"beta": false,
|
||||
"bundleIdentifier": "com.stossy11.MeloNX",
|
||||
"developerName": "Stossy11",
|
||||
"downloadURL": "https://git.743378673.xyz/MeloNX/MeloNX/releases/download/1.7.0/MeloNX.ipa",
|
||||
"iconURL": "https://melonx.org/static/imgs/MeloNX.svg",
|
||||
"localizedDescription": "MeloNX is an iOS Nintendo Switch emulator based on Ryujinx, written primarily in C#. Designed to bring accurate performance and a user-friendly interface to iOS, MeloNX makes Switch games accessible on Apple devices. Developed from the ground up, MeloNX is open-source and available on Github under the MeloNX license (Based on MIT).",
|
||||
"name": "MeloNX",
|
||||
"screenshotURLs": [
|
||||
],
|
||||
"size": 68015680,
|
||||
"subtitle": "A Nintendo Switch Emulator based on Ryujinx for iOS",
|
||||
"tintColor": "",
|
||||
"version": "1.7.0",
|
||||
"versionDate": "2025-03-1T14:00:00-07:00",
|
||||
"versionDescription": "Add SDL Window back\nSDL Window can now be enabled on all OS versions\n"
|
||||
}
|
||||
],
|
||||
"identifier": "com.stossy11.MeloNX",
|
||||
"name": "MeloNX",
|
||||
"news": [],
|
||||
"userInfo": {}
|
||||
}
|
@ -7,7 +7,6 @@ namespace ARMeilleure.Memory
|
||||
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
|
||||
|
||||
public IJitMemoryBlock Block { get; }
|
||||
public IJitMemoryAllocator Allocator { get; }
|
||||
|
||||
public IntPtr Pointer => Block.Pointer;
|
||||
|
||||
@ -22,7 +21,6 @@ namespace ARMeilleure.Memory
|
||||
granularity = DefaultGranularity;
|
||||
}
|
||||
|
||||
Allocator = allocator;
|
||||
Block = allocator.Reserve(maxSize);
|
||||
_maxSize = maxSize;
|
||||
_sizeGranularity = granularity;
|
||||
|
@ -3,7 +3,6 @@ using ARMeilleure.CodeGen.Unwinding;
|
||||
using ARMeilleure.Memory;
|
||||
using ARMeilleure.Native;
|
||||
using Ryujinx.Memory;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@ -16,11 +15,11 @@ namespace ARMeilleure.Translation.Cache
|
||||
static partial class JitCache
|
||||
{
|
||||
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
|
||||
private static readonly int _pageMask = _pageSize - 4;
|
||||
private static readonly int _pageMask = _pageSize - 8;
|
||||
|
||||
private const int CodeAlignment = 4; // Bytes.
|
||||
private const int CacheSize = 128 * 1024 * 1024;
|
||||
private const int CacheSizeIOS = 128 * 1024 * 1024;
|
||||
private const int CacheSize = 128 * 1024 * 1024;
|
||||
private const int CacheSizeIOS = 64 * 1024 * 1024;
|
||||
|
||||
private static ReservedRegion _jitRegion;
|
||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||
@ -32,10 +31,6 @@ namespace ARMeilleure.Translation.Cache
|
||||
private static readonly object _lock = new();
|
||||
private static bool _initialized;
|
||||
|
||||
private static readonly List<ReservedRegion> _jitRegions = new();
|
||||
|
||||
private static int _activeRegionIndex = 0;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize);
|
||||
@ -54,11 +49,7 @@ namespace ARMeilleure.Translation.Cache
|
||||
return;
|
||||
}
|
||||
|
||||
var firstRegion = new ReservedRegion(allocator, CacheSize);
|
||||
|
||||
|
||||
_jitRegions.Add(firstRegion);
|
||||
_activeRegionIndex = 0;
|
||||
_jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize));
|
||||
|
||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
|
||||
{
|
||||
@ -69,9 +60,7 @@ namespace ARMeilleure.Translation.Cache
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
JitUnwindWindows.InstallFunctionTableHandler(
|
||||
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
|
||||
);
|
||||
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
@ -84,9 +73,7 @@ namespace ARMeilleure.Translation.Cache
|
||||
{
|
||||
while (_deferredRxProtect.TryDequeue(out var result))
|
||||
{
|
||||
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
||||
|
||||
ReprotectAsExecutable(targetRegion, result.funcOffset, result.length);
|
||||
ReprotectAsExecutable(result.funcOffset, result.length);
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,14 +87,21 @@ namespace ARMeilleure.Translation.Cache
|
||||
|
||||
int funcOffset = Allocate(code.Length, deferProtect);
|
||||
|
||||
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
||||
IntPtr funcPtr = targetRegion.Pointer + funcOffset;
|
||||
IntPtr funcPtr = _jitRegion.Pointer + funcOffset;
|
||||
|
||||
if (OperatingSystem.IsIOS())
|
||||
{
|
||||
Marshal.Copy(code, 0, funcPtr, code.Length);
|
||||
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
||||
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
|
||||
if (deferProtect)
|
||||
{
|
||||
_deferredRxProtect.Enqueue((funcOffset, code.Length));
|
||||
}
|
||||
else
|
||||
{
|
||||
ReprotectAsExecutable(funcOffset, code.Length);
|
||||
|
||||
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
|
||||
}
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
@ -121,9 +115,9 @@ namespace ARMeilleure.Translation.Cache
|
||||
}
|
||||
else
|
||||
{
|
||||
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
|
||||
ReprotectAsWritable(funcOffset, code.Length);
|
||||
Marshal.Copy(code, 0, funcPtr, code.Length);
|
||||
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
||||
ReprotectAsExecutable(funcOffset, code.Length);
|
||||
|
||||
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||
{
|
||||
@ -145,50 +139,41 @@ namespace ARMeilleure.Translation.Cache
|
||||
{
|
||||
if (OperatingSystem.IsIOS())
|
||||
{
|
||||
// return;
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var region in _jitRegions)
|
||||
Debug.Assert(_initialized);
|
||||
|
||||
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
|
||||
|
||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||
{
|
||||
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
|
||||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
|
||||
|
||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||
{
|
||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||
_cacheEntries.RemoveAt(entryIndex);
|
||||
}
|
||||
|
||||
return;
|
||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||
_cacheEntries.RemoveAt(entryIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
|
||||
private static void ReprotectAsWritable(int offset, int size)
|
||||
{
|
||||
int endOffs = offset + size;
|
||||
|
||||
int regionStart = offset & ~_pageMask;
|
||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||
|
||||
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
}
|
||||
|
||||
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
|
||||
private static void ReprotectAsExecutable(int offset, int size)
|
||||
{
|
||||
int endOffs = offset + size;
|
||||
|
||||
int regionStart = offset & ~_pageMask;
|
||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||
|
||||
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||
}
|
||||
|
||||
private static int Allocate(int codeSize, bool deferProtect = false)
|
||||
@ -202,35 +187,20 @@ namespace ARMeilleure.Translation.Cache
|
||||
alignment = 0x4000;
|
||||
}
|
||||
|
||||
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
|
||||
{
|
||||
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
|
||||
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
|
||||
|
||||
if (allocOffset >= 0)
|
||||
{
|
||||
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||
_activeRegionIndex = i;
|
||||
return allocOffset;
|
||||
}
|
||||
//DEBUG: Show JIT Memory Allocation
|
||||
|
||||
//Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}");
|
||||
|
||||
if (allocOffset < 0)
|
||||
{
|
||||
throw new OutOfMemoryException("JIT Cache exhausted.");
|
||||
}
|
||||
|
||||
int exhaustedRegion = _activeRegionIndex;
|
||||
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
|
||||
_jitRegions.Add(newRegion);
|
||||
_activeRegionIndex = _jitRegions.Count - 1;
|
||||
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||
|
||||
int newRegionNumber = _activeRegionIndex;
|
||||
|
||||
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
|
||||
|
||||
int allocOffsetNew = _cacheAllocator.Allocate(ref codeSize, alignment);
|
||||
if (allocOffsetNew < 0)
|
||||
{
|
||||
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
|
||||
}
|
||||
|
||||
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
|
||||
return allocOffsetNew;
|
||||
return allocOffset;
|
||||
}
|
||||
|
||||
private static int AlignCodeSize(int codeSize, bool deferProtect = false)
|
||||
|
@ -25,9 +25,11 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
|
||||
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
||||
4E4854022D138D7600A446A6 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
||||
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
|
||||
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
||||
4EA894EB2D3E3DC700FABB01 /* Ryujinx.Headless.SDL2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */; };
|
||||
4EA894EC2D3E3DC700FABB01 /* Ryujinx.Headless.SDL2.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
5650564B2D2A758600C8BB1E /* dotnet.xcconfig.example in Resources */ = {isa = PBXBuildFile; fileRef = 5650564A2D2A758600C8BB1E /* dotnet.xcconfig.example */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -55,22 +57,13 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
4E50F49E2D5CC28B0080F1D1 /* Embed Watch Content */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Watch Content";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
4E80AA092CD6FAA800029585 /* Embed Libraries */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
4EA894EC2D3E3DC700FABB01 /* Ryujinx.Headless.SDL2.dylib in Embed Libraries */,
|
||||
);
|
||||
name = "Embed Libraries";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -78,19 +71,19 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
4E7023A52D5A98E2002C7183 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||
4E80A98D2CD6F54500029585 /* MeloNX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MeloNX.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4E80A99D2CD6F54700029585 /* MeloNXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4E80A9A72CD6F54700029585 /* MeloNXUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4E80AA622CD7122800029585 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = System/Library/Frameworks/GameController.framework; sourceTree = SDKROOT; };
|
||||
5650564A2D2A758600C8BB1E /* dotnet.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = dotnet.xcconfig; sourceTree = "<group>"; };
|
||||
5650564A2D2A758600C8BB1E /* dotnet.xcconfig.example */ = {isa = PBXFileReference; lastKnownFileType = text; path = dotnet.xcconfig.example; sourceTree = "<group>"; };
|
||||
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = Ryujinx.Headless.SDL2.dylib; path = "MeloNX/Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
CA8F9C2D2D3F5A3A00D7E586 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
565056492D2A756A00C8BB1E /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
|
||||
Info.plist,
|
||||
);
|
||||
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
||||
@ -98,17 +91,14 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */
|
||||
CA0AE31D2D3EECBC00F6D350 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = {
|
||||
5650564D2D2A75B300C8BB1E /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;
|
||||
attributesByRelativePath = {
|
||||
"Dependencies/Dynamic Libraries/Hypervisor.framework" = (
|
||||
CodeSignOnCopy,
|
||||
RemoveHeadersOnCopy,
|
||||
);
|
||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
|
||||
CodeSignOnCopy,
|
||||
);
|
||||
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework" = (
|
||||
"Dependencies/Dynamic Libraries/SoftwareKeyboard.framework" = (
|
||||
CodeSignOnCopy,
|
||||
RemoveHeadersOnCopy,
|
||||
);
|
||||
@ -168,8 +158,7 @@
|
||||
"Dependencies/Dynamic Libraries/libavcodec.dylib",
|
||||
"Dependencies/Dynamic Libraries/libavutil.dylib",
|
||||
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
|
||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
|
||||
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework",
|
||||
"Dependencies/Dynamic Libraries/SoftwareKeyboard.framework",
|
||||
Dependencies/XCFrameworks/libavcodec.xcframework,
|
||||
Dependencies/XCFrameworks/libavfilter.xcframework,
|
||||
Dependencies/XCFrameworks/libavformat.xcframework,
|
||||
@ -185,7 +174,7 @@
|
||||
/* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
4E80A98F2CD6F54500029585 /* MeloNX */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CA8F9C2D2D3F5A3A00D7E586 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, CA0AE31D2D3EECBC00F6D350 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = MeloNX; sourceTree = "<group>"; };
|
||||
4E80A98F2CD6F54500029585 /* MeloNX */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (565056492D2A756A00C8BB1E /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 5650564D2D2A75B300C8BB1E /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = MeloNX; sourceTree = "<group>"; };
|
||||
4E80A9A02CD6F54700029585 /* MeloNXTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MeloNXTests; sourceTree = "<group>"; };
|
||||
4E80A9AA2CD6F54700029585 /* MeloNXUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MeloNXUITests; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
@ -195,10 +184,10 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4E4854022D138D7600A446A6 /* GameController.framework in Frameworks */,
|
||||
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
|
||||
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
|
||||
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
|
||||
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
|
||||
4EA894EB2D3E3DC700FABB01 /* Ryujinx.Headless.SDL2.dylib in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -222,7 +211,7 @@
|
||||
4E80A9842CD6F54500029585 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5650564A2D2A758600C8BB1E /* dotnet.xcconfig */,
|
||||
5650564A2D2A758600C8BB1E /* dotnet.xcconfig.example */,
|
||||
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */,
|
||||
4E80A98F2CD6F54500029585 /* MeloNX */,
|
||||
4E80A9A02CD6F54700029585 /* MeloNXTests */,
|
||||
@ -245,7 +234,6 @@
|
||||
4E80AA192CD700F500029585 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4E7023A52D5A98E2002C7183 /* UIKit.framework */,
|
||||
4E80AA622CD7122800029585 /* GameController.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
@ -260,7 +248,7 @@
|
||||
buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */;
|
||||
buildPhases = (
|
||||
);
|
||||
buildToolPath = /usr/local/share/dotnet/dotnet;
|
||||
buildToolPath = "$(DOTNET_PATH)";
|
||||
buildWorkingDirectory = "$(SRCROOT)/../..";
|
||||
dependencies = (
|
||||
);
|
||||
@ -281,7 +269,6 @@
|
||||
4E80A98A2CD6F54500029585 /* Frameworks */,
|
||||
4E80A98B2CD6F54500029585 /* Resources */,
|
||||
4E80AA092CD6FAA800029585 /* Embed Libraries */,
|
||||
4E50F49E2D5CC28B0080F1D1 /* Embed Watch Content */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -352,7 +339,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1620;
|
||||
LastSwiftUpdateCheck = 1610;
|
||||
LastUpgradeCheck = 1610;
|
||||
TargetAttributes = {
|
||||
4E80A98C2CD6F54500029585 = {
|
||||
@ -406,6 +393,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5650564B2D2A758600C8BB1E /* dotnet.xcconfig.example in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -647,47 +635,18 @@
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(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 = fast;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = MeloNX/Info.plist;
|
||||
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
|
||||
INFOPLIST_KEY_GCSupportsGameMode = YES;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "MeloNX needs access to your Photo Library in order to save images";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -709,6 +668,36 @@
|
||||
"$(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",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
@ -761,31 +750,6 @@
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
4E80A9B32CD6F54700029585 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/XCFrameworks",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
@ -822,20 +786,103 @@
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(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 = 0.0.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
4E80A9B32CD6F54700029585 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/XCFrameworks",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(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 = fast;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = MeloNX/Info.plist;
|
||||
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
|
||||
INFOPLIST_KEY_GCSupportsGameMode = YES;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "MeloNX needs access to your Photo Library in order to save images";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@ -857,6 +904,18 @@
|
||||
"$(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",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
@ -909,8 +968,113 @@
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/XCFrameworks",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(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 = 1.3.0;
|
||||
MARKETING_VERSION = 0.0.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b",
|
||||
"originHash" : "1b46f7a56d6f994a826e31441c25b929398800cf38b3e9be23ae6e0ef8fc32c7",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "swiftsvg",
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>MeloNX.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>Ryujinx.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "271EB822-2830-4016-A3D7-CA2DEBEDCD27"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
<Breakpoints>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "499F5405-B63B-4623-9332-1E44FC449FD0"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "MeloNX/Views/GamesList/GameListView.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "309"
|
||||
endingLineNumber = "309"
|
||||
landmarkName = "loadGames()"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
<BreakpointProxy
|
||||
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||
<BreakpointContent
|
||||
uuid = "0BB7C122-8933-48E8-ABA3-1ABB39594258"
|
||||
shouldBeEnabled = "No"
|
||||
ignoreCount = "0"
|
||||
continueAfterRunningActions = "No"
|
||||
filePath = "MeloNX/Models/Game.swift"
|
||||
startingColumnNumber = "9223372036854775807"
|
||||
endingColumnNumber = "9223372036854775807"
|
||||
startingLineNumber = "37"
|
||||
endingLineNumber = "37"
|
||||
landmarkName = "createImage(from:)"
|
||||
landmarkType = "7">
|
||||
</BreakpointContent>
|
||||
</BreakpointProxy>
|
||||
</Breakpoints>
|
||||
</Bucket>
|
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>MeloNX.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>Ryujinx.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>4E80A98C2CD6F54500029585</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>4E80A99C2CD6F54700029585</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>4E80A9A62CD6F54700029585</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>MeloNX.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>Ryujinx.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
@ -1,58 +0,0 @@
|
||||
//
|
||||
// EntitlementChecker.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 15/02/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
typealias SecTaskRef = OpaquePointer
|
||||
|
||||
@_silgen_name("SecTaskCopyValueForEntitlement")
|
||||
func SecTaskCopyValueForEntitlement(
|
||||
_ task: SecTaskRef,
|
||||
_ entitlement: NSString,
|
||||
_ error: NSErrorPointer
|
||||
) -> CFTypeRef?
|
||||
|
||||
@_silgen_name("SecTaskCreateFromSelf")
|
||||
func SecTaskCreateFromSelf(
|
||||
_ allocator: CFAllocator?
|
||||
) -> SecTaskRef?
|
||||
|
||||
@_silgen_name("SecTaskCopyValuesForEntitlements")
|
||||
func SecTaskCopyValuesForEntitlements(
|
||||
_ task: SecTaskRef,
|
||||
_ entitlements: CFArray,
|
||||
_ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
|
||||
) -> CFDictionary?
|
||||
|
||||
func checkAppEntitlements(_ ents: [String]) -> [String: Any] {
|
||||
guard let task = SecTaskCreateFromSelf(nil) else {
|
||||
print("Failed to create SecTask")
|
||||
return [:]
|
||||
}
|
||||
|
||||
guard let entitlements = SecTaskCopyValuesForEntitlements(task, ents as CFArray, nil) else {
|
||||
print("Failed to get entitlements")
|
||||
return [:]
|
||||
}
|
||||
|
||||
return (entitlements as? [String: Any]) ?? [:]
|
||||
}
|
||||
|
||||
func checkAppEntitlement(_ ent: String) -> Bool {
|
||||
guard let task = SecTaskCreateFromSelf(nil) else {
|
||||
print("Failed to create SecTask")
|
||||
return false
|
||||
}
|
||||
|
||||
guard let entitlements = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else {
|
||||
print("Failed to get entitlements")
|
||||
return false
|
||||
}
|
||||
|
||||
return entitlements.boolValue != nil && entitlements.boolValue
|
||||
}
|
@ -5,15 +5,11 @@
|
||||
// Created by Stossy11 on 3/11/2024.
|
||||
//
|
||||
|
||||
#define DRM 0
|
||||
#define CS_DEBUGGED 0x10000000
|
||||
|
||||
#ifndef RyujinxHeader
|
||||
#define RyujinxHeader
|
||||
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_syswm.h>
|
||||
#import "SDL2/SDL.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@ -22,28 +18,15 @@ extern "C" {
|
||||
struct GameInfo {
|
||||
long FileSize;
|
||||
char TitleName[512];
|
||||
char TitleId[32];
|
||||
long TitleId;
|
||||
char Developer[256];
|
||||
char Version[16];
|
||||
unsigned char* ImageData;
|
||||
int Version;
|
||||
unsigned char* ImageData;
|
||||
unsigned int ImageSize;
|
||||
};
|
||||
|
||||
struct DlcNcaListItem {
|
||||
char Path[256];
|
||||
unsigned long TitleId;
|
||||
};
|
||||
|
||||
struct DlcNcaList {
|
||||
bool success;
|
||||
unsigned int size;
|
||||
struct DlcNcaListItem* items;
|
||||
};
|
||||
|
||||
extern struct GameInfo get_game_info(int, char*);
|
||||
|
||||
extern struct DlcNcaList get_dlc_nca_list(const char* titleIdPtr, const char* pathPtr);
|
||||
|
||||
void install_firmware(const char* inputPtr);
|
||||
|
||||
char* installed_firmware_version();
|
||||
@ -56,6 +39,8 @@ int get_current_fps();
|
||||
|
||||
void initialize();
|
||||
|
||||
const char* get_game_controllers();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
@ -1,53 +0,0 @@
|
||||
//
|
||||
// IsJITEnabled.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 10/02/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
|
||||
var region: vm_address_t = vm_address_t(UInt(bitPattern: address))
|
||||
var regionSize: vm_size_t = 0
|
||||
var info = vm_region_basic_info_64()
|
||||
var infoCount = mach_msg_type_number_t(MemoryLayout<vm_region_basic_info_64>.size / MemoryLayout<integer_t>.size)
|
||||
var objectName: mach_port_t = UInt32(MACH_PORT_NULL)
|
||||
|
||||
let result = withUnsafeMutablePointer(to: &info) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: Int(infoCount)) {
|
||||
vm_region_64(mach_task_self_, ®ion, ®ionSize, VM_REGION_BASIC_INFO_64, $0, &infoCount, &objectName)
|
||||
}
|
||||
}
|
||||
|
||||
if result != KERN_SUCCESS {
|
||||
print("Failed to reach \(address)")
|
||||
return false
|
||||
}
|
||||
|
||||
return info.protection & VM_PROT_EXECUTE != 0
|
||||
}
|
||||
|
||||
func isJITEnabled() -> Bool {
|
||||
let pageSize = sysconf(_SC_PAGESIZE)
|
||||
let code: [UInt32] = [0x52800540, 0xD65F03C0]
|
||||
|
||||
guard let jitMemory = mmap(nil, pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0), jitMemory != MAP_FAILED else {
|
||||
return false
|
||||
}
|
||||
|
||||
defer {
|
||||
munmap(jitMemory, pageSize)
|
||||
}
|
||||
|
||||
|
||||
memcpy(jitMemory, code, code.count)
|
||||
|
||||
if mprotect(jitMemory, pageSize, PROT_READ | PROT_EXEC) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
let checkMem = checkMemoryPermissions(at: jitMemory)
|
||||
|
||||
return checkMem
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
//
|
||||
// EnableJIT.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 10/02/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func enableJITEB() {
|
||||
guard let bundleID = Bundle.main.bundleIdentifier else {
|
||||
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 {
|
||||
showLaunchAppAlert(jsonData: data!, in: UIApplication.shared.windows.last!.rootViewController!)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
struct LaunchApp: Codable {
|
||||
let ok: Bool
|
||||
let error: String?
|
||||
let launching: Bool
|
||||
let position: Int?
|
||||
let mounting: Bool
|
||||
}
|
||||
|
||||
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
|
||||
do {
|
||||
let result = try JSONDecoder().decode(LaunchApp.self, from: jsonData)
|
||||
|
||||
var message = ""
|
||||
|
||||
if let error = result.error {
|
||||
message = "Error: \(error)"
|
||||
} else if result.mounting {
|
||||
message = "App is mounting..."
|
||||
} else if result.launching {
|
||||
message = "App is launching..."
|
||||
} else {
|
||||
message = "App launch status unknown."
|
||||
}
|
||||
|
||||
if let position = result.position {
|
||||
message += "\nPosition: \(position)"
|
||||
}
|
||||
|
||||
let alert = UIAlertController(title: "Launch Status", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
viewController.present(alert, animated: true)
|
||||
}
|
||||
|
||||
} catch {
|
||||
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
viewController.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
27
src/MeloNX/MeloNX/App/Core/JIT/utils.h
Normal file
27
src/MeloNX/MeloNX/App/Core/JIT/utils.h
Normal file
@ -0,0 +1,27 @@
|
||||
#if __has_feature(modules)
|
||||
@import UIKit;
|
||||
@import Foundation;
|
||||
#else
|
||||
#import "UIKit/UIKit.h"
|
||||
#import "Foundation/Foundation.h"
|
||||
#endif
|
||||
|
||||
#define DISPATCH_ASYNC_START dispatch_async(dispatch_get_main_queue(), ^{
|
||||
#define DISPATCH_ASYNC_CLOSE });
|
||||
|
||||
#define PT_TRACE_ME 0
|
||||
extern int ptrace(int, pid_t, caddr_t, int);
|
||||
|
||||
#define CS_DEBUGGED 0x10000000
|
||||
extern int csops(
|
||||
pid_t pid,
|
||||
unsigned int ops,
|
||||
void *useraddr,
|
||||
size_t usersize
|
||||
);
|
||||
|
||||
extern BOOL getEntitlementValue(NSString *key);
|
||||
extern BOOL isJITEnabled(void);
|
||||
|
||||
#define DLOG(format, ...) ShowAlert(@"DEBUG", [NSString stringWithFormat:@"\n %s [Line %d] \n %@", __PRETTY_FUNCTION__, __LINE__, [NSString stringWithFormat:format, ##__VA_ARGS__]])
|
||||
void ShowAlert(NSString* title, NSString* message, _Bool* showok);
|
91
src/MeloNX/MeloNX/App/Core/JIT/utils.m
Normal file
91
src/MeloNX/MeloNX/App/Core/JIT/utils.m
Normal file
@ -0,0 +1,91 @@
|
||||
#import "utils.h"
|
||||
|
||||
typedef struct __SecTask * SecTaskRef;
|
||||
extern CFTypeRef SecTaskCopyValueForEntitlement(
|
||||
SecTaskRef task,
|
||||
NSString* entitlement,
|
||||
CFErrorRef _Nullable *error
|
||||
)
|
||||
__attribute__((weak_import));
|
||||
|
||||
extern SecTaskRef SecTaskCreateFromSelf(CFAllocatorRef allocator)
|
||||
__attribute__((weak_import));
|
||||
|
||||
BOOL getEntitlementValue(NSString *key)
|
||||
{
|
||||
if (SecTaskCreateFromSelf == NULL || SecTaskCopyValueForEntitlement == NULL)
|
||||
return NO;
|
||||
SecTaskRef sec_task = SecTaskCreateFromSelf(NULL);
|
||||
if(!sec_task) return NO;
|
||||
CFTypeRef value = SecTaskCopyValueForEntitlement(sec_task, key, nil);
|
||||
if (value != nil)
|
||||
{
|
||||
CFRelease(value);
|
||||
}
|
||||
CFRelease(sec_task);
|
||||
return value != nil && [(__bridge id)value boolValue];
|
||||
}
|
||||
|
||||
BOOL isJITEnabled(void)
|
||||
{
|
||||
if (getEntitlementValue(@"dynamic-codesigning"))
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
int flags;
|
||||
csops(getpid(), 0, &flags, sizeof(flags));
|
||||
return (flags & CS_DEBUGGED) != 0;
|
||||
}
|
||||
|
||||
void ShowAlert(NSString* title, NSString* message, _Bool* showok)
|
||||
{
|
||||
DISPATCH_ASYNC_START
|
||||
UIWindow* mainWindow = [[UIApplication sharedApplication] windows].lastObject;
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
if (showok) {
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"ok!"
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:nil]];
|
||||
}
|
||||
[mainWindow.rootViewController presentViewController:alert
|
||||
animated:true
|
||||
completion:nil];
|
||||
DISPATCH_ASYNC_CLOSE
|
||||
}
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
__attribute__((constructor)) static void entry(int argc, char **argv)
|
||||
{
|
||||
if (isJITEnabled()) {
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults setBool:YES forKey:@"JIT"];
|
||||
[defaults synchronize]; // Ensure the value is saved immediately
|
||||
} else {
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults setBool:NO forKey:@"JIT"];
|
||||
[defaults synchronize]; // Ensure the value is saved immediately
|
||||
}
|
||||
|
||||
if (getEntitlementValue(@"com.apple.developer.kernel.increased-memory-limit")) {
|
||||
NSLog(@"Entitlement Does Exist");
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults setBool:YES forKey:@"increased-memory-limit"];
|
||||
[defaults synchronize]; // Ensure the value is saved immediately
|
||||
}
|
||||
|
||||
if (getEntitlementValue(@"com.apple.developer.kernel.increased-debugging-memory-limit")) {
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults setBool:YES forKey:@"increased-debugging-memory-limit"];
|
||||
[defaults synchronize]; // Ensure the value is saved immediately
|
||||
}
|
||||
if (getEntitlementValue(@"com.apple.developer.kernel.extended-virtual-addressing")) {
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults setBool:YES forKey:@"extended-virtual-addressing"];
|
||||
[defaults synchronize]; // Ensure the value is saved immediately
|
||||
}
|
||||
|
||||
}
|
@ -1,237 +0,0 @@
|
||||
//
|
||||
// NativeController.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by XITRIX on 15/02/2025.
|
||||
//
|
||||
|
||||
import CoreHaptics
|
||||
import GameController
|
||||
|
||||
class NativeController: Hashable {
|
||||
private var instanceID: SDL_JoystickID = -1
|
||||
private var controller: OpaquePointer?
|
||||
private var nativeController: GCController
|
||||
private let controllerHaptics: CHHapticEngine?
|
||||
|
||||
public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" }
|
||||
|
||||
init(_ controller: GCController) {
|
||||
nativeController = controller
|
||||
controllerHaptics = nativeController.haptics?.createEngine(withLocality: .default)
|
||||
try? controllerHaptics?.start()
|
||||
setupHandheldController()
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
private func setupHandheldController() {
|
||||
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
|
||||
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
|
||||
}
|
||||
|
||||
var joystickDesc = SDL_VirtualJoystickDesc(
|
||||
version: UInt16(SDL_VIRTUAL_JOYSTICK_DESC_VERSION),
|
||||
type: Uint16(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue),
|
||||
naxes: 6,
|
||||
nbuttons: 15,
|
||||
nhats: 1,
|
||||
vendor_id: 0,
|
||||
product_id: 0,
|
||||
padding: 0,
|
||||
button_mask: 0,
|
||||
axis_mask: 0,
|
||||
name: (controllername as NSString).utf8String,
|
||||
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
||||
Update: { userdata in
|
||||
// Update joystick state here
|
||||
},
|
||||
SetPlayerIndex: { userdata, playerIndex in
|
||||
print("Player index set to \(playerIndex)")
|
||||
},
|
||||
Rumble: { userdata, lowFreq, highFreq in
|
||||
print("Rumble with \(lowFreq), \(highFreq)")
|
||||
guard let userdata else { return 0 }
|
||||
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
|
||||
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics)
|
||||
return 0
|
||||
},
|
||||
RumbleTriggers: { userdata, leftRumble, rightRumble in
|
||||
print("Trigger rumble with \(leftRumble), \(rightRumble)")
|
||||
return 0
|
||||
},
|
||||
SetLED: { userdata, red, green, blue in
|
||||
print("Set LED to RGB(\(red), \(green), \(blue))")
|
||||
return 0
|
||||
},
|
||||
SendEffect: { userdata, data, size in
|
||||
print("Effect sent with size \(size)")
|
||||
return 0
|
||||
}
|
||||
)
|
||||
|
||||
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
|
||||
if instanceID < 0 {
|
||||
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
|
||||
return
|
||||
}
|
||||
|
||||
// Open a game controller for the virtual joystick
|
||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
||||
controller = SDL_GameControllerOpen(Int32(instanceID))
|
||||
|
||||
if controller == nil {
|
||||
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
|
||||
return
|
||||
}
|
||||
|
||||
if #available(iOS 16, *) {
|
||||
guard let gamepad = nativeController.extendedGamepad
|
||||
else { return }
|
||||
|
||||
setupButtonChangeListener(gamepad.buttonA, for: .B)
|
||||
setupButtonChangeListener(gamepad.buttonB, for: .A)
|
||||
setupButtonChangeListener(gamepad.buttonX, for: .Y)
|
||||
setupButtonChangeListener(gamepad.buttonY, for: .X)
|
||||
|
||||
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
|
||||
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
|
||||
setupButtonChangeListener(gamepad.dpad.left, for: .dPadLeft)
|
||||
setupButtonChangeListener(gamepad.dpad.right, for: .dPadRight)
|
||||
|
||||
setupButtonChangeListener(gamepad.leftShoulder, for: .leftShoulder)
|
||||
setupButtonChangeListener(gamepad.rightShoulder, for: .rightShoulder)
|
||||
gamepad.leftThumbstickButton.map { setupButtonChangeListener($0, for: .leftStick) }
|
||||
gamepad.rightThumbstickButton.map { setupButtonChangeListener($0, for: .rightStick) }
|
||||
|
||||
setupButtonChangeListener(gamepad.buttonMenu, for: .start)
|
||||
gamepad.buttonOptions.map { setupButtonChangeListener($0, for: .back) }
|
||||
|
||||
setupStickChangeListener(gamepad.leftThumbstick, for: .left)
|
||||
setupStickChangeListener(gamepad.rightThumbstick, for: .right)
|
||||
|
||||
setupTriggerChangeListener(gamepad.leftTrigger, for: .left)
|
||||
setupTriggerChangeListener(gamepad.rightTrigger, for: .right)
|
||||
}
|
||||
}
|
||||
|
||||
func setupButtonChangeListener(_ button: GCControllerButtonInput, for key: VirtualControllerButton) {
|
||||
button.valueChangedHandler = { [unowned self] _, _, pressed in
|
||||
setButtonState(pressed ? 1 : 0, for: key)
|
||||
}
|
||||
}
|
||||
|
||||
func setupStickChangeListener(_ button: GCControllerDirectionPad, for key: ThumbstickType) {
|
||||
button.valueChangedHandler = { [unowned self] _, xValue, yValue in
|
||||
let scaledX = Sint16(xValue * 32767.0)
|
||||
let scaledY = -Sint16(yValue * 32767.0)
|
||||
|
||||
switch key {
|
||||
case .left:
|
||||
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTX.rawValue))
|
||||
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTY.rawValue))
|
||||
case .right:
|
||||
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
|
||||
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTY.rawValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) {
|
||||
button.valueChangedHandler = { [unowned self] _, value, pressed in
|
||||
// print("Value: \(value), Is pressed: \(pressed)")
|
||||
let axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
||||
let scaledValue = Sint16(value * 32767.0)
|
||||
updateAxisValue(value: scaledValue, forAxis: axis)
|
||||
}
|
||||
}
|
||||
|
||||
static func rumble(lowFreq: Float, highFreq: Float) {
|
||||
do {
|
||||
// Low-frequency haptic pattern
|
||||
let lowFreqPattern = try CHHapticPattern(events: [
|
||||
CHHapticEvent(eventType: .hapticTransient, parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
|
||||
], relativeTime: 0, duration: 0.2)
|
||||
], parameters: [])
|
||||
|
||||
// High-frequency haptic pattern
|
||||
let highFreqPattern = try CHHapticPattern(events: [
|
||||
CHHapticEvent(eventType: .hapticTransient, parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
|
||||
], relativeTime: 0.2, duration: 0.2)
|
||||
], parameters: [])
|
||||
|
||||
// Create and start the haptic engine
|
||||
let engine = try CHHapticEngine()
|
||||
try engine.start()
|
||||
|
||||
// Create and play the low-frequency player
|
||||
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
|
||||
try lowFreqPlayer.start(atTime: 0)
|
||||
|
||||
// Create and play the high-frequency player after a short delay
|
||||
let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
|
||||
try highFreqPlayer.start(atTime: 0.2)
|
||||
|
||||
} catch {
|
||||
print("Error creating haptic patterns: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
|
||||
guard controller != nil else { return }
|
||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
||||
SDL_JoystickSetVirtualAxis(joystick, axis.rawValue, value)
|
||||
}
|
||||
|
||||
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
|
||||
let scaleFactor = 32767.0 / 160
|
||||
|
||||
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
|
||||
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
|
||||
|
||||
if stick == .right {
|
||||
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
|
||||
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTY.rawValue))
|
||||
} else { // ThumbstickType.left
|
||||
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTX.rawValue))
|
||||
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTY.rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
|
||||
guard controller != nil else { return }
|
||||
|
||||
// print("Button: \(button.rawValue) {state: \(state)}")
|
||||
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
|
||||
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
||||
let value: Int = (state == 1) ? 32767 : 0
|
||||
updateAxisValue(value: Sint16(value), forAxis: axis)
|
||||
} else {
|
||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
||||
SDL_JoystickSetVirtualButton(joystick, Int32(button.rawValue), state)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
if let controller {
|
||||
SDL_JoystickDetachVirtual(instanceID)
|
||||
SDL_GameControllerClose(controller)
|
||||
self.controller = nil
|
||||
}
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(nativeController)
|
||||
}
|
||||
|
||||
static func == (lhs: NativeController, rhs: NativeController) -> Bool {
|
||||
lhs.nativeController == rhs.nativeController
|
||||
}
|
||||
}
|
@ -20,10 +20,12 @@ class VirtualController {
|
||||
}
|
||||
|
||||
private func setupVirtualController() {
|
||||
// Initialize SDL if not already initialized
|
||||
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
|
||||
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
|
||||
}
|
||||
|
||||
// Create virtual controller
|
||||
var joystickDesc = SDL_VirtualJoystickDesc(
|
||||
version: UInt16(SDL_VIRTUAL_JOYSTICK_DESC_VERSION),
|
||||
type: Uint16(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue),
|
||||
@ -78,7 +80,7 @@ class VirtualController {
|
||||
}
|
||||
}
|
||||
|
||||
static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
|
||||
static func rumble(lowFreq: Float, highFreq: Float) {
|
||||
do {
|
||||
// Low-frequency haptic pattern
|
||||
let lowFreqPattern = try CHHapticPattern(events: [
|
||||
@ -96,23 +98,9 @@ class VirtualController {
|
||||
], relativeTime: 0.2, duration: 0.2)
|
||||
], parameters: [])
|
||||
|
||||
// Mutable engine
|
||||
var engine = engine
|
||||
|
||||
// If no engine passed, use device engine
|
||||
if engine == nil {
|
||||
// Create and start the haptic engine
|
||||
if hapticEngine == nil {
|
||||
hapticEngine = try CHHapticEngine()
|
||||
try hapticEngine?.start()
|
||||
}
|
||||
|
||||
engine = hapticEngine
|
||||
}
|
||||
|
||||
guard let engine else {
|
||||
return print("Error creating haptic patterns: hapticEngine is nil")
|
||||
}
|
||||
// Create and start the haptic engine
|
||||
let engine = try CHHapticEngine()
|
||||
try engine.start()
|
||||
|
||||
// Create and play the low-frequency player
|
||||
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
|
||||
@ -127,8 +115,6 @@ class VirtualController {
|
||||
}
|
||||
}
|
||||
|
||||
private static var hapticEngine: CHHapticEngine?
|
||||
|
||||
|
||||
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
|
||||
guard controller != nil else { return }
|
||||
|
@ -0,0 +1,80 @@
|
||||
//
|
||||
// VirtualController.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 28/11/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GameController
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
func waitforcontroller() {
|
||||
if let window = theWindow {
|
||||
// Function to recursively search for GCControllerView
|
||||
func findGCControllerView(in view: UIView) -> UIView? {
|
||||
// Check if current view is GCControllerView
|
||||
if String(describing: type(of: view)) == "ControllerView" {
|
||||
return view
|
||||
}
|
||||
|
||||
// Search through subviews
|
||||
for subview in view.subviews {
|
||||
if let found = findGCControllerView(in: subview) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let controllerView = ControllerView()
|
||||
let controllerHostingController = UIHostingController(rootView: controllerView)
|
||||
let containerView = TransparentHostingContainerView(frame: window.bounds)
|
||||
containerView.backgroundColor = .clear
|
||||
|
||||
controllerHostingController.view.frame = containerView.bounds
|
||||
controllerHostingController.view.backgroundColor = .clear
|
||||
containerView.addSubview(controllerHostingController.view)
|
||||
|
||||
class LandscapeViewController: UIViewController {
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .landscape
|
||||
}
|
||||
|
||||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
||||
return .landscapeLeft
|
||||
}
|
||||
}
|
||||
|
||||
let landscapeVC = LandscapeViewController()
|
||||
landscapeVC.modalPresentationStyle = .fullScreen
|
||||
window.rootViewController?.present(landscapeVC, animated: false, completion: nil)
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
if findGCControllerView(in: window) == nil {
|
||||
window.addSubview(containerView)
|
||||
|
||||
window.bringSubviewToFront(containerView)
|
||||
|
||||
timer.invalidate()
|
||||
} else {
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TransparentHostingContainerView: UIView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
// Check if the point is within the subviews of this container
|
||||
let view = super.hitTest(point, with: event)
|
||||
print(view)
|
||||
|
||||
// Return nil if the touch is outside visible content (passes through to views below)
|
||||
return view === self ? nil : view
|
||||
}
|
||||
}
|
@ -8,92 +8,31 @@
|
||||
import Foundation
|
||||
import GameController
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
var theWindow: UIWindow? = nil
|
||||
extension UIWindow {
|
||||
// Makes the SDLWindow use the current WindowScene instead of making its own window.
|
||||
// Also waits for the window to append the on-screen controller
|
||||
@objc func wdb_makeKeyAndVisible() {
|
||||
let enabled = UserDefaults.standard.bool(forKey: "oldWindowCode")
|
||||
|
||||
if #unavailable(iOS 17.0), enabled {
|
||||
if #available(iOS 13.0, *) {
|
||||
self.windowScene = (UIApplication.shared.connectedScenes.first! as! UIWindowScene)
|
||||
}
|
||||
|
||||
self.wdb_makeKeyAndVisible()
|
||||
theWindow = self
|
||||
|
||||
if #available(iOS 17, *) {
|
||||
Ryujinx.shared.repeatuntilfindLayer()
|
||||
} else if UserDefaults.standard.bool(forKey: "isVirtualController") && enabled {
|
||||
waitForController()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iOS 16 and below Only
|
||||
|
||||
var hostingController: UIHostingController<ControllerView>?
|
||||
func waitForController() {
|
||||
guard let window = theWindow else { return }
|
||||
|
||||
// Function to search for an existing UIHostingController with ControllerView
|
||||
func findGCControllerView(in view: UIView) -> UIHostingController<ControllerView>? {
|
||||
if let hostingVC = view.next as? UIHostingController<ControllerView> {
|
||||
return hostingVC
|
||||
}
|
||||
|
||||
for subview in view.subviews {
|
||||
if let found = findGCControllerView(in: subview) {
|
||||
return found
|
||||
if UserDefaults.standard.bool(forKey: "isVirtualController") {
|
||||
if let window = theWindow {
|
||||
|
||||
|
||||
|
||||
waitforcontroller()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let controllerView = ControllerView()
|
||||
let newHostingController = UIHostingController(rootView: controllerView)
|
||||
|
||||
hostingController = newHostingController
|
||||
|
||||
let containerView = newHostingController.view!
|
||||
containerView.backgroundColor = .clear
|
||||
containerView.frame = window.bounds
|
||||
containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
// Timer for controller
|
||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
if findGCControllerView(in: window) == nil {
|
||||
// Adds Virtual Controller Subview
|
||||
window.addSubview(containerView)
|
||||
window.bringSubviewToFront(containerView)
|
||||
|
||||
if let sdlWindow = SDL_GetWindowFromID(1) {
|
||||
SDL_SetWindowPosition(sdlWindow, 0, 0)
|
||||
}
|
||||
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TransparentHostingContainerView: UIView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
// Check if the point is within the subviews of this container
|
||||
let view = super.hitTest(point, with: event)
|
||||
print(view)
|
||||
|
||||
// Return nil if the touch is outside visible content (passes through to views below)
|
||||
return view === self ? nil : view
|
||||
}
|
||||
}
|
||||
|
||||
// Patches makeKeyAndVisible to wdb_makeKeyAndVisible
|
||||
func patchMakeKeyAndVisible() {
|
||||
let uiwindowClass = UIWindow.self
|
||||
if let m1 = class_getInstanceMethod(uiwindowClass, #selector(UIWindow.makeKeyAndVisible)),
|
||||
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// Screenshot.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 09/02/2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIView {
|
||||
func screenshot() -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0)
|
||||
defer { UIGraphicsEndImageContext() }
|
||||
|
||||
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
//
|
||||
// AspectRatio.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 16/02/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum AspectRatio: String, Codable, CaseIterable {
|
||||
case fixed4x3 = "Fixed4x3"
|
||||
case fixed16x9 = "Fixed16x9"
|
||||
case fixed16x10 = "Fixed16x10"
|
||||
case fixed21x9 = "Fixed21x9"
|
||||
case fixed32x9 = "Fixed32x9"
|
||||
case stretched = "Stretched"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .fixed4x3: return "4:3"
|
||||
case .fixed16x9: return "16:9 (Default)"
|
||||
case .fixed16x10: return "16:10"
|
||||
case .fixed21x9: return "21:9"
|
||||
case .fixed32x9: return "32:9"
|
||||
case .stretched: return "Stretched (Full Screen)"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
//
|
||||
// Language.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 16/02/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum SystemLanguage: String, Codable, CaseIterable {
|
||||
case japanese = "Japanese"
|
||||
case americanEnglish = "AmericanEnglish"
|
||||
case french = "French"
|
||||
case german = "German"
|
||||
case italian = "Italian"
|
||||
case spanish = "Spanish"
|
||||
case chinese = "Chinese"
|
||||
case korean = "Korean"
|
||||
case dutch = "Dutch"
|
||||
case portuguese = "Portuguese"
|
||||
case russian = "Russian"
|
||||
case taiwanese = "Taiwanese"
|
||||
case britishEnglish = "BritishEnglish"
|
||||
case canadianFrench = "CanadianFrench"
|
||||
case latinAmericanSpanish = "LatinAmericanSpanish"
|
||||
case simplifiedChinese = "SimplifiedChinese"
|
||||
case traditionalChinese = "TraditionalChinese"
|
||||
case brazilianPortuguese = "BrazilianPortuguese"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .japanese: return "Japanese"
|
||||
case .americanEnglish: return "American English"
|
||||
case .french: return "French"
|
||||
case .german: return "German"
|
||||
case .italian: return "Italian"
|
||||
case .spanish: return "Spanish"
|
||||
case .chinese: return "Chinese"
|
||||
case .korean: return "Korean"
|
||||
case .dutch: return "Dutch"
|
||||
case .portuguese: return "Portuguese"
|
||||
case .russian: return "Russian"
|
||||
case .taiwanese: return "Taiwanese"
|
||||
case .britishEnglish: return "British English"
|
||||
case .canadianFrench: return "Canadian French"
|
||||
case .latinAmericanSpanish: return "Latin American Spanish"
|
||||
case .simplifiedChinese: return "Simplified Chinese"
|
||||
case .traditionalChinese: return "Traditional Chinese"
|
||||
case .brazilianPortuguese: return "Brazilian Portuguese"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
//
|
||||
// Region.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 16/02/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum SystemRegionCode: String, Codable, CaseIterable {
|
||||
case japan = "Japan"
|
||||
case usa = "USA"
|
||||
case europe = "Europe"
|
||||
case australia = "Australia"
|
||||
case china = "China"
|
||||
case korea = "Korea"
|
||||
case taiwan = "Taiwan"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .japan: return "Japan"
|
||||
case .usa: return "United States"
|
||||
case .europe: return "Europe"
|
||||
case .australia: return "Australia"
|
||||
case .china: return "China"
|
||||
case .korea: return "Korea"
|
||||
case .taiwan: return "Taiwan"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,29 +28,17 @@ struct iOSNav<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Ryujinx {
|
||||
private var isRunning = false
|
||||
|
||||
let virtualController = VirtualController()
|
||||
|
||||
@Published var controllerMap: [Controller] = []
|
||||
@Published var metalLayer: CAMetalLayer? = nil
|
||||
@Published var firmwareversion = "0"
|
||||
@Published var emulationUIView = UIView()
|
||||
@Published var games: [Game] = []
|
||||
|
||||
@Published var defMLContentSize: CGFloat?
|
||||
|
||||
var shouldMetal: Bool {
|
||||
metalLayer == nil
|
||||
}
|
||||
@State var firmwareversion = "0"
|
||||
|
||||
static let shared = Ryujinx()
|
||||
|
||||
private init() {
|
||||
self.games = loadGames()
|
||||
}
|
||||
private init() {}
|
||||
|
||||
public struct Configuration : Codable, Equatable {
|
||||
var gamepath: String
|
||||
@ -61,23 +49,13 @@ class Ryujinx {
|
||||
var nintendoinput: Bool
|
||||
var enableInternet: Bool
|
||||
var listinputids: Bool
|
||||
var aspectRatio: AspectRatio
|
||||
var fullscreen: Bool
|
||||
var memoryManagerMode: String
|
||||
var disableShaderCache: Bool
|
||||
var hypervisor: Bool
|
||||
var disableDockedMode: Bool
|
||||
var enableTextureRecompression: Bool
|
||||
var additionalArgs: [String]
|
||||
var maxAnisotropy: Float
|
||||
var macroHLE: Bool
|
||||
var ignoreMissingServices: Bool
|
||||
var expandRam: Bool
|
||||
var dfsIntegrityChecks: Bool
|
||||
var disablePTC: Bool
|
||||
var disablevsync: Bool
|
||||
var language: SystemLanguage
|
||||
var regioncode: SystemRegionCode
|
||||
var handHeldController: Bool
|
||||
|
||||
|
||||
init(gamepath: String,
|
||||
@ -85,8 +63,8 @@ class Ryujinx {
|
||||
debuglogs: Bool = false,
|
||||
tracelogs: Bool = false,
|
||||
listinputids: Bool = false,
|
||||
aspectRatio: AspectRatio = .fixed16x9,
|
||||
memoryManagerMode: String = "HostMappedUnsafe",
|
||||
fullscreen: Bool = true,
|
||||
memoryManagerMode: String = "HostMapped",
|
||||
disableShaderCache: Bool = false,
|
||||
disableDockedMode: Bool = false,
|
||||
nintendoinput: Bool = true,
|
||||
@ -94,24 +72,14 @@ class Ryujinx {
|
||||
enableTextureRecompression: Bool = true,
|
||||
additionalArgs: [String] = [],
|
||||
resscale: Float = 1.00,
|
||||
maxAnisotropy: Float = 0,
|
||||
macroHLE: Bool = false,
|
||||
ignoreMissingServices: Bool = false,
|
||||
hypervisor: Bool = false,
|
||||
expandRam: Bool = false,
|
||||
dfsIntegrityChecks: Bool = false,
|
||||
disablePTC: Bool = false,
|
||||
disablevsync: Bool = false,
|
||||
language: SystemLanguage = .americanEnglish,
|
||||
regioncode: SystemRegionCode = .usa,
|
||||
handHeldController: Bool = false
|
||||
hypervisor: Bool = false
|
||||
) {
|
||||
self.gamepath = gamepath
|
||||
self.inputids = inputids
|
||||
self.debuglogs = debuglogs
|
||||
self.tracelogs = tracelogs
|
||||
self.listinputids = listinputids
|
||||
self.aspectRatio = aspectRatio
|
||||
self.fullscreen = fullscreen
|
||||
self.disableShaderCache = disableShaderCache
|
||||
self.disableDockedMode = disableDockedMode
|
||||
self.enableTextureRecompression = enableTextureRecompression
|
||||
@ -120,17 +88,7 @@ class Ryujinx {
|
||||
self.resscale = resscale
|
||||
self.nintendoinput = nintendoinput
|
||||
self.enableInternet = enableInternet
|
||||
self.maxAnisotropy = maxAnisotropy
|
||||
self.macroHLE = macroHLE
|
||||
self.expandRam = expandRam
|
||||
self.ignoreMissingServices = ignoreMissingServices
|
||||
self.hypervisor = hypervisor
|
||||
self.dfsIntegrityChecks = dfsIntegrityChecks
|
||||
self.disablePTC = disablePTC
|
||||
self.disablevsync = disablevsync
|
||||
self.language = language
|
||||
self.regioncode = regioncode
|
||||
self.handHeldController = handHeldController
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,12 +101,11 @@ class Ryujinx {
|
||||
isRunning = true
|
||||
|
||||
RunLoop.current.perform {
|
||||
|
||||
let url = URL(string: config.gamepath)
|
||||
let url = URL(string: config.gamepath)!
|
||||
|
||||
do {
|
||||
let args = self.buildCommandLineArgs(from: config)
|
||||
let accessing = url?.startAccessingSecurityScopedResource()
|
||||
let accessing = url.startAccessingSecurityScopedResource()
|
||||
|
||||
// Convert Arguments to ones that Ryujinx can Read
|
||||
let cArgs = args.map { strdup($0) }
|
||||
@ -160,8 +117,8 @@ class Ryujinx {
|
||||
|
||||
if result != 0 {
|
||||
self.isRunning = false
|
||||
if let accessing, accessing {
|
||||
url!.stopAccessingSecurityScopedResource()
|
||||
if accessing {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
throw RyujinxError.executionError(code: result)
|
||||
@ -185,54 +142,6 @@ class Ryujinx {
|
||||
var running: Bool {
|
||||
return isRunning
|
||||
}
|
||||
|
||||
|
||||
func loadGames() -> [Game] {
|
||||
let fileManager = FileManager.default
|
||||
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return [] }
|
||||
|
||||
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
|
||||
|
||||
if (!fileManager.fileExists(atPath: romsDirectory.path)) {
|
||||
do {
|
||||
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
print("Failed to create roms directory: \(error)")
|
||||
}
|
||||
}
|
||||
var games: [Game] = []
|
||||
|
||||
do {
|
||||
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
|
||||
|
||||
for fileURLCandidate in files {
|
||||
if fileURLCandidate.pathExtension == "zip" {
|
||||
continue
|
||||
}
|
||||
|
||||
do {
|
||||
let handle = try FileHandle(forReadingFrom: fileURLCandidate)
|
||||
let fileExtension = (fileURLCandidate.pathExtension as NSString).utf8String
|
||||
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
||||
|
||||
|
||||
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
||||
|
||||
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: fileURLCandidate)
|
||||
|
||||
games.append(game)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
return games
|
||||
} catch {
|
||||
print("Error loading games from roms folder: \(error)")
|
||||
return games
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func buildCommandLineArgs(from config: Configuration) -> [String] {
|
||||
var args: [String] = []
|
||||
@ -252,55 +161,27 @@ class Ryujinx {
|
||||
// We don't need this. Ryujinx should handle it fine :3
|
||||
// this also causes crashes in some games :3
|
||||
|
||||
args.append(contentsOf: ["--system-language", config.language.rawValue])
|
||||
if config.fullscreen {
|
||||
args.append(contentsOf: ["--aspect-ratio", "Stretched"])
|
||||
}
|
||||
|
||||
args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
|
||||
|
||||
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
|
||||
|
||||
if config.nintendoinput {
|
||||
args.append("--correct-controller")
|
||||
}
|
||||
|
||||
if config.disablePTC {
|
||||
args.append("--disable-ptc")
|
||||
}
|
||||
|
||||
if config.disablevsync {
|
||||
args.append("--disable-vsync")
|
||||
}
|
||||
// args.append("--disable-vsync")
|
||||
|
||||
|
||||
if config.hypervisor {
|
||||
args.append("--use-hypervisor")
|
||||
}
|
||||
|
||||
if config.dfsIntegrityChecks {
|
||||
args.append("--disable-fs-integrity-checks")
|
||||
}
|
||||
|
||||
|
||||
if config.resscale != 1.0 {
|
||||
args.append(contentsOf: ["--resolution-scale", String(config.resscale)])
|
||||
}
|
||||
|
||||
if config.expandRam {
|
||||
args.append(contentsOf: ["--expand-ram", String(config.expandRam)])
|
||||
}
|
||||
|
||||
if config.ignoreMissingServices {
|
||||
// args.append(contentsOf: ["--ignore-missing-services"])
|
||||
args.append("--ignore-missing-services")
|
||||
}
|
||||
|
||||
if config.maxAnisotropy != 0 {
|
||||
args.append(contentsOf: ["--max-anisotropy", String(config.maxAnisotropy)])
|
||||
}
|
||||
|
||||
if !config.macroHLE {
|
||||
args.append("--disable-macro-hle")
|
||||
}
|
||||
|
||||
if !config.disableShaderCache { // same with disableShaderCache
|
||||
args.append("--disable-shader-cache")
|
||||
}
|
||||
@ -313,25 +194,21 @@ class Ryujinx {
|
||||
}
|
||||
|
||||
if config.debuglogs {
|
||||
args.append("--enable-debug-logs")
|
||||
args.append(contentsOf: ["--enable-debug-logs"])
|
||||
}
|
||||
if config.tracelogs {
|
||||
args.append("--enable-trace-logs")
|
||||
args.append(contentsOf: ["--enable-trace-logs"])
|
||||
}
|
||||
|
||||
// List the input ids
|
||||
if config.listinputids {
|
||||
args.append("--list-inputs-ids")
|
||||
args.append(contentsOf: ["--list-inputs-ids"])
|
||||
}
|
||||
|
||||
// Append the input ids (limit to 8 (used to be 4) just in case)
|
||||
// Append the input ids (limit to 4 just in case)
|
||||
if !config.inputids.isEmpty {
|
||||
config.inputids.prefix(8).enumerated().forEach { index, inputId in
|
||||
if config.handHeldController {
|
||||
args.append(contentsOf: ["\(index == 0 ? "--input-id-handheld" : "--input-id-\(index + 1)")", inputId])
|
||||
} else {
|
||||
args.append(contentsOf: ["--input-id-\(index + 1)", inputId])
|
||||
}
|
||||
config.inputids.prefix(4).enumerated().forEach { index, inputId in
|
||||
args.append(contentsOf: ["--input-id-\(index + 1)", inputId])
|
||||
}
|
||||
}
|
||||
|
||||
@ -372,82 +249,36 @@ class Ryujinx {
|
||||
self.firmwareversion = version
|
||||
}
|
||||
}
|
||||
|
||||
func getDlcNcaList(titleId: String, path: String) -> [DownloadableContentNca] {
|
||||
guard let titleIdCString = titleId.cString(using: .utf8),
|
||||
let pathCString = path.cString(using: .utf8)
|
||||
else {
|
||||
print("Invalid path")
|
||||
return []
|
||||
}
|
||||
|
||||
let listPointer = get_dlc_nca_list(titleIdCString, pathCString)
|
||||
print("DLC parcing success: \(listPointer.success)")
|
||||
guard listPointer.success else { return [] }
|
||||
|
||||
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
|
||||
|
||||
return list.map { item in
|
||||
.init(fullPath: withUnsafePointer(to: item.Path) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}, titleId: item.TitleId, enabled: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func generateGamepadId(joystickIndex: Int32) -> String? {
|
||||
let guid = SDL_JoystickGetDeviceGUID(joystickIndex)
|
||||
|
||||
if guid.data.0 == 0 && guid.data.1 == 0 && guid.data.2 == 0 && guid.data.3 == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
let reorderedGUID: [UInt8] = [
|
||||
guid.data.3, guid.data.2, guid.data.1, guid.data.0,
|
||||
guid.data.5, guid.data.4,
|
||||
guid.data.7, guid.data.6,
|
||||
guid.data.8, guid.data.9,
|
||||
guid.data.10, guid.data.11, guid.data.12, guid.data.13, guid.data.14, guid.data.15
|
||||
]
|
||||
|
||||
let guidString = reorderedGUID.map { String(format: "%02X", $0) }.joined().lowercased()
|
||||
|
||||
func substring(_ str: String, _ start: Int, _ end: Int) -> String {
|
||||
let startIdx = str.index(str.startIndex, offsetBy: start)
|
||||
let endIdx = str.index(str.startIndex, offsetBy: end)
|
||||
return String(str[startIdx..<endIdx])
|
||||
}
|
||||
|
||||
let formattedGUID = "\(substring(guidString, 0, 8))-\(substring(guidString, 8, 12))-\(substring(guidString, 12, 16))-\(substring(guidString, 16, 20))-\(substring(guidString, 20, 32))"
|
||||
|
||||
return "\(joystickIndex)-\(formattedGUID)"
|
||||
}
|
||||
|
||||
func getConnectedControllers() -> [Controller] {
|
||||
|
||||
|
||||
guard let jsonPtr = get_game_controllers() else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Convert the unmanaged memory (C string) to a Swift String
|
||||
let jsonString = String(cString: jsonPtr)
|
||||
|
||||
var controllers: [Controller] = []
|
||||
|
||||
let numJoysticks = SDL_NumJoysticks()
|
||||
|
||||
for i in 0..<numJoysticks {
|
||||
if let controller = SDL_GameControllerOpen(i) {
|
||||
let guid = generateGamepadId(joystickIndex: i)
|
||||
let name = String(cString: SDL_GameControllerName(controller))
|
||||
|
||||
print("Controller \(i): \(name), GUID: \(guid ?? "")")
|
||||
|
||||
guard let guid else {
|
||||
SDL_GameControllerClose(controller)
|
||||
return []
|
||||
|
||||
// Splitting the string by newline
|
||||
let lines = jsonString.components(separatedBy: "\n")
|
||||
|
||||
// Parsing each line
|
||||
for line in lines {
|
||||
if line.contains(":") {
|
||||
let parts = line.components(separatedBy: ":")
|
||||
if parts.count == 2 {
|
||||
let id = parts[0].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let name = parts[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
controllers.append(Controller(id: id, name: name))
|
||||
}
|
||||
|
||||
controllers.append(Controller(id: guid, name: name))
|
||||
|
||||
SDL_GameControllerClose(controller)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return controllers
|
||||
|
||||
}
|
||||
|
||||
func removeFirmware() {
|
||||
@ -481,66 +312,6 @@ class Ryujinx {
|
||||
print("Error removing folder: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func repeatuntilfindLayer() {
|
||||
Task { @MainActor in
|
||||
while self.metalLayer == nil {
|
||||
let layer = self.getMetalLayer(nil)
|
||||
|
||||
if layer != nil {
|
||||
DispatchQueue.main.async {
|
||||
self.metalLayer = layer
|
||||
}
|
||||
self.metalLayer = layer
|
||||
break
|
||||
}
|
||||
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
try await Task.sleep(nanoseconds: 100_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -550,3 +321,4 @@ class Ryujinx {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -1,86 +0,0 @@
|
||||
//
|
||||
// LaunchGameIntentDef.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 10/02/2025.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Intents
|
||||
import AppIntents
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct LaunchGameIntentDef: AppIntent {
|
||||
|
||||
static let title: LocalizedStringResource = "Launch Game"
|
||||
|
||||
static var description = IntentDescription("Launches the Selected Game.")
|
||||
|
||||
@Parameter(title: "Game", optionsProvider: GameOptionsProvider())
|
||||
var gameName: String
|
||||
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("Launch \(\.$gameName)")
|
||||
}
|
||||
|
||||
static var openAppWhenRun: Bool = true
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
||||
let ryujinx = Ryujinx.shared.games
|
||||
|
||||
let name = findClosestGameName(input: gameName, games: ryujinx.flatMap(\.titleName))
|
||||
|
||||
let urlString = "melonx://game?name=\(name ?? gameName)"
|
||||
print(urlString)
|
||||
if let url = URL(string: urlString) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
|
||||
return .result()
|
||||
}
|
||||
|
||||
func levenshteinDistance(_ a: String, _ b: String) -> Int {
|
||||
let aCount = a.count
|
||||
let bCount = b.count
|
||||
var matrix = [[Int]](repeating: [Int](repeating: 0, count: bCount + 1), count: aCount + 1)
|
||||
|
||||
for i in 0...aCount {
|
||||
matrix[i][0] = i
|
||||
}
|
||||
|
||||
for j in 0...bCount {
|
||||
matrix[0][j] = j
|
||||
}
|
||||
|
||||
for i in 1...aCount {
|
||||
for j in 1...bCount {
|
||||
let cost = a[a.index(a.startIndex, offsetBy: i - 1)] == b[b.index(b.startIndex, offsetBy: j - 1)] ? 0 : 1
|
||||
matrix[i][j] = min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[aCount][bCount]
|
||||
}
|
||||
|
||||
func findClosestGameName(input: String, games: [String]) -> String? {
|
||||
let closestGame = games.min { a, b in
|
||||
let distanceA = levenshteinDistance(input, a)
|
||||
let distanceB = levenshteinDistance(input, b)
|
||||
return distanceA < distanceB
|
||||
}
|
||||
return closestGame
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct GameOptionsProvider: DynamicOptionsProvider {
|
||||
func results() async throws -> [String] {
|
||||
let dynamicGames = Ryujinx.shared.loadGames()
|
||||
|
||||
return dynamicGames.map { $0.titleName }
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// GameInfo.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 9/12/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
public struct Game: Identifiable, Equatable, Hashable {
|
||||
public var id: URL { fileURL }
|
||||
|
||||
var containerFolder: URL
|
||||
var fileType: UTType
|
||||
var fileURL: URL
|
||||
|
||||
var titleName: String
|
||||
var titleId: String
|
||||
var developer: String
|
||||
var version: String
|
||||
var icon: UIImage?
|
||||
|
||||
|
||||
static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game {
|
||||
var gameInfo = gameInfo
|
||||
var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
|
||||
|
||||
gameTemp.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}
|
||||
|
||||
gameTemp.developer = withUnsafePointer(to: &gameInfo.Developer) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}
|
||||
|
||||
gameTemp.titleId = withUnsafePointer(to: &gameInfo.TitleId) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
gameTemp.version = withUnsafePointer(to: &gameInfo.Version) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}
|
||||
|
||||
let imageSize = Int(gameInfo.ImageSize)
|
||||
if imageSize > 0, imageSize <= 1024 * 1024 {
|
||||
let imageData = Data(bytes: gameInfo.ImageData, count: imageSize)
|
||||
|
||||
gameTemp.icon = UIImage(data: imageData)
|
||||
} else {
|
||||
print("Invalid image size.")
|
||||
}
|
||||
return gameTemp
|
||||
}
|
||||
|
||||
func createImage(from gameInfo: GameInfo) -> UIImage? {
|
||||
// Access the struct
|
||||
let gameInfoValue = gameInfo
|
||||
|
||||
// Get the image data
|
||||
let imageSize = Int(gameInfoValue.ImageSize)
|
||||
guard imageSize > 0, imageSize <= 1024 * 1024 else {
|
||||
print("Invalid image size.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert the ImageData byte array to Swift's Data
|
||||
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
|
||||
|
||||
// Create a UIImage (or NSImage on macOS)
|
||||
print(imageData)
|
||||
|
||||
return UIImage(data: imageData)
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import Darwin
|
||||
import UIKit
|
||||
import MetalKit
|
||||
// import SDL
|
||||
import SoftwareKeyboard
|
||||
|
||||
struct MoltenVKSettings: Codable, Hashable {
|
||||
let string: String
|
||||
@ -19,55 +20,45 @@ struct MoltenVKSettings: Codable, Hashable {
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
// Games
|
||||
// MARK: - Properties
|
||||
@State private var theWindow: UIWindow?
|
||||
@State private var game: Game?
|
||||
|
||||
// Controllers
|
||||
@State private var controllersList: [Controller] = []
|
||||
@State private var currentControllers: [Controller] = []
|
||||
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
||||
@State var nativeControllers: [GCController: NativeController] = [:]
|
||||
@State private var isVirtualControllerActive: Bool = false
|
||||
@AppStorage("isVirtualController") var isVCA: Bool = true
|
||||
|
||||
// Settings and Configuration
|
||||
@State private var config: Ryujinx.Configuration
|
||||
@State var settings: [MoltenVKSettings]
|
||||
@AppStorage("useTrollStore") var useTrollStore: Bool = false
|
||||
|
||||
// JIT
|
||||
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
|
||||
|
||||
// Other Configuration
|
||||
@State private var isVirtualControllerActive: Bool = false
|
||||
@AppStorage("isVirtualController") var isVCA: Bool = true
|
||||
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
||||
@AppStorage("JIT") var isJITEnabled: Bool = false
|
||||
@State var isMK8: Bool = false
|
||||
@AppStorage("quit") var quit: Bool = false
|
||||
@State var quits: Bool = false
|
||||
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
|
||||
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true
|
||||
|
||||
// Loading Animation
|
||||
@State var quits: Bool = false
|
||||
@State private var clumpOffset: CGFloat = -100
|
||||
private let clumpWidth: CGFloat = 100
|
||||
private let animationDuration: Double = 1.0
|
||||
@State private var isAnimating = false
|
||||
@State var isLoading = true
|
||||
|
||||
|
||||
// MARK: - Initialization
|
||||
init() {
|
||||
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
||||
_config = State(initialValue: defaultConfig)
|
||||
|
||||
let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings.
|
||||
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
|
||||
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
|
||||
MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
|
||||
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", 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: "128"),
|
||||
let defaultSettings: [MoltenVKSettings] = [
|
||||
// MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"),
|
||||
// MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2"),
|
||||
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "0"),
|
||||
// MoltenVKSettings(string: "MVK_CONFIG_RESUME_LOST_DEVICE", value: "1"),
|
||||
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "0")
|
||||
]
|
||||
|
||||
_settings = State(initialValue: defaultSettings)
|
||||
|
||||
print("JIT Enabled: \(isJITEnabled)")
|
||||
|
||||
initializeSDL()
|
||||
}
|
||||
|
||||
@ -75,80 +66,34 @@ struct ContentView: View {
|
||||
var body: some View {
|
||||
if game != nil, quits == false {
|
||||
if isLoading {
|
||||
if Air.shared.connected {
|
||||
Text("")
|
||||
.onAppear() {
|
||||
Air.play(AnyView(emulationView))
|
||||
emulationView
|
||||
.onAppear() {
|
||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
timer.invalidate()
|
||||
quits = quit
|
||||
|
||||
if quits {
|
||||
quit = false
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ZStack {
|
||||
emulationView
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is when the game starts to stop the animation
|
||||
if #available(iOS 16, *) {
|
||||
EmulationView()
|
||||
.persistentSystemOverlays(.hidden)
|
||||
.onAppear() {
|
||||
isAnimating = false
|
||||
}
|
||||
} else {
|
||||
EmulationView()
|
||||
.onAppear() {
|
||||
isAnimating = false
|
||||
}
|
||||
VStack {
|
||||
|
||||
}
|
||||
.onAppear() {
|
||||
isAnimating = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is the main menu view that includes the Settings and the Game Selector
|
||||
mainMenuView
|
||||
.onAppear() {
|
||||
quits = false
|
||||
|
||||
initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
|
||||
}
|
||||
.onOpenURL() { url in
|
||||
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
components.host == "game" {
|
||||
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
||||
|
||||
game = Ryujinx.shared.games.first(where: { $0.titleId == text })
|
||||
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
||||
game = Ryujinx.shared.games.first(where: { $0.titleName == text })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private func initControllerObservers() {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .GCControllerDidConnect,
|
||||
object: nil,
|
||||
queue: .main) { notification in
|
||||
if let controller = notification.object as? GCController {
|
||||
print("Controller connected: \(controller.productCategory)")
|
||||
nativeControllers[controller] = .init(controller)
|
||||
refreshControllersList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .GCControllerDidDisconnect,
|
||||
object: nil,
|
||||
queue: .main) { notification in
|
||||
if let controller = notification.object as? GCController {
|
||||
print("Controller disconnected: \(controller.productCategory)")
|
||||
nativeControllers[controller]?.cleanup()
|
||||
nativeControllers[controller] = nil
|
||||
refreshControllersList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View Components
|
||||
private var emulationView: some View {
|
||||
@ -175,12 +120,14 @@ struct ContentView: View {
|
||||
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
|
||||
|
||||
ZStack(alignment: .leading) {
|
||||
// Background track
|
||||
Rectangle()
|
||||
.cornerRadius(10)
|
||||
.frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
||||
.foregroundColor(.gray.opacity(0.3))
|
||||
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
|
||||
|
||||
// Animated loading bar
|
||||
Rectangle()
|
||||
.cornerRadius(10)
|
||||
.frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
||||
@ -202,10 +149,7 @@ struct ContentView: View {
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
|
||||
if get_current_fps() != 0 {
|
||||
withAnimation {
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
isAnimating = false
|
||||
timer.invalidate()
|
||||
}
|
||||
@ -230,46 +174,48 @@ struct ContentView: View {
|
||||
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)
|
||||
refreshControllersList()
|
||||
|
||||
|
||||
Text("Select Game")
|
||||
.font(.system(size: 150))
|
||||
.bold()
|
||||
}
|
||||
))
|
||||
|
||||
let isJIT = isJITEnabled()
|
||||
if !isJIT {
|
||||
useTrollStore ? askForJIT() : enableJITEB()
|
||||
}
|
||||
let isJIT = UserDefaults.standard.bool(forKey: "JIT-ENABLED")
|
||||
|
||||
if !isJIT, useTrollStore {
|
||||
askForJIT()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO;
|
||||
private func initializeSDL() {
|
||||
setMoltenVKSettings()
|
||||
SDL_SetMainReady() // Sets SDL Ready
|
||||
SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true
|
||||
SDL_Init(SdlInitFlags) // Initialises SDL2
|
||||
SDL_SetMainReady()
|
||||
SDL_iPhoneSetEventPump(SDL_TRUE)
|
||||
SDL_Init(SdlInitFlags)
|
||||
initialize()
|
||||
}
|
||||
|
||||
private func setupEmulation() {
|
||||
patchMakeKeyAndVisible()
|
||||
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
start(displayid: 1)
|
||||
if (currentControllers.first(where: { $0 == onscreencontroller }) != nil) {
|
||||
|
||||
isVCA = true
|
||||
|
||||
DispatchQueue.main.async {
|
||||
start(displayid: 1)
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
isVCA = false
|
||||
|
||||
DispatchQueue.main.async {
|
||||
start(displayid: 1)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,23 +226,35 @@ struct ContentView: View {
|
||||
self.onscreencontroller = onscreen
|
||||
}
|
||||
|
||||
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
|
||||
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
|
||||
|
||||
currentControllers = []
|
||||
controllersList.removeAll(where: { $0.id == "0"})
|
||||
|
||||
if controllersList.count == 1 {
|
||||
let controller = controllersList[0]
|
||||
if controllersList.count > 2 {
|
||||
let controller = controllersList[2]
|
||||
currentControllers.append(controller)
|
||||
} else if (controllersList.count - 1) >= 1 {
|
||||
for controller in controllersList {
|
||||
if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
|
||||
currentControllers.append(controller)
|
||||
} else if let controller = controllersList.first(where: { $0.id == onscreencontroller.id }), !controllersList.isEmpty {
|
||||
currentControllers.append(controller)
|
||||
}
|
||||
}
|
||||
|
||||
func showAlert(title: String, message: String, showOk: Bool, completion: @escaping (Bool) -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
if let mainWindow = UIApplication.shared.windows.last {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
||||
if showOk {
|
||||
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
||||
completion(true)
|
||||
}
|
||||
|
||||
alert.addAction(okAction)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
|
||||
mainWindow.rootViewController?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func start(displayid: UInt32) {
|
||||
@ -305,15 +263,11 @@ struct ContentView: View {
|
||||
config.gamepath = game.fileURL.path
|
||||
config.inputids = Array(Set(currentControllers.map(\.id)))
|
||||
|
||||
if mVKPreFillBuffer {
|
||||
let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2")
|
||||
if game.titleName.lowercased() == "super mario odyssey" {
|
||||
let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "1")
|
||||
setenv(setting.string, setting.value, 1)
|
||||
}
|
||||
|
||||
if syncqsubmits {
|
||||
let setting = MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "2")
|
||||
setenv(setting.string, setting.value, 1)
|
||||
}
|
||||
|
||||
if config.inputids.isEmpty {
|
||||
config.inputids.append("0")
|
||||
@ -328,8 +282,9 @@ struct ContentView: View {
|
||||
|
||||
|
||||
|
||||
// Sets MoltenVK Environment Variables
|
||||
|
||||
private func setMoltenVKSettings() {
|
||||
|
||||
settings.forEach { setting in
|
||||
setenv(setting.string, setting.value, 1)
|
||||
}
|
||||
@ -351,10 +306,3 @@ func loadSettings() -> Ryujinx.Configuration? {
|
||||
}
|
||||
}
|
||||
|
||||
extension Array {
|
||||
@inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
|
||||
for index in self.indices {
|
||||
try body(&self[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ struct ControllerView: View {
|
||||
DPadView()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
.padding()
|
||||
VStack {
|
||||
ShoulderButtonsViewRight()
|
||||
ZStack {
|
||||
@ -53,6 +53,7 @@ struct ControllerView: View {
|
||||
ABXYView()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
HStack {
|
||||
@ -62,8 +63,8 @@ struct ControllerView: View {
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, geometry.size.height / 3.2) // very broken
|
||||
}
|
||||
|
||||
} else {
|
||||
// could be landscape
|
||||
VStack {
|
||||
@ -99,12 +100,12 @@ struct ControllerView: View {
|
||||
// Spacer()
|
||||
VStack {
|
||||
// Spacer()
|
||||
ButtonView(button: .back) // Adding the - button
|
||||
ButtonView(button: .back) // Adding the + button
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
// Spacer()
|
||||
ButtonView(button: .start) // Adding the + button
|
||||
ButtonView(button: .start) // Adding the - button
|
||||
}
|
||||
// Spacer()
|
||||
}
|
||||
@ -129,8 +130,6 @@ struct ControllerView: View {
|
||||
struct ShoulderButtonsViewLeft: View {
|
||||
@State var width: CGFloat = 160
|
||||
@State var height: CGFloat = 20
|
||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
ButtonView(button: .leftTrigger)
|
||||
@ -144,9 +143,6 @@ struct ShoulderButtonsViewLeft: View {
|
||||
width *= 1.2
|
||||
height *= 1.2
|
||||
}
|
||||
|
||||
width *= CGFloat(controllerScale)
|
||||
height *= CGFloat(controllerScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,8 +150,6 @@ struct ShoulderButtonsViewLeft: View {
|
||||
struct ShoulderButtonsViewRight: View {
|
||||
@State var width: CGFloat = 160
|
||||
@State var height: CGFloat = 20
|
||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
ButtonView(button: .rightShoulder)
|
||||
@ -169,16 +163,12 @@ struct ShoulderButtonsViewRight: View {
|
||||
width *= 1.2
|
||||
height *= 1.2
|
||||
}
|
||||
|
||||
width *= CGFloat(controllerScale)
|
||||
height *= CGFloat(controllerScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DPadView: View {
|
||||
@State var size: CGFloat = 145
|
||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||
var body: some View {
|
||||
VStack {
|
||||
ButtonView(button: .dPadUp)
|
||||
@ -195,16 +185,12 @@ struct DPadView: View {
|
||||
if UIDevice.current.systemName.contains("iPadOS") {
|
||||
size *= 1.2
|
||||
}
|
||||
|
||||
size *= CGFloat(controllerScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ABXYView: View {
|
||||
@State var size: CGFloat = 145
|
||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ButtonView(button: .X)
|
||||
@ -221,8 +207,6 @@ struct ABXYView: View {
|
||||
if UIDevice.current.systemName.contains("iPadOS") {
|
||||
size *= 1.2
|
||||
}
|
||||
|
||||
size *= CGFloat(controllerScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -235,7 +219,6 @@ struct ButtonView: View {
|
||||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||
|
||||
|
||||
|
||||
@ -274,9 +257,6 @@ struct ButtonView: View {
|
||||
width *= 1.2
|
||||
height *= 1.2
|
||||
}
|
||||
|
||||
width *= CGFloat(controllerScale)
|
||||
height *= CGFloat(controllerScale)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,14 +13,11 @@ public struct Joystick: View {
|
||||
@State var iscool: Bool? = nil
|
||||
|
||||
@ObservedObject public var joystickMonitor = JoystickMonitor()
|
||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||
var dragDiameter: CGFloat {
|
||||
var selfs = CGFloat(160)
|
||||
selfs *= controllerScale
|
||||
if UIDevice.current.systemName.contains("iPadOS") {
|
||||
return selfs * 1.2
|
||||
}
|
||||
|
||||
return selfs
|
||||
}
|
||||
private let shape: JoystickShape = .circle
|
||||
|
@ -1,143 +0,0 @@
|
||||
// Credit https://github.com/heestand-xyz/AirKit
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
public class Air {
|
||||
|
||||
static let shared = Air()
|
||||
|
||||
public var connected: Bool = false {
|
||||
didSet {
|
||||
connectionCallbacks.forEach({ $0(connected) })
|
||||
}
|
||||
}
|
||||
var connectionCallbacks: [(Bool) -> ()] = []
|
||||
|
||||
var airScreen: UIScreen?
|
||||
var airWindow: UIWindow?
|
||||
|
||||
var hostingController: UIHostingController<AnyView>?
|
||||
|
||||
var appIsActive: Bool { UIApplication.shared.applicationState == .active }
|
||||
|
||||
init() {
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(didConnect),
|
||||
name: UIScreen.didConnectNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(didDisconnect),
|
||||
name: UIScreen.didDisconnectNotification, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive),
|
||||
name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive),
|
||||
name: UIApplication.willResignActiveNotification, object: nil)
|
||||
}
|
||||
|
||||
private func check() {
|
||||
if let connectedScreen = UIScreen.screens.first(where: { $0 != .main }) {
|
||||
add(screen: connectedScreen) { success in
|
||||
guard success else { return }
|
||||
self.connected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func play(_ view: AnyView) {
|
||||
Air.shared.hostingController = UIHostingController<AnyView>(rootView: view)
|
||||
Air.shared.check()
|
||||
}
|
||||
|
||||
public static func stop() {
|
||||
Air.shared.remove()
|
||||
Air.shared.hostingController = nil
|
||||
}
|
||||
|
||||
public static func connection(_ callback: @escaping (Bool) -> ()) {
|
||||
Air.shared.connectionCallbacks.append(callback)
|
||||
}
|
||||
|
||||
@objc func didConnect(sender: NSNotification) {
|
||||
print("AirKit - Connect")
|
||||
self.connected = true
|
||||
guard let screen: UIScreen = sender.object as? UIScreen else { return }
|
||||
add(screen: screen) { success in
|
||||
guard success else { return }
|
||||
self.connected = true
|
||||
}
|
||||
}
|
||||
|
||||
func add(screen: UIScreen, completion: @escaping (Bool) -> ()) {
|
||||
|
||||
print("AirKit - Add Screen")
|
||||
|
||||
airScreen = screen
|
||||
|
||||
airWindow = UIWindow(frame: airScreen!.bounds)
|
||||
|
||||
guard let viewController: UIViewController = hostingController else {
|
||||
print("AirKit - Add - Failed: Hosting Controller Not Found")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
findWindowScene(for: airScreen!) { windowScene in
|
||||
guard let airWindowScene: UIWindowScene = windowScene else {
|
||||
print("AirKit - Add - Failed: Window Scene Not Found")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
self.airWindow?.rootViewController = viewController
|
||||
self.airWindow?.windowScene = airWindowScene
|
||||
self.airWindow?.isHidden = false
|
||||
print("AirKit - Add Screen - Done")
|
||||
completion(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func findWindowScene(for screen: UIScreen, shouldRecurse: Bool = true, completion: @escaping (UIWindowScene?) -> ()) {
|
||||
print("AirKit - Find Window Scene")
|
||||
var matchingWindowScene: UIWindowScene? = nil
|
||||
let scenes = UIApplication.shared.connectedScenes
|
||||
for scene in scenes {
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
if windowScene.screen == screen {
|
||||
matchingWindowScene = windowScene
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
guard let windowScene: UIWindowScene = matchingWindowScene else {
|
||||
DispatchQueue.main.async {
|
||||
self.findWindowScene(for: screen, shouldRecurse: false) { windowScene in
|
||||
completion(windowScene)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
completion(windowScene)
|
||||
}
|
||||
|
||||
@objc func didDisconnect() {
|
||||
print("AirKit - Disconnect")
|
||||
remove()
|
||||
connected = false
|
||||
}
|
||||
|
||||
func remove() {
|
||||
print("AirKit - Remove")
|
||||
airWindow = nil
|
||||
airScreen = nil
|
||||
}
|
||||
|
||||
@objc func didBecomeActive() {
|
||||
print("AirKit - App Active")
|
||||
}
|
||||
|
||||
@objc func willResignActive() {
|
||||
print("AirKit - App Inactive")
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
public extension View {
|
||||
|
||||
func airPlay() -> some View {
|
||||
print("AirKit - airPlay")
|
||||
Air.play(AnyView(self))
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,70 +0,0 @@
|
||||
//
|
||||
// EmulationView.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 09/02/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// Emulation View
|
||||
struct EmulationView: View {
|
||||
@AppStorage("isVirtualController") var isVCA: Bool = true
|
||||
@AppStorage("showScreenShotButton") var ssb: Bool = false
|
||||
@State var isPresentedThree: Bool = false
|
||||
@State var isAirplaying = Air.shared.connected
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if isAirplaying {
|
||||
Text("")
|
||||
.onAppear {
|
||||
Air.play(AnyView(MetalView().ignoresSafeArea()))
|
||||
}
|
||||
} else {
|
||||
MetalView() // The Emulation View
|
||||
.ignoresSafeArea()
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
|
||||
// Above Emulation View
|
||||
|
||||
if isVCA {
|
||||
ControllerView() // Virtual Controller
|
||||
}
|
||||
|
||||
|
||||
if ssb {
|
||||
Group {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
|
||||
Button {
|
||||
if let screenshot = Ryujinx.shared.emulationUIView.screenshot() {
|
||||
UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.frame(width: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45, height: UIDevice.current.systemName.contains("iPadOS") ? 60 * 1.2 : 45)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Air.shared.connectionCallbacks.append { cool in
|
||||
DispatchQueue.main.async {
|
||||
isAirplaying = cool
|
||||
print(cool)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
//
|
||||
// MetalView.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 09/02/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MetalKit
|
||||
|
||||
struct MetalView: UIViewRepresentable {
|
||||
|
||||
var airplay: Bool = Air.shared.connected // just in case :3
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let metalLayer = Ryujinx.shared.metalLayer!
|
||||
|
||||
var view = UIView()
|
||||
|
||||
metalLayer.frame = view.bounds
|
||||
if airplay {
|
||||
metalLayer.contentsScale = view.contentScaleFactor
|
||||
} else {
|
||||
Ryujinx.shared.emulationUIView.contentScaleFactor = metalLayer.contentsScale // Right size and Fix Touch :3
|
||||
}
|
||||
|
||||
Ryujinx.shared.emulationUIView = view
|
||||
|
||||
if !Ryujinx.shared.emulationUIView.subviews.contains(where: { $0 == metalLayer }) {
|
||||
Ryujinx.shared.emulationUIView.layer.addSublayer(metalLayer)
|
||||
}
|
||||
|
||||
return Ryujinx.shared.emulationUIView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
// nothin
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
//
|
||||
// GameInfoSheet.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Bella on 08/02/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct GameInfoSheet: View {
|
||||
let game: Game
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
iOSNav {
|
||||
List {
|
||||
Section {}
|
||||
header: {
|
||||
VStack(alignment: .center) {
|
||||
if let icon = game.icon {
|
||||
Image(uiImage: icon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 250, height: 250)
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
.contextMenu {
|
||||
Button {
|
||||
UIImageWriteToSavedPhotosAlbum(icon, nil, nil, nil)
|
||||
} label: {
|
||||
Label("Save to Photos", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "questionmark.circle")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 150, height: 150)
|
||||
.padding()
|
||||
}
|
||||
VStack(alignment: .center) {
|
||||
Text("**\(game.titleName)** | \(game.titleId.capitalized)")
|
||||
.multilineTextAlignment(.center)
|
||||
Text(game.developer)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 3)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Text("**Version**")
|
||||
Spacer()
|
||||
Text(game.version)
|
||||
.foregroundStyle(Color.secondary)
|
||||
}
|
||||
HStack {
|
||||
Text("**Title ID**")
|
||||
.contextMenu {
|
||||
Button {
|
||||
UIPasteboard.general.string = game.titleId
|
||||
} label: {
|
||||
Text("Copy Title ID")
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text(game.titleId)
|
||||
.foregroundStyle(Color.secondary)
|
||||
}
|
||||
HStack {
|
||||
Text("**Game Size**")
|
||||
Spacer()
|
||||
Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes")
|
||||
.foregroundStyle(Color.secondary)
|
||||
}
|
||||
HStack {
|
||||
Text("**File Type**")
|
||||
Spacer()
|
||||
Text(getFileType(game.fileURL))
|
||||
.foregroundStyle(Color.secondary)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("**Game URL**")
|
||||
Text(trimGameURL(game.fileURL))
|
||||
.foregroundStyle(Color.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Information")
|
||||
}
|
||||
.headerProminence(.increased)
|
||||
}
|
||||
.navigationTitle(game.titleName)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchFileSize(for gamePath: URL) -> UInt64? {
|
||||
let fileManager = FileManager.default
|
||||
do {
|
||||
let attributes = try fileManager.attributesOfItem(atPath: gamePath.path)
|
||||
if let size = attributes[FileAttributeKey.size] as? UInt64 {
|
||||
return size
|
||||
}
|
||||
} catch {
|
||||
print("Error getting file size: \(error)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func trimGameURL(_ url: URL) -> String {
|
||||
let path = url.path
|
||||
if let range = path.range(of: "/roms/") {
|
||||
return String(path[range.lowerBound...])
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func getFileType(_ url: URL) -> String {
|
||||
url.pathExtension
|
||||
}
|
||||
}
|
@ -8,14 +8,10 @@
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
extension UTType {
|
||||
static let nsp = UTType(exportedAs: "com.nintendo.switch-package")
|
||||
static let xci = UTType(exportedAs: "com.nintendo.switch-cartridge")
|
||||
}
|
||||
|
||||
struct GameLibraryView: View {
|
||||
@Binding var startemu: Game?
|
||||
// @State var importDLCs = false
|
||||
@State private var games: [Game] = []
|
||||
@State private var searchText = ""
|
||||
@State private var isSearching = false
|
||||
@AppStorage("recentGames") private var recentGamesData: Data = Data()
|
||||
@ -25,169 +21,142 @@ struct GameLibraryView: View {
|
||||
@State var firmwareversion = "0"
|
||||
@State var isImporting: Bool = false
|
||||
@State var startgame = false
|
||||
@State var isSelectingGameFile = false
|
||||
@State var isViewingGameInfo: Bool = false
|
||||
@State var isSelectingGameUpdate: Bool = false
|
||||
@State var isSelectingGameDLC: Bool = false
|
||||
@State var gameInfo: Game?
|
||||
var games: Binding<[Game]> {
|
||||
Binding(
|
||||
get: { Ryujinx.shared.games },
|
||||
set: { Ryujinx.shared.games = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
var filteredGames: [Game] {
|
||||
if searchText.isEmpty {
|
||||
return Ryujinx.shared.games.filter { game in
|
||||
!realRecentGames.contains(where: { $0.fileURL == game.fileURL })
|
||||
}
|
||||
return games
|
||||
}
|
||||
return Ryujinx.shared.games.filter {
|
||||
return games.filter {
|
||||
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
|
||||
$0.developer.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
var realRecentGames: [Game] {
|
||||
let games = Ryujinx.shared.games
|
||||
return recentGames.compactMap { recentGame in
|
||||
games.first(where: { $0.fileURL == recentGame.fileURL })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
iOSNav {
|
||||
List {
|
||||
if Ryujinx.shared.games.isEmpty {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "gamecontroller.fill")
|
||||
.font(.system(size: 64))
|
||||
.foregroundColor(.secondary.opacity(0.7))
|
||||
.padding(.top, 60)
|
||||
Text("No Games Found")
|
||||
.font(.title2.bold())
|
||||
.foregroundColor(.primary)
|
||||
Text("Add ROM, Keys and Firmware to get started")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 20) {
|
||||
if !isSearching {
|
||||
Text("Games")
|
||||
.font(.system(size: 34, weight: .bold))
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 12)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 40)
|
||||
} else {
|
||||
if !isSearching && !realRecentGames.isEmpty {
|
||||
Section {
|
||||
ForEach(realRecentGames) { game in
|
||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(role: .destructive) {
|
||||
removeFromRecentGames(game)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
|
||||
if games.isEmpty {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "gamecontroller.fill")
|
||||
.font(.system(size: 64))
|
||||
.foregroundColor(.secondary.opacity(0.7))
|
||||
.padding(.top, 60)
|
||||
Text("No Games Found")
|
||||
.font(.title2.bold())
|
||||
.foregroundColor(.primary)
|
||||
Text("Add ROM, Keys and Firmware to get started")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 40)
|
||||
} else {
|
||||
if !isSearching && !recentGames.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Recent")
|
||||
.font(.title2.bold())
|
||||
.padding(.horizontal)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 16) {
|
||||
ForEach(recentGames) { game in
|
||||
RecentGameCard(game: game, startemu: $startemu)
|
||||
.onTapGesture {
|
||||
addToRecentGames(game)
|
||||
startemu = game
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Recent")
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(filteredGames) { game in
|
||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("All Games")
|
||||
.font(.title2.bold())
|
||||
.padding(.horizontal)
|
||||
|
||||
LazyVStack(spacing: 2) {
|
||||
ForEach(filteredGames) { game in
|
||||
GameListRow(game: game, startemu: $startemu)
|
||||
.onTapGesture {
|
||||
addToRecentGames(game)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyVStack(spacing: 2) {
|
||||
ForEach(filteredGames) { game in
|
||||
GameListRow(game: game, startemu: $startemu)
|
||||
.onTapGesture {
|
||||
addToRecentGames(game)
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Others")
|
||||
}
|
||||
} else {
|
||||
ForEach(filteredGames) { game in
|
||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Games")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.onAppear {
|
||||
loadRecentGames()
|
||||
.onAppear {
|
||||
loadGames()
|
||||
loadRecentGames()
|
||||
|
||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||
firmwareversion = (firmware == "" ? "0" : firmware)
|
||||
}
|
||||
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
do {
|
||||
let fun = url.startAccessingSecurityScopedResource()
|
||||
let path = url.path
|
||||
|
||||
Ryujinx.shared.installFirmware(firmwarePath: path)
|
||||
|
||||
firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
|
||||
if fun {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
|
||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||
firmwareversion = (firmware == "" ? "0" : firmware)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
isSelectingGameFile.toggle()
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Menu {
|
||||
|
||||
Text("Firmware Version: \(firmwareversion)")
|
||||
.tint(.white)
|
||||
|
||||
if firmwareversion == "0" {
|
||||
Button {
|
||||
DispatchQueue.main.async {
|
||||
firmwareInstaller.toggle()
|
||||
}
|
||||
firmwareInstaller.toggle()
|
||||
} label: {
|
||||
Text("Install Firmware")
|
||||
}
|
||||
|
||||
} else {
|
||||
Menu("Firmware") {
|
||||
Button {
|
||||
Ryujinx.shared.removeFirmware()
|
||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||
firmwareversion = (firmware == "" ? "0" : firmware)
|
||||
} label: {
|
||||
Text("Remove Firmware")
|
||||
}
|
||||
Button {
|
||||
Ryujinx.shared.removeFirmware()
|
||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||
firmwareversion = (firmware == "" ? "0" : firmware)
|
||||
} label: {
|
||||
Text("Remove Firmware")
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
|
||||
|
||||
Button {
|
||||
let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
|
||||
|
||||
self.startemu = game
|
||||
} label: {
|
||||
Text("Mii Maker")
|
||||
}
|
||||
Button {
|
||||
DispatchQueue.main.async {
|
||||
isImporting.toggle()
|
||||
}
|
||||
} label: {
|
||||
Text("Open game from system")
|
||||
}
|
||||
self.startemu = game
|
||||
} label: {
|
||||
Text("Mii Maker")
|
||||
}
|
||||
Button {
|
||||
|
||||
isImporting.toggle()
|
||||
} label: {
|
||||
Text("Open game from system")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
var sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
sharedurl = documentsUrl.absoluteString
|
||||
}
|
||||
print(sharedurl)
|
||||
let sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
|
||||
let furl = URL(string: sharedurl)!
|
||||
if UIApplication.shared.canOpenURL(furl) {
|
||||
UIApplication.shared.open(furl, options: [:])
|
||||
@ -199,19 +168,38 @@ struct GameLibraryView: View {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.onChange(of: startemu) { game in
|
||||
guard let game else { return }
|
||||
addToRecentGames(game)
|
||||
}
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.searchable(text: $searchText)
|
||||
.animation(.easeInOut, value: searchText)
|
||||
.onChange(of: searchText) { _ in
|
||||
isSearching = !searchText.isEmpty
|
||||
}
|
||||
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .folder, .nsp, .xci]) { result in
|
||||
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
|
||||
switch result {
|
||||
|
||||
case .success(let url):
|
||||
|
||||
do {
|
||||
|
||||
let fun = url.startAccessingSecurityScopedResource()
|
||||
let path = url.path
|
||||
|
||||
Ryujinx.shared.installFirmware(firmwarePath: path)
|
||||
|
||||
firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
|
||||
if fun {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .data]) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
@ -227,72 +215,50 @@ struct GameLibraryView: View {
|
||||
|
||||
var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
||||
|
||||
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
|
||||
var game = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
|
||||
|
||||
game.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}
|
||||
|
||||
game.developer = withUnsafePointer(to: &gameInfo.Developer) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}
|
||||
|
||||
game.titleId = String(gameInfo.TitleId)
|
||||
|
||||
print(String(gameInfo.TitleId))
|
||||
|
||||
|
||||
game.version = String(gameInfo.Version)
|
||||
|
||||
game.icon = game.createImage(from: gameInfo)
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
startemu = game
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
|
||||
|
||||
case .failure(let err):
|
||||
print("File import failed: \(err.localizedDescription)")
|
||||
}
|
||||
}
|
||||
.fileImporter(isPresented: $isSelectingGameFile, allowedContentTypes: [.nsp, .xci, .zip, .folder]) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
print("Failed to access security-scoped resource")
|
||||
return
|
||||
}
|
||||
defer { url.stopAccessingSecurityScopedResource() }
|
||||
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
|
||||
|
||||
if !fileManager.fileExists(atPath: romsDirectory.path) {
|
||||
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
|
||||
try fileManager.copyItem(at: url, to: destinationURL)
|
||||
|
||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||
} catch {
|
||||
print("Error copying game file: \(error)")
|
||||
}
|
||||
case .failure(let err):
|
||||
print("File import failed: \(err.localizedDescription)")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isSelectingGameUpdate) {
|
||||
UpdateManagerSheet(game: $gameInfo)
|
||||
}
|
||||
.sheet(isPresented: $isSelectingGameDLC) {
|
||||
DLCManagerSheet(game: $gameInfo)
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { isViewingGameInfo && gameInfo != nil },
|
||||
set: { newValue in
|
||||
if !newValue {
|
||||
isViewingGameInfo = false
|
||||
gameInfo = nil
|
||||
}
|
||||
}
|
||||
)) {
|
||||
if let game = gameInfo {
|
||||
GameInfoSheet(game: game)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private func addToRecentGames(_ game: Game) {
|
||||
recentGames.removeAll { $0.titleId == game.titleId }
|
||||
|
||||
recentGames.removeAll { $0.id == game.id }
|
||||
|
||||
recentGames.insert(game, at: 0)
|
||||
|
||||
if recentGames.count > 5 {
|
||||
@ -301,12 +267,7 @@ struct GameLibraryView: View {
|
||||
|
||||
saveRecentGames()
|
||||
}
|
||||
|
||||
private func removeFromRecentGames(_ game: Game) {
|
||||
recentGames.removeAll { $0.titleId == game.titleId }
|
||||
saveRecentGames()
|
||||
}
|
||||
|
||||
|
||||
private func saveRecentGames() {
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
@ -326,21 +287,68 @@ struct GameLibraryView: View {
|
||||
recentGames = []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delete Game Function
|
||||
func deleteGame(game: Game) {
|
||||
|
||||
private func loadGames() {
|
||||
let fileManager = FileManager.default
|
||||
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||
|
||||
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
|
||||
|
||||
// Check if "roms" folder exists; if not, create it
|
||||
if !fileManager.fileExists(atPath: romsDirectory.path) {
|
||||
do {
|
||||
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
print("Failed to create roms directory: \(error)")
|
||||
}
|
||||
}
|
||||
games = []
|
||||
// Load games only from "roms" folder
|
||||
do {
|
||||
try fileManager.removeItem(at: game.fileURL)
|
||||
Ryujinx.shared.games.removeAll { $0.id == game.id }
|
||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
|
||||
|
||||
files.forEach { fileURLCandidate in
|
||||
do {
|
||||
let handle = try FileHandle(forReadingFrom: fileURLCandidate)
|
||||
let fileExtension = (fileURLCandidate.pathExtension as NSString).utf8String
|
||||
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
||||
|
||||
var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
||||
|
||||
var game = Game(containerFolder: romsDirectory, fileType: .item, fileURL: fileURLCandidate, titleName: "", titleId: "", developer: "", version: "")
|
||||
|
||||
game.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}
|
||||
|
||||
game.developer = withUnsafePointer(to: &gameInfo.Developer) {
|
||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}
|
||||
|
||||
game.titleId = String(gameInfo.TitleId)
|
||||
|
||||
|
||||
game.version = String(gameInfo.Version)
|
||||
|
||||
game.icon = game.createImage(from: gameInfo)
|
||||
|
||||
|
||||
games.append(game)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("Error deleting game: \(error)")
|
||||
print("Error loading games from roms folder: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Game Model
|
||||
extension Game: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case titleName, titleId, developer, version, fileURL
|
||||
@ -369,21 +377,57 @@ extension Game: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Game List Item
|
||||
struct RecentGameCard: View {
|
||||
let game: Game
|
||||
@Binding var startemu: Game?
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
startemu = game
|
||||
}) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let icon = game.icon {
|
||||
Image(uiImage: icon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 140, height: 140)
|
||||
.cornerRadius(12)
|
||||
} else {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(colorScheme == .dark ?
|
||||
Color(.systemGray5) : Color(.systemGray6))
|
||||
.frame(width: 140, height: 140)
|
||||
|
||||
Image(systemName: "gamecontroller.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(game.titleName)
|
||||
.font(.subheadline.bold())
|
||||
.lineLimit(1)
|
||||
|
||||
Text(game.developer)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
struct GameListRow: View {
|
||||
let game: Game
|
||||
@Binding var startemu: Game?
|
||||
@Binding var games: [Game] // Add this binding
|
||||
@Binding var isViewingGameInfo: Bool
|
||||
@Binding var isSelectingGameUpdate: Bool
|
||||
@Binding var isSelectingGameDLC: Bool
|
||||
@Binding var gameInfo: Game?
|
||||
@State var gametoDelete: Game?
|
||||
@State var showGameDeleteConfirmation: Bool = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@AppStorage("portal") var gamepo = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
startemu = game
|
||||
@ -400,7 +444,7 @@ struct GameListRow: View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(colorScheme == .dark ?
|
||||
Color(.systemGray5) : Color(.systemGray6))
|
||||
Color(.systemGray5) : Color(.systemGray6))
|
||||
.frame(width: 45, height: 45)
|
||||
|
||||
Image(systemName: "gamecontroller.fill")
|
||||
@ -427,73 +471,23 @@ struct GameListRow: View {
|
||||
.foregroundColor(.accentColor)
|
||||
.opacity(0.8)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
Section {
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(.systemBackground))
|
||||
.contextMenu {
|
||||
Button {
|
||||
startemu = game
|
||||
} label: {
|
||||
Label("Play Now", systemImage: "play.fill")
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
gameInfo = game
|
||||
isViewingGameInfo.toggle()
|
||||
|
||||
if game.titleName.lowercased() == "portal" {
|
||||
gamepo = true
|
||||
} else if game.titleName.lowercased() == "portal 2" {
|
||||
gamepo = true
|
||||
}
|
||||
// Add info action
|
||||
} label: {
|
||||
Label("Game Info", systemImage: "info.circle")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
gameInfo = game
|
||||
isSelectingGameUpdate.toggle()
|
||||
} label: {
|
||||
Label("Game Update Manager", systemImage: "chevron.up.circle")
|
||||
}
|
||||
|
||||
Button {
|
||||
gameInfo = game
|
||||
isSelectingGameDLC.toggle()
|
||||
} label: {
|
||||
Label("Game DLC Manager", systemImage: "plus.viewfinder")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
gametoDelete = game
|
||||
showGameDeleteConfirmation.toggle()
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
|
||||
Button("Delete", role: .destructive) {
|
||||
if let game = gametoDelete {
|
||||
deleteGame(game: game)
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?")
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteGame(game: Game) {
|
||||
let fileManager = FileManager.default
|
||||
do {
|
||||
try fileManager.removeItem(at: game.fileURL)
|
||||
games.removeAll { $0.id == game.id }
|
||||
} catch {
|
||||
print("Error deleting game: \(error)")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
@ -1,100 +0,0 @@
|
||||
//
|
||||
// LogEntry.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 09/02/2025.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LogEntry: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let text: String
|
||||
|
||||
static func == (lhs: LogEntry, rhs: LogEntry) -> Bool {
|
||||
return lhs.id == rhs.id && lhs.text == rhs.text
|
||||
}
|
||||
}
|
||||
|
||||
struct LogViewer: View {
|
||||
@State private var logs: [LogEntry] = []
|
||||
@State private var latestLogFilePath: String?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
ForEach(logs) { log in
|
||||
Text(log.text)
|
||||
.padding(4)
|
||||
.background(Color.black.opacity(0.7))
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
.animation(.easeOut(duration: 2), value: logs)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.onAppear {
|
||||
findNewestLogFile()
|
||||
}
|
||||
}
|
||||
|
||||
func findNewestLogFile() {
|
||||
let logsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("logs")
|
||||
|
||||
guard let directory = logsDirectory else { return }
|
||||
|
||||
do {
|
||||
let logFiles = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.contentModificationDateKey], options: .skipsHiddenFiles)
|
||||
|
||||
// Sort files by modification date (newest first)
|
||||
let sortedFiles = logFiles.sorted {
|
||||
(try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? Date.distantPast >
|
||||
(try? $1.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? Date.distantPast
|
||||
}
|
||||
|
||||
if let newestLogFile = sortedFiles.first {
|
||||
latestLogFilePath = newestLogFile.path
|
||||
startReadingLogFile()
|
||||
}
|
||||
} catch {
|
||||
print("Error reading log files: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func startReadingLogFile() {
|
||||
guard let path = latestLogFilePath else { return }
|
||||
let fileHandle = try? FileHandle(forReadingAtPath: path)
|
||||
fileHandle?.seekToEndOfFile()
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .NSFileHandleDataAvailable, object: fileHandle, queue: .main) { _ in
|
||||
if let data = fileHandle?.availableData, !data.isEmpty {
|
||||
if let logLine = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) {
|
||||
DispatchQueue.main.async {
|
||||
withAnimation {
|
||||
logs.append(LogEntry(text: logLine))
|
||||
}
|
||||
// Remove old logs after a delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
withAnimation {
|
||||
removelogfirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fileHandle?.waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
|
||||
fileHandle?.waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
|
||||
func removelogfirst() {
|
||||
logs.removeFirst()
|
||||
}
|
||||
}
|
@ -18,8 +18,6 @@ struct SettingsView: View {
|
||||
@Binding var onscreencontroller: Controller
|
||||
@AppStorage("useTrollStore") var useTrollStore: Bool = false
|
||||
|
||||
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
|
||||
|
||||
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
||||
|
||||
var memoryManagerModes = [
|
||||
@ -31,71 +29,45 @@ struct SettingsView: View {
|
||||
@AppStorage("RyuDemoControls") var ryuDemo: Bool = false
|
||||
@AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false
|
||||
|
||||
@AppStorage("showScreenShotButton") var ssb: Bool = false
|
||||
|
||||
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = false
|
||||
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
|
||||
|
||||
@AppStorage("performacehud") var performacehud: Bool = false
|
||||
|
||||
@AppStorage("oldWindowCode") var windowCode: Bool = false
|
||||
|
||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||
|
||||
@State private var showResolutionInfo = false
|
||||
@State private var showAnisotropicInfo = false
|
||||
@State private var showControllerInfo = false
|
||||
@State private var searchText = ""
|
||||
@AppStorage("portal") var gamepo = false
|
||||
|
||||
var filteredMemoryModes: [(String, String)] {
|
||||
guard !searchText.isEmpty else { return memoryManagerModes }
|
||||
return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
|
||||
|
||||
|
||||
var body: some View {
|
||||
iOSNav {
|
||||
List {
|
||||
|
||||
|
||||
|
||||
|
||||
// Graphics & Performance
|
||||
Section {
|
||||
Picker(selection: $config.aspectRatio) {
|
||||
ForEach(AspectRatio.allCases, id: \.self) { ratio in
|
||||
Text(ratio.displayName).tag(ratio)
|
||||
}
|
||||
} label: {
|
||||
labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
|
||||
Toggle(isOn: $config.fullscreen) {
|
||||
labelWithIcon("Fullscreen", iconName: "rectangle.expand.vertical")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
|
||||
Toggle(isOn: $config.disableShaderCache) {
|
||||
labelWithIcon("Shader Cache", iconName: "memorychip")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
Toggle(isOn: $config.disablevsync) {
|
||||
labelWithIcon("Disable VSync", iconName: "arrow.triangle.2.circlepath")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
|
||||
|
||||
Toggle(isOn: $config.enableTextureRecompression) {
|
||||
labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
|
||||
Toggle(isOn: $config.disableDockedMode) {
|
||||
labelWithIcon("Docked Mode", iconName: "dock.rectangle")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
Toggle(isOn: $config.macroHLE) {
|
||||
labelWithIcon("Macro HLE", iconName: "gearshape")
|
||||
}.tint(.blue)
|
||||
|
||||
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
|
||||
@ -118,8 +90,8 @@ struct SettingsView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Slider(value: $config.resscale, in: 0.1...3.0, step: 0.05) {
|
||||
|
||||
Slider(value: $config.resscale, in: 0.1...3.0, step: 0.1) {
|
||||
Text("Resolution Scale")
|
||||
} minimumValueLabel: {
|
||||
Text("0.1x")
|
||||
@ -135,46 +107,6 @@ struct SettingsView: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
labelWithIcon("Max Anisotropic Scale", iconName: "magnifyingglass")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Button {
|
||||
showAnisotropicInfo.toggle()
|
||||
} label: {
|
||||
Image(systemName: "info.circle")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Learn more about Max Anisotropic Scale")
|
||||
.alert(isPresented: $showAnisotropicInfo) {
|
||||
Alert(
|
||||
title: Text("Max Anisotripic Scale"),
|
||||
message: Text("Adjust the internal Anisotropic resolution. Higher values improve visuals but may reduce performance. Default at 0 lets game decide."),
|
||||
dismissButton: .default(Text("OK"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Slider(value: $config.maxAnisotropy, in: 0...16.0, step: 0.1) {
|
||||
Text("Resolution Scale")
|
||||
} minimumValueLabel: {
|
||||
Text("0x")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} maximumValueLabel: {
|
||||
Text("16.0x")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Text("\(config.maxAnisotropy, specifier: "%.2f")x")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
|
||||
Toggle(isOn: $performacehud) {
|
||||
labelWithIcon("Performance Overlay", iconName: "speedometer")
|
||||
@ -218,7 +150,7 @@ struct SettingsView: View {
|
||||
|
||||
|
||||
|
||||
ForEach(currentControllers) { controller in
|
||||
ForEach(controllersList) { controller in
|
||||
|
||||
var customBinding: Binding<Bool> {
|
||||
Binding(
|
||||
@ -242,28 +174,15 @@ struct SettingsView: View {
|
||||
.font(.body)
|
||||
}
|
||||
.tint(.blue)
|
||||
.onDrag({ NSItemProvider() })
|
||||
} label: {
|
||||
let controller = String((controllersList.firstIndex(where: { $0.id == controller.id }) ?? 0) + 1)
|
||||
|
||||
if let controller = currentControllers.firstIndex(where: { $0.id == controller.id } ) {
|
||||
Text("Player \(controller + 1)")
|
||||
.onAppear() {
|
||||
// print(currentControllers.firstIndex(where: { $0.id == controller.id }) ?? 0)
|
||||
print(currentControllers.count)
|
||||
|
||||
if currentControllers.count > 2 {
|
||||
print(currentControllers[1])
|
||||
print(currentControllers[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("Player \(controller)")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.onMove { from, to in
|
||||
currentControllers.move(fromOffsets: from, toOffset: to)
|
||||
}
|
||||
} header: {
|
||||
Text("Input Selector")
|
||||
.font(.title3.weight(.semibold))
|
||||
@ -272,59 +191,20 @@ struct SettingsView: View {
|
||||
} footer: {
|
||||
Text("Select input devices and on-screen controls to play with. ")
|
||||
}
|
||||
|
||||
|
||||
// Input Settings
|
||||
Section {
|
||||
Toggle(isOn: $config.macroHLE) {
|
||||
labelWithIcon("Player 1 to Handheld Input", iconName: "formfitting.gamecontroller")
|
||||
}.tint(.blue)
|
||||
|
||||
|
||||
Toggle(isOn: $config.listinputids) {
|
||||
labelWithIcon("List Input IDs", iconName: "list.bullet")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
Toggle(isOn: $ryuDemo) {
|
||||
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
|
||||
}
|
||||
.tint(.blue)
|
||||
.disabled(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
labelWithIcon("On-Screen Controller Scale", iconName: "magnifyingglass")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Button {
|
||||
showControllerInfo.toggle()
|
||||
} label: {
|
||||
Image(systemName: "info.circle")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Learn more about On-Screen Controller Scale")
|
||||
.alert(isPresented: $showControllerInfo) {
|
||||
Alert(
|
||||
title: Text("On-Screen Controller Scale"),
|
||||
message: Text("Adjust the On-Screen Controller size."),
|
||||
dismissButton: .default(Text("OK"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Slider(value: $controllerScale, in: 0.1...3.0, step: 0.05) {
|
||||
Text("Resolution Scale")
|
||||
} minimumValueLabel: {
|
||||
Text("0.1x")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
} maximumValueLabel: {
|
||||
Text("3.0x")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Text("\(controllerScale, specifier: "%.2f")x")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
} header: {
|
||||
Text("Input Settings")
|
||||
.font(.title3.weight(.semibold))
|
||||
@ -334,35 +214,6 @@ struct SettingsView: View {
|
||||
Text("Configure input devices and on-screen controls for easier navigation and play.")
|
||||
}
|
||||
|
||||
// Language and Region Settings
|
||||
Section {
|
||||
Picker(selection: $config.language) {
|
||||
ForEach(SystemLanguage.allCases, id: \.self) { ratio in
|
||||
Text(ratio.displayName).tag(ratio)
|
||||
}
|
||||
} label: {
|
||||
labelWithIcon("Language", iconName: "character.bubble")
|
||||
}
|
||||
|
||||
Picker(selection: $config.regioncode) {
|
||||
ForEach(SystemRegionCode.allCases, id: \.self) { ratio in
|
||||
Text(ratio.displayName).tag(ratio)
|
||||
}
|
||||
} label: {
|
||||
labelWithIcon("Region", iconName: "globe")
|
||||
}
|
||||
|
||||
|
||||
// globe
|
||||
} header: {
|
||||
Text("Language and Region Settings")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
.headerProminence(.increased)
|
||||
} footer: {
|
||||
Text("Configure the System Language and the Region.")
|
||||
}
|
||||
|
||||
// CPU Mode
|
||||
Section {
|
||||
if filteredMemoryModes.isEmpty {
|
||||
@ -378,32 +229,31 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Toggle(isOn: $config.disablePTC) {
|
||||
labelWithIcon("Disable PTC", iconName: "cpu")
|
||||
}.tint(.blue)
|
||||
|
||||
if let cpuInfo = getCPUInfo(), cpuInfo.hasPrefix("Apple M") {
|
||||
if #available (iOS 16.4, *) {
|
||||
if #available (iOS 16.4, *), (false) {
|
||||
Toggle(isOn: .constant(false)) {
|
||||
labelWithIcon("Hypervisor", iconName: "bolt")
|
||||
labelWithIcon("Hypervisor", iconName: "bolt.fill")
|
||||
}
|
||||
.tint(.blue)
|
||||
.disabled(true)
|
||||
.onAppear() {
|
||||
print("CPU Info: \(cpuInfo)")
|
||||
}
|
||||
} else if checkAppEntitlement("com.apple.private.hypervisor") {
|
||||
} else {
|
||||
Toggle(isOn: $config.hypervisor) {
|
||||
labelWithIcon("Hypervisor", iconName: "bolt")
|
||||
labelWithIcon("Hypervisor", iconName: "bolt.fill")
|
||||
}
|
||||
.tint(.blue)
|
||||
.onAppear() {
|
||||
print("CPU Info: \(cpuInfo)")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} header: {
|
||||
Text("CPU")
|
||||
Text("CPU Mode")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
.headerProminence(.increased)
|
||||
@ -411,154 +261,37 @@ struct SettingsView: View {
|
||||
Text("Select how memory is managed. 'Host (fast)' is best for most users.")
|
||||
}
|
||||
|
||||
|
||||
Section {
|
||||
|
||||
|
||||
Toggle(isOn: $config.expandRam) {
|
||||
labelWithIcon("Expand Guest Ram (6GB)", iconName: "exclamationmark.bubble")
|
||||
}
|
||||
.tint(.red)
|
||||
|
||||
Toggle(isOn: $config.ignoreMissingServices) {
|
||||
labelWithIcon("Ignore Missing Services", iconName: "waveform.path")
|
||||
}
|
||||
.tint(.red)
|
||||
} header: {
|
||||
Text("Hacks")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
.headerProminence(.increased)
|
||||
}
|
||||
|
||||
// Other Settings
|
||||
Section {
|
||||
|
||||
Toggle(isOn: $ssb) {
|
||||
labelWithIcon("Screenshot Button", iconName: "square.and.arrow.up")
|
||||
Toggle(isOn: $useTrollStore) {
|
||||
labelWithIcon("TrollStore", iconName: "troll.svg")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
if #available(iOS 17.0.1, *) {
|
||||
Toggle(isOn: $jitStreamerEB) {
|
||||
labelWithIcon("JitStreamer EB", iconName: "bolt.heart")
|
||||
}
|
||||
.tint(.blue)
|
||||
.contextMenu {
|
||||
Button {
|
||||
if let mainWindow = UIApplication.shared.windows.last {
|
||||
let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best iOS developers of all time jkcoxson <3", preferredStyle: .alert)
|
||||
|
||||
let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
|
||||
UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
|
||||
}
|
||||
alertController.addAction(learnMoreButton)
|
||||
|
||||
let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil)
|
||||
alertController.addAction(doneButton)
|
||||
|
||||
mainWindow.rootViewController?.present(alertController, animated: true)
|
||||
}
|
||||
} label: {
|
||||
Text("About")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toggle(isOn: $useTrollStore) {
|
||||
labelWithIcon("TrollStore JIT", iconName: "troll.svg")
|
||||
}
|
||||
.tint(.blue)
|
||||
Toggle(isOn: $config.debuglogs) {
|
||||
labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble")
|
||||
}
|
||||
|
||||
Toggle(isOn: $syncqsubmits) {
|
||||
labelWithIcon("MVK: Synchronous Queue Submits", iconName: "line.diagonal")
|
||||
}.tint(.blue)
|
||||
.contextMenu() {
|
||||
Button {
|
||||
if let mainWindow = UIApplication.shared.windows.last {
|
||||
let alertController = UIAlertController(title: "About MVK: Synchronous Queue Submits", message: "Enable this option if Mario Kart 8 is crashing at Grand Prix mode.", preferredStyle: .alert)
|
||||
|
||||
let doneButton = UIAlertAction(title: "OK", style: .cancel, handler: nil)
|
||||
alertController.addAction(doneButton)
|
||||
|
||||
mainWindow.rootViewController?.present(alertController, animated: true)
|
||||
}
|
||||
} label: {
|
||||
Text("About")
|
||||
}
|
||||
}
|
||||
|
||||
DisclosureGroup {
|
||||
Toggle(isOn: $config.debuglogs) {
|
||||
labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
Toggle(isOn: $config.tracelogs) {
|
||||
labelWithIcon("Trace Logs", iconName: "waveform.path")
|
||||
}
|
||||
.tint(.blue)
|
||||
} label: {
|
||||
Text("Logs")
|
||||
.tint(.blue)
|
||||
|
||||
Toggle(isOn: $config.tracelogs) {
|
||||
labelWithIcon("Trace Logs", iconName: "waveform.path")
|
||||
}
|
||||
|
||||
.tint(.blue)
|
||||
} header: {
|
||||
Text("Miscellaneous Options")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
.headerProminence(.increased)
|
||||
} footer: {
|
||||
Text("Enable trace and debug logs for advanced troubleshooting (Note: This degrades performance),\nEnable Screenshot Button for better screenshots\nand Enable TrollStore for automatic TrollStore JIT.")
|
||||
Text("Enable logs for troubleshooting and Enable automatic TrollStore JIT.")
|
||||
}
|
||||
|
||||
// Info
|
||||
Section {
|
||||
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
||||
let model = getDeviceModel()
|
||||
let deviceType = model.hasPrefix("iPad") ? "iPadOS" :
|
||||
model.hasPrefix("iPhone") ? "iOS" :
|
||||
"macOS"
|
||||
|
||||
let iconName = model.hasPrefix("iPad") ? "ipad.landscape" :
|
||||
model.hasPrefix("iPhone") ? "iphone" :
|
||||
"macwindow"
|
||||
|
||||
labelWithIcon("JIT Acquisition: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
|
||||
|
||||
labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
|
||||
|
||||
labelWithIcon("Device: \(getDeviceModel())", iconName: iconName)
|
||||
|
||||
labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill")
|
||||
|
||||
labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo")
|
||||
|
||||
} header: {
|
||||
Text("Information")
|
||||
.font(.title3.weight(.semibold))
|
||||
.textCase(nil)
|
||||
.headerProminence(.increased)
|
||||
} footer: {
|
||||
Text("Shows info about Memory, Entitlement and JIT.")
|
||||
}
|
||||
|
||||
|
||||
// Advanced
|
||||
Section {
|
||||
Toggle(isOn: $windowCode) {
|
||||
labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
DisclosureGroup {
|
||||
|
||||
Toggle(isOn: $mVKPreFillBuffer) {
|
||||
labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape")
|
||||
}.tint(.blue)
|
||||
|
||||
Toggle(isOn: $config.dfsIntegrityChecks) {
|
||||
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
|
||||
}.tint(.blue)
|
||||
|
||||
|
||||
HStack {
|
||||
labelWithIcon("Page Size", iconName: "textformat.size")
|
||||
Spacer()
|
||||
@ -566,7 +299,7 @@ struct SettingsView: View {
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
}
|
||||
|
||||
|
||||
TextField("Additional Arguments", text: Binding(
|
||||
get: {
|
||||
config.additionalArgs.joined(separator: " ")
|
||||
@ -588,8 +321,6 @@ struct SettingsView: View {
|
||||
Text("Remove Firmware")
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
|
||||
} label: {
|
||||
Text("Advanced Options")
|
||||
}
|
||||
@ -599,11 +330,7 @@ struct SettingsView: View {
|
||||
.textCase(nil)
|
||||
.headerProminence(.increased)
|
||||
} footer: {
|
||||
if #available(iOS 17, *) {
|
||||
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" : "")")
|
||||
} else {
|
||||
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). If the emulation is not showing (you may hear audio in some games), try enabling \"SDL Window\" \n \n\(gamepo ? "the cake is a lie" : "")")
|
||||
}
|
||||
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)")
|
||||
}
|
||||
|
||||
}
|
||||
@ -631,15 +358,13 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func getDeviceModel() -> String {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||
let identifier = machineMirror.children.reduce("") { identifier, element in
|
||||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||
}
|
||||
return identifier
|
||||
func getCPUInfo() -> String? {
|
||||
let device = MTLCreateSystemDefaultDevice()
|
||||
|
||||
let gpu = device?.name
|
||||
print("GPU: " + (gpu ?? ""))
|
||||
print(config.hypervisor)
|
||||
return gpu
|
||||
}
|
||||
|
||||
|
||||
@ -660,15 +385,6 @@ struct SettingsView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
func getCPUInfo() -> String? {
|
||||
let device = MTLCreateSystemDefaultDevice()
|
||||
|
||||
let gpu = device?.name
|
||||
print("GPU: " + (gpu ?? ""))
|
||||
return gpu
|
||||
}
|
||||
|
||||
|
||||
// Original loadSettings function assumed to exist
|
||||
func loadSettings() -> Ryujinx.Configuration? {
|
||||
|
||||
@ -718,6 +434,8 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct SVGView: UIViewRepresentable {
|
||||
var svgName: String
|
||||
var color: Color = Color.black
|
||||
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// RyujinxKeyboard.h
|
||||
// RyujinxKeyboard
|
||||
//
|
||||
// Created by Stossy11 on 11/02/2025.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for RyujinxKeyboard.
|
||||
FOUNDATION_EXPORT double RyujinxKeyboardVersionNumber;
|
||||
|
||||
//! Project version string for RyujinxKeyboard.
|
||||
FOUNDATION_EXPORT const unsigned char RyujinxKeyboardVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <RyujinxKeyboard/PublicHeader.h>
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
framework module RyujinxKeyboard {
|
||||
umbrella header "RyujinxKeyboard.h"
|
||||
export *
|
||||
|
||||
module * { export * }
|
||||
}
|
Binary file not shown.
@ -1,124 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>files</key>
|
||||
<dict>
|
||||
<key>Headers/RyujinxKeyboard.h</key>
|
||||
<data>
|
||||
5P7GN4g050n199pV6/+SpfMBgJc=
|
||||
</data>
|
||||
<key>Info.plist</key>
|
||||
<data>
|
||||
hYdI/ktAKwjBSfaJpt6Yc8UKLCY=
|
||||
</data>
|
||||
<key>Modules/module.modulemap</key>
|
||||
<data>
|
||||
0kFAMoTn+4Q1J/dM6uMLe3EhbL0=
|
||||
</data>
|
||||
</dict>
|
||||
<key>files2</key>
|
||||
<dict>
|
||||
<key>Headers/RyujinxKeyboard.h</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
/yGmHq9NdBF/ruesISIj7vml0ySgoJkrFOcrw0vaIxQ=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Modules/module.modulemap</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
K+ZyxKhTI4bMVZuHBIspvd2PFqvCOlVUFYmwF96O5NQ=
|
||||
</data>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>rules</key>
|
||||
<dict>
|
||||
<key>^.*</key>
|
||||
<true/>
|
||||
<key>^.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^version.plist$</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>rules2</key>
|
||||
<dict>
|
||||
<key>.*\.dSYM($|/)</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>11</real>
|
||||
</dict>
|
||||
<key>^(.*/)?\.DS_Store$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>2000</real>
|
||||
</dict>
|
||||
<key>^.*</key>
|
||||
<true/>
|
||||
<key>^.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^Info\.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^PkgInfo$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^embedded\.provisionprofile$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^version\.plist$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,18 @@
|
||||
//
|
||||
// SoftwareKeyboard.h
|
||||
// SoftwareKeyboard
|
||||
//
|
||||
// Created by Stossy11 on 19/12/2024.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for SoftwareKeyboard.
|
||||
FOUNDATION_EXPORT double SoftwareKeyboardVersionNumber;
|
||||
|
||||
//! Project version string for SoftwareKeyboard.
|
||||
FOUNDATION_EXPORT const unsigned char SoftwareKeyboardVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <SoftwareKeyboard/PublicHeader.h>
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@
|
||||
// swift-interface-format-version: 1.0
|
||||
// swift-compiler-version: Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.4 clang-1600.0.30)
|
||||
// swift-module-flags: -target arm64-apple-ios14.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -enable-experimental-feature OpaqueTypeErasure -enable-bare-slash-regex -module-name SoftwareKeyboard
|
||||
@_exported import SoftwareKeyboard
|
||||
import Swift
|
||||
import UIKit
|
||||
import _Concurrency
|
||||
import _StringProcessing
|
||||
import _SwiftConcurrencyShims
|
||||
@objc public enum KeyboardMode : Swift.UInt32 {
|
||||
case `default` = 0
|
||||
case numeric = 1
|
||||
case ascii = 2
|
||||
case fullLatin = 3
|
||||
case alphabet = 4
|
||||
case simplifiedChinese = 5
|
||||
case traditionalChinese = 6
|
||||
case korean = 7
|
||||
case languageSet2 = 8
|
||||
case languageSet2Latin = 9
|
||||
public init?(rawValue: Swift.UInt32)
|
||||
public typealias RawValue = Swift.UInt32
|
||||
public var rawValue: Swift.UInt32 {
|
||||
get
|
||||
}
|
||||
}
|
||||
public struct SoftwareKeyboardUiArgs {
|
||||
public var keyboardMode: SoftwareKeyboard.KeyboardMode
|
||||
public var headerText: Swift.String
|
||||
public var subtitleText: Swift.String
|
||||
public var submitText: Swift.String
|
||||
public var stringLengthMin: Swift.Int32
|
||||
public var stringLengthMax: Swift.Int32
|
||||
public var initialText: Swift.String?
|
||||
}
|
||||
extension SoftwareKeyboard.KeyboardMode : Swift.Equatable {}
|
||||
extension SoftwareKeyboard.KeyboardMode : Swift.Hashable {}
|
||||
extension SoftwareKeyboard.KeyboardMode : Swift.RawRepresentable {}
|
Binary file not shown.
@ -0,0 +1,38 @@
|
||||
// swift-interface-format-version: 1.0
|
||||
// swift-compiler-version: Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.4 clang-1600.0.30)
|
||||
// swift-module-flags: -target arm64-apple-ios14.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -Onone -enable-experimental-feature OpaqueTypeErasure -enable-bare-slash-regex -module-name SoftwareKeyboard
|
||||
@_exported import SoftwareKeyboard
|
||||
import Swift
|
||||
import UIKit
|
||||
import _Concurrency
|
||||
import _StringProcessing
|
||||
import _SwiftConcurrencyShims
|
||||
@objc public enum KeyboardMode : Swift.UInt32 {
|
||||
case `default` = 0
|
||||
case numeric = 1
|
||||
case ascii = 2
|
||||
case fullLatin = 3
|
||||
case alphabet = 4
|
||||
case simplifiedChinese = 5
|
||||
case traditionalChinese = 6
|
||||
case korean = 7
|
||||
case languageSet2 = 8
|
||||
case languageSet2Latin = 9
|
||||
public init?(rawValue: Swift.UInt32)
|
||||
public typealias RawValue = Swift.UInt32
|
||||
public var rawValue: Swift.UInt32 {
|
||||
get
|
||||
}
|
||||
}
|
||||
public struct SoftwareKeyboardUiArgs {
|
||||
public var keyboardMode: SoftwareKeyboard.KeyboardMode
|
||||
public var headerText: Swift.String
|
||||
public var subtitleText: Swift.String
|
||||
public var submitText: Swift.String
|
||||
public var stringLengthMin: Swift.Int32
|
||||
public var stringLengthMax: Swift.Int32
|
||||
public var initialText: Swift.String?
|
||||
}
|
||||
extension SoftwareKeyboard.KeyboardMode : Swift.Equatable {}
|
||||
extension SoftwareKeyboard.KeyboardMode : Swift.Hashable {}
|
||||
extension SoftwareKeyboard.KeyboardMode : Swift.RawRepresentable {}
|
Binary file not shown.
@ -0,0 +1,6 @@
|
||||
framework module SoftwareKeyboard {
|
||||
umbrella header "SoftwareKeyboard.h"
|
||||
export *
|
||||
|
||||
module * { export * }
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,89 +2,9 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.stossy11.MeloNX</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>melonx</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GCSupportedGameControllers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>ProfileName</key>
|
||||
<string>ExtendedGamepad</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>ProfileName</key>
|
||||
<string>MicroGamepad</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>melonx</string>
|
||||
</array>
|
||||
<key>MeloID</key>
|
||||
<string>83f67a0a96bd8628a150d7853e360db5bae64e7769524fae399c4b8e7e6aff17</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>LaunchGameIntent</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<string>1d0e26921bac938456ee7210ff4f2fa701dc16c02de1760e0aa757db28818ec7</string>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.archive</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Nintendo Switch Package</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.nintendo.switch-package</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>nsp</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/x-nsp</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
<string>public.archive</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Nintendo Switch Cartridge</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.nintendo.switch-cartridge</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>xci</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/x-xci</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
@ -9,91 +9,48 @@ import SwiftUI
|
||||
import UIKit
|
||||
import CryptoKit
|
||||
|
||||
|
||||
|
||||
@main
|
||||
struct MeloNXApp: App {
|
||||
|
||||
@State var showed = false
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State var alert: UIAlertController? = nil
|
||||
@AppStorage("showeddrmcheck") var showed = true
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ZStack {
|
||||
if showed || DRM != 1 {
|
||||
ContentView()
|
||||
init() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
// drmcheck()
|
||||
InitializeRyujinx() { bool in
|
||||
if bool {
|
||||
print("Ryujinx Files Initialized Successfully")
|
||||
} else {
|
||||
Group {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Text("Loading...")
|
||||
ProgressView()
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Text(UIDevice.current.identifierForVendor?.uuidString ?? "")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
initR()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.black.opacity(1))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initR() {
|
||||
if DRM == 1 {
|
||||
DispatchQueue.main.async { [self] in
|
||||
// drmcheck()
|
||||
InitializeRyujinx() { bool in
|
||||
if bool {
|
||||
print("Ryujinx Files Initialized Successfully")
|
||||
DispatchQueue.main.async { [self] in
|
||||
withAnimation {
|
||||
showed = true
|
||||
}
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
InitializeRyujinx() { bool in
|
||||
if !bool, (scenePhase != .background || scenePhase == .inactive) {
|
||||
withAnimation {
|
||||
showed = false
|
||||
}
|
||||
if !(alert?.isViewLoaded ?? false) {
|
||||
alert = showDMCAAlert()
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
alert?.dismiss(animated: true)
|
||||
showed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
showDMCAAlert()
|
||||
}
|
||||
|
||||
exit(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
InitializeRyujinx() { bool in
|
||||
if !bool {
|
||||
exit(0)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func showAlert() -> UIAlertController? {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
if showed {
|
||||
ContentView()
|
||||
} else {
|
||||
HStack {
|
||||
Text("Loading...")
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showAlert() {
|
||||
// Create the alert controller
|
||||
if let mainWindow = UIApplication.shared.windows.last {
|
||||
let alertController = UIAlertController(title: "Enter license", message: "Enter license key:", preferredStyle: .alert)
|
||||
@ -126,30 +83,12 @@ struct MeloNXApp: App {
|
||||
|
||||
// Present the alert
|
||||
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
|
||||
|
||||
return alertController
|
||||
} else {
|
||||
return nil
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func showDMCAAlert() -> UIAlertController? {
|
||||
if let mainWindow = UIApplication.shared.windows.first {
|
||||
let alertController = UIAlertController(title: "Unauthorized Copy Notice", message: "This app was illegally leaked. Please report the download on the MeloNX Discord. In the meantime, check out Pomelo! \n -Stossy11", preferredStyle: .alert)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
return alertController
|
||||
} else {
|
||||
// uhoh
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func drmcheck(completion: @escaping (Bool) -> Void) {
|
||||
@ -193,47 +132,21 @@ func drmcheck(completion: @escaping (Bool) -> Void) {
|
||||
*/
|
||||
|
||||
func InitializeRyujinx(completion: @escaping (Bool) -> Void) {
|
||||
let path = "aHR0cHM6Ly9teC5zdG9zc3kxMS5jb20v"
|
||||
let path = "aHR0cHM6Ly9zdG9zc3kxMS5jb20vd293LnR4dA=="
|
||||
|
||||
guard let value = Bundle.main.object(forInfoDictionaryKey: "MeloID") as? String, !value.isEmpty else {
|
||||
completion(false)
|
||||
return
|
||||
exit(0)
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (detectRoms(path: path) != value) {
|
||||
completion(false)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
|
||||
configuration.urlCache = nil
|
||||
|
||||
let session = URLSession(configuration: configuration)
|
||||
|
||||
guard let url = URL(string: addFolders(path)!) else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
let task = session.dataTask(with: url) { data, response, error in
|
||||
if error != nil {
|
||||
completion(false)
|
||||
}
|
||||
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
if httpResponse.statusCode == 200 {
|
||||
completion(true)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
let task = URLSession.shared.dataTask(with: URL(string: addFolders(path)!)!) { data, _, _ in
|
||||
let text = String(data: data ?? Data(), encoding: .utf8) ?? ""
|
||||
completion(text.contains("true"))
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
@ -249,15 +162,8 @@ func detectRoms(path string: String) -> String {
|
||||
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 + "/"
|
||||
let decodedString = String(data: data, encoding: .utf8) {
|
||||
return decodedString
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
func print() {
|
||||
Swift.print(self)
|
||||
}
|
||||
}
|
||||
|
45
src/MeloNX/MeloNX/Models/Game.swift
Normal file
45
src/MeloNX/MeloNX/Models/Game.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// GameInfo.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 9/12/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
public struct Game: Identifiable, Equatable {
|
||||
public var id = UUID()
|
||||
|
||||
var containerFolder: URL
|
||||
var fileType: UTType
|
||||
|
||||
var fileURL: URL
|
||||
|
||||
var titleName: String
|
||||
var titleId: String
|
||||
var developer: String
|
||||
var version: String
|
||||
var icon: UIImage?
|
||||
|
||||
func createImage(from gameInfo: GameInfo) -> UIImage? {
|
||||
// Access the struct
|
||||
let gameInfoValue = gameInfo
|
||||
|
||||
// Get the image data
|
||||
let imageSize = Int(gameInfoValue.ImageSize)
|
||||
guard imageSize > 0, imageSize <= 1024 * 1024 else {
|
||||
print("Invalid image size.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert the ImageData byte array to Swift's Data
|
||||
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
|
||||
|
||||
// Create a UIImage (or NSImage on macOS)
|
||||
|
||||
print(imageData)
|
||||
|
||||
return UIImage(data: imageData)
|
||||
}
|
||||
}
|
11
src/MeloNX/dotnet.xcconfig.example
Normal file
11
src/MeloNX/dotnet.xcconfig.example
Normal file
@ -0,0 +1,11 @@
|
||||
//
|
||||
// dotnet.xcconfig
|
||||
// MeloNX
|
||||
//
|
||||
// Created by June P on 12/25/24.
|
||||
//
|
||||
|
||||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
DOTNET_PATH = $(HOME)/.dotnet/dotnet
|
@ -98,10 +98,43 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
return okPressed;
|
||||
}
|
||||
|
||||
public void DisplayInputDialog(SoftwareKeyboardUiArgs args, Action<string> onTextEntered)
|
||||
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
|
||||
{
|
||||
onTextEntered?.Invoke("MeloNX");
|
||||
return;
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool okPressed = false;
|
||||
bool error = false;
|
||||
string inputText = args.InitialText ?? "";
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.SoftwareKeyboard], args);
|
||||
|
||||
if (response.Result == UserResult.Ok)
|
||||
{
|
||||
inputText = response.Input;
|
||||
okPressed = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = true;
|
||||
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogSoftwareKeyboardErrorExceptionMessage, ex));
|
||||
}
|
||||
finally
|
||||
{
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
userText = error ? null : inputText;
|
||||
|
||||
return error || okPressed;
|
||||
}
|
||||
|
||||
public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
|
||||
|
@ -10,7 +10,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
|
||||
class NoWxCache : IDisposable
|
||||
{
|
||||
private const int CodeAlignment = 4; // Bytes.
|
||||
private const int SharedCacheSize = 192 * 1024 * 1024;
|
||||
private const int SharedCacheSize = 2047 * 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
|
||||
|
@ -26,16 +26,10 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native
|
||||
{
|
||||
return $"lib{libraryName}.so.{version}";
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) // TODO: ffmpeg on ios
|
||||
{
|
||||
return $"lib{libraryName}.{version}.dylib";
|
||||
}
|
||||
else if (OperatingSystem.IsIOS())
|
||||
{
|
||||
string libName = $"lib{libraryName}.{version}.dylib";
|
||||
Console.WriteLine($"[iOS] Required firmware library: {libName}");
|
||||
return libName;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException($"Unsupported OS for FFmpeg: {RuntimeInformation.RuntimeIdentifier}");
|
||||
|
@ -6,7 +6,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
{
|
||||
class DescriptorSetManager : IDisposable
|
||||
{
|
||||
public const uint MaxSets = 32;
|
||||
public const uint MaxSets = 16;
|
||||
|
||||
public class DescriptorPoolHolder : IDisposable
|
||||
{
|
||||
|
@ -580,7 +580,6 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
{
|
||||
texture.Sampler = _dummySampler.GetSampler().Get(cbs).Value;
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsIOS()) {
|
||||
Span<DescriptorImageInfo> singleTexture = textures.Slice(i, 1);
|
||||
dsc.UpdateImages(0, binding + i, singleTexture, DescriptorType.CombinedImageSampler);
|
||||
|
@ -9,10 +9,10 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
|
||||
[SupportedOSPlatform("ios")]
|
||||
public static partial class MVKInitialization
|
||||
{
|
||||
[LibraryImport("libMoltenVK.dylib")]
|
||||
[LibraryImport("MoltenVK.framework/MoltenVK")]
|
||||
private static partial Result vkGetMoltenVKConfigurationMVK(IntPtr unusedInstance, out MVKConfiguration config, in IntPtr configSize);
|
||||
|
||||
[LibraryImport("libMoltenVK.dylib")]
|
||||
[LibraryImport("MoltenVK.framework/MoltenVK")]
|
||||
private static partial Result vkSetMoltenVKConfigurationMVK(IntPtr unusedInstance, in MVKConfiguration config, in IntPtr configSize);
|
||||
|
||||
public static void Initialize()
|
||||
@ -24,8 +24,7 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
|
||||
config.UseMetalArgumentBuffers = true;
|
||||
|
||||
config.SemaphoreSupportStyle = MVKVkSemaphoreSupportStyle.MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_SINGLE_QUEUE;
|
||||
|
||||
config.MaxActiveMetalCommandBuffersPerQueue = 1024;
|
||||
config.SynchronousQueueSubmits = false;
|
||||
|
||||
config.ResumeLostDevice = true;
|
||||
|
||||
|
@ -1255,7 +1255,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
int vbSize = vertexBuffer.Buffer.Size;
|
||||
|
||||
if ((Gd.Vendor == Vendor.Amd || !OperatingSystem.IsIOSVersionAtLeast(17)) && !Gd.IsMoltenVk && vertexBuffer.Stride > 0)
|
||||
if (Gd.Vendor == Vendor.Amd && !Gd.IsMoltenVk && vertexBuffer.Stride > 0)
|
||||
{
|
||||
// AMD has a bug where if offset + stride * count is greater than
|
||||
// the size, then the last attribute will have the wrong value.
|
||||
|
@ -601,7 +601,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
if (supportsExtDynamicState)
|
||||
{
|
||||
// dynamicStates[8] = DynamicState.VertexInputBindingStrideExt;
|
||||
dynamicStates[8] = DynamicState.VertexInputBindingStrideExt;
|
||||
}
|
||||
|
||||
var pipelineDynamicStateCreateInfo = new PipelineDynamicStateCreateInfo
|
||||
|
@ -14,8 +14,6 @@ using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Applets
|
||||
{
|
||||
@ -53,10 +51,10 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||
|
||||
private byte[] _transferMemory;
|
||||
|
||||
public string _textValue = "";
|
||||
private string _textValue = "";
|
||||
private int _cursorBegin = 0;
|
||||
private Encoding _encoding = Encoding.Unicode;
|
||||
public KeyboardResult _lastResult = KeyboardResult.NotSet;
|
||||
private KeyboardResult _lastResult = KeyboardResult.NotSet;
|
||||
|
||||
private IDynamicTextInputHandler _dynamicTextInputHandler = null;
|
||||
private SoftwareKeyboardRenderer _keyboardRenderer = null;
|
||||
@ -182,6 +180,9 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||
return _keyboardRenderer?.DrawTo(destination, position) ?? false;
|
||||
}
|
||||
|
||||
[DllImport("SoftwareKeyboard.framework/SoftwareKeyboard", EntryPoint = "displayInputDialog", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void DisplayInputDialog(ref SoftwareKeyboardUiArgs args, out IntPtr userInput);
|
||||
|
||||
|
||||
private void ExecuteForegroundKeyboard()
|
||||
{
|
||||
@ -222,8 +223,26 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||
InitialText = initialText,
|
||||
};
|
||||
|
||||
_textValue = DefaultInputText;
|
||||
_lastResult = KeyboardResult.Cancel;
|
||||
IntPtr userInputPtr;
|
||||
|
||||
DisplayInputDialog(ref args, out userInputPtr);
|
||||
if (userInputPtr != IntPtr.Zero)
|
||||
{
|
||||
// Convert the IntPtr to a string
|
||||
string userInput = Marshal.PtrToStringAnsi(userInputPtr);
|
||||
|
||||
_textValue = userInput ?? DefaultInputText;
|
||||
_lastResult = KeyboardResult.Accept;
|
||||
|
||||
Console.WriteLine($"User input: {userInput}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("No input was received or input was canceled.");
|
||||
|
||||
_textValue = DefaultInputText;
|
||||
_lastResult = KeyboardResult.Cancel;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -240,40 +259,37 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||
StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
|
||||
InitialText = initialText,
|
||||
};
|
||||
_device.UiHandler.DisplayInputDialog(args, inputText =>
|
||||
{
|
||||
Console.WriteLine($"User entered: {inputText}");
|
||||
|
||||
_textValue = inputText ?? initialText ?? DefaultInputText;
|
||||
_lastResult = !string.IsNullOrEmpty(inputText) ? KeyboardResult.Accept : KeyboardResult.Cancel;
|
||||
|
||||
while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin)
|
||||
{
|
||||
_textValue = string.Join(" ", _textValue, _textValue);
|
||||
}
|
||||
_lastResult = _device.UiHandler.DisplayInputDialog(args, out _textValue) ? KeyboardResult.Accept : KeyboardResult.Cancel;
|
||||
_textValue ??= initialText ?? DefaultInputText;
|
||||
}
|
||||
|
||||
// Truncate the text if it exceeds the maximum length
|
||||
if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax)
|
||||
{
|
||||
_textValue = _textValue[.._keyboardForegroundConfig.StringLengthMax];
|
||||
}
|
||||
// Ensure the text meets the minimum length requirement
|
||||
while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin)
|
||||
{
|
||||
_textValue = string.Join(" ", _textValue, _textValue);
|
||||
}
|
||||
|
||||
// Handle text validation if required
|
||||
if (_keyboardForegroundConfig.CheckText)
|
||||
{
|
||||
// Submit text for validation
|
||||
_foregroundState = SoftwareKeyboardState.ValidationPending;
|
||||
PushForegroundResponse(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Submit text as complete
|
||||
_foregroundState = SoftwareKeyboardState.Complete;
|
||||
PushForegroundResponse(false);
|
||||
// Truncate the text if it exceeds the maximum length
|
||||
if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax)
|
||||
{
|
||||
_textValue = _textValue[.._keyboardForegroundConfig.StringLengthMax];
|
||||
}
|
||||
|
||||
AppletStateChanged?.Invoke(this, null);
|
||||
}
|
||||
});
|
||||
// Handle text validation if required
|
||||
if (_keyboardForegroundConfig.CheckText)
|
||||
{
|
||||
// Submit text for validation
|
||||
_foregroundState = SoftwareKeyboardState.ValidationPending;
|
||||
PushForegroundResponse(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Submit text as complete
|
||||
_foregroundState = SoftwareKeyboardState.Complete;
|
||||
PushForegroundResponse(false);
|
||||
|
||||
AppletStateChanged?.Invoke(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Cpu;
|
||||
using Ryujinx.Cpu.AppleHv;
|
||||
@ -49,7 +49,7 @@ namespace Ryujinx.HLE.HOS
|
||||
|
||||
bool isArm64Host = RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
|
||||
|
||||
if ((OperatingSystem.IsMacOS() || (!OperatingSystem.IsIOSVersionAtLeast(16, 4))) && isArm64Host && for64Bit && context.Device.Configuration.UseHypervisor)
|
||||
if ((OperatingSystem.IsMacOS() || OperatingSystem.IsIOSVersionAtLeast(16, 3, 2)) && isArm64Host && for64Bit && context.Device.Configuration.UseHypervisor)
|
||||
{
|
||||
var cpuEngine = new HvEngine(_tickSource);
|
||||
var memoryManager = new HvMemoryManager(context.Memory, addressSpaceSize, invalidAccessHandler);
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Ryujinx.HLE.HOS.Applets;
|
||||
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.HLE.Ui
|
||||
{
|
||||
@ -11,7 +10,7 @@ namespace Ryujinx.HLE.Ui
|
||||
/// </summary>
|
||||
/// <param name="userText">Text that the user entered. Set to `null` on internal errors</param>
|
||||
/// <returns>True when OK is pressed, False otherwise. Also returns True on internal errors</returns>
|
||||
public void DisplayInputDialog(SoftwareKeyboardUiArgs args, Action<string> onTextEntered);
|
||||
bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText);
|
||||
|
||||
/// <summary>
|
||||
/// Displays a Message Dialog box to the user and blocks until it is closed.
|
||||
|
@ -1,42 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
public static class AlertHelper
|
||||
{
|
||||
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void showKeyboardAlert(string title, string message, string placeholder);
|
||||
|
||||
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern IntPtr getKeyboardInput();
|
||||
|
||||
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void clearKeyboardInput();
|
||||
|
||||
public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered)
|
||||
{
|
||||
showKeyboardAlert(title, message, placeholder);
|
||||
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
string result = null;
|
||||
while (result == null)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
|
||||
IntPtr inputPtr = getKeyboardInput();
|
||||
if (inputPtr != IntPtr.Zero)
|
||||
{
|
||||
result = Marshal.PtrToStringAnsi(inputPtr);
|
||||
clearKeyboardInput();
|
||||
|
||||
onTextEntered?.Invoke(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -147,7 +147,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
[Option("audio-volume", Required = false, Default = 1.0f, HelpText = "The audio level (0 to 1).")]
|
||||
public float AudioVolume { get; set; }
|
||||
|
||||
[Option("use-hypervisor", Required = false, Default = false, HelpText = "Uses Hypervisor over JIT if available.")]
|
||||
[Option("use-hypervisor", Required = false, Default = false, HelpText = "Uses Hypervisor over JIT if available.")]
|
||||
public bool UseHypervisor { get; set; }
|
||||
|
||||
[Option("lan-interface-id", Required = false, Default = "0", HelpText = "GUID for the network interface used by LAN.")]
|
||||
|
@ -95,6 +95,12 @@ using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using SDL2;
|
||||
|
||||
public class GamepadInfo
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
class Program
|
||||
@ -115,7 +121,6 @@ namespace Ryujinx.Headless.SDL2
|
||||
private static bool _enableMouse;
|
||||
|
||||
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")]
|
||||
public static unsafe int MainExternal(int argCount, IntPtr* pArgs)
|
||||
@ -142,95 +147,6 @@ namespace Ryujinx.Headless.SDL2
|
||||
return 0;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "get_dlc_nca_list")]
|
||||
public static unsafe DlcNcaList GetDlcNcaList(IntPtr titleIdPtr, IntPtr pathPtr)
|
||||
{
|
||||
var titleId = Marshal.PtrToStringAnsi(titleIdPtr);
|
||||
var containerPath = Marshal.PtrToStringAnsi(pathPtr);
|
||||
|
||||
if (!File.Exists(containerPath))
|
||||
{
|
||||
return new DlcNcaList { success = false };
|
||||
}
|
||||
|
||||
using FileStream containerFile = File.OpenRead(containerPath);
|
||||
|
||||
PartitionFileSystem pfs = new();
|
||||
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||
bool containsDlc = false;
|
||||
|
||||
_virtualFileSystem.ImportTickets(pfs);
|
||||
|
||||
// TreeIter? parentIter = null;
|
||||
|
||||
List<DlcNcaListItem> listItems = new();
|
||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath);
|
||||
|
||||
if (nca == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||
{
|
||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != titleId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Logger.Warning?.Print(LogClass.Application, $"ContainerPath: {containerPath}");
|
||||
Logger.Warning?.Print(LogClass.Application, $"TitleId: {nca.Header.TitleId}");
|
||||
Logger.Warning?.Print(LogClass.Application, $"fileEntry.FullPath: {fileEntry.FullPath}");
|
||||
|
||||
// parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
|
||||
// ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
|
||||
|
||||
DlcNcaListItem item = new();
|
||||
CopyStringToFixedArray(fileEntry.FullPath, item.Path, 256);
|
||||
item.TitleId = nca.Header.TitleId;
|
||||
listItems.Add(item);
|
||||
|
||||
containsDlc = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsDlc)
|
||||
{
|
||||
return new DlcNcaList { success = false };
|
||||
// GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
|
||||
}
|
||||
|
||||
var list = new DlcNcaList { success = true, size = (uint) listItems.Count };
|
||||
|
||||
DlcNcaListItem[] items = listItems.ToArray();
|
||||
|
||||
fixed (DlcNcaListItem* p = &items[0])
|
||||
{
|
||||
list.items = p;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static Nca TryCreateNca(IStorage ncaStorage, string containerPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
||||
public static unsafe int GetFPS()
|
||||
@ -319,7 +235,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
var result = Parser.Default.ParseArguments<Options>(args)
|
||||
.WithParsed(options =>
|
||||
{
|
||||
Load(options);
|
||||
Load(options);
|
||||
})
|
||||
.WithNotParsed(errors => errors.Output());
|
||||
|
||||
@ -394,11 +310,50 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
if (_window != null)
|
||||
{
|
||||
|
||||
_window.Exit();
|
||||
_emulationContext.Dispose();
|
||||
_emulationContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "get_game_controllers")]
|
||||
public static unsafe IntPtr GetGamepadList()
|
||||
{
|
||||
List<GamepadInfo> gamepads = new List<GamepadInfo>();
|
||||
IGamepad gamepad;
|
||||
if (_inputManager == null)
|
||||
{
|
||||
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
|
||||
}
|
||||
|
||||
// Collect gamepads from the keyboard driver
|
||||
foreach (string id in _inputManager.KeyboardDriver.GamepadsIds)
|
||||
{
|
||||
gamepad = _inputManager.KeyboardDriver.GetGamepad(id);
|
||||
gamepads.Add(new GamepadInfo { Id = id, Name = gamepad.Name });
|
||||
gamepad.Dispose();
|
||||
}
|
||||
|
||||
// Collect gamepads from the gamepad driver
|
||||
foreach (string id in _inputManager.GamepadDriver.GamepadsIds)
|
||||
{
|
||||
gamepad = _inputManager.GamepadDriver.GetGamepad(id);
|
||||
gamepads.Add(new GamepadInfo { Id = id, Name = gamepad.Name });
|
||||
gamepad.Dispose();
|
||||
}
|
||||
|
||||
// Serialize the gamepad list to a custom string format
|
||||
string result = string.Join("\n", gamepads.Select(g => $"{g.Id}:{g.Name}")); // Ensure System.Linq is available
|
||||
|
||||
// Convert the string to unmanaged memory
|
||||
IntPtr ptr = Marshal.StringToHGlobalAnsi(result);
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "get_game_info")]
|
||||
public static GameInfoNative GetGameInfoNative(int descriptor, IntPtr extensionPtr)
|
||||
@ -406,23 +361,12 @@ namespace Ryujinx.Headless.SDL2
|
||||
if (_virtualFileSystem == null) {
|
||||
_virtualFileSystem = VirtualFileSystem.CreateInstance();
|
||||
}
|
||||
|
||||
var extension = Marshal.PtrToStringAnsi(extensionPtr);
|
||||
var stream = OpenFile(descriptor);
|
||||
|
||||
var gameInfo = GetGameInfo(stream, extension);
|
||||
if (gameInfo == null) {
|
||||
return new GameInfoNative(0, "", "", "", "", new byte[0]);
|
||||
}
|
||||
|
||||
return new GameInfoNative(
|
||||
(ulong)gameInfo.FileSize,
|
||||
gameInfo.TitleName,
|
||||
gameInfo.TitleId,
|
||||
gameInfo.Developer,
|
||||
gameInfo.Version,
|
||||
gameInfo.Icon
|
||||
);
|
||||
return new GameInfoNative(0, gameInfo.TitleName, 0, gameInfo.Developer, 0, gameInfo.Icon);
|
||||
}
|
||||
|
||||
public static GameInfo? GetGameInfo(Stream gameStream, string extension)
|
||||
@ -812,8 +756,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
if (File.Exists(titleUpdateMetadataPath))
|
||||
{
|
||||
string updatePathRelative = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
||||
updatePath = Path.Combine(AppDataManager.BaseDirPath, updatePathRelative);
|
||||
// updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected;
|
||||
|
||||
if (File.Exists(updatePath))
|
||||
{
|
||||
@ -1340,19 +1283,6 @@ namespace Ryujinx.Headless.SDL2
|
||||
renderer = new ThreadedRenderer(renderer);
|
||||
}
|
||||
|
||||
bool AppleHV = false;
|
||||
|
||||
if ((!OperatingSystem.IsIOSVersionAtLeast(16, 4)) && options.UseHypervisor)
|
||||
{
|
||||
AppleHV = true;
|
||||
}
|
||||
else if (OperatingSystem.IsIOS())
|
||||
{
|
||||
AppleHV = false;
|
||||
} else {
|
||||
AppleHV = options.UseHypervisor;
|
||||
}
|
||||
|
||||
HLEConfiguration configuration = new(_virtualFileSystem,
|
||||
_libHacHorizonManager,
|
||||
_contentManager,
|
||||
@ -1376,7 +1306,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
options.IgnoreMissingServices,
|
||||
options.AspectRatio,
|
||||
options.AudioVolume,
|
||||
AppleHV,
|
||||
options.UseHypervisor,
|
||||
options.MultiplayerLanInterfaceId,
|
||||
Common.Configuration.Multiplayer.MultiplayerMode.LdnMitm);
|
||||
|
||||
@ -1579,58 +1509,46 @@ namespace Ryujinx.Headless.SDL2
|
||||
public byte[]? Icon;
|
||||
}
|
||||
|
||||
public unsafe struct DlcNcaListItem
|
||||
{
|
||||
public fixed byte Path[256];
|
||||
public ulong TitleId;
|
||||
}
|
||||
|
||||
public unsafe struct DlcNcaList
|
||||
{
|
||||
public bool success;
|
||||
public uint size;
|
||||
public unsafe DlcNcaListItem* items;
|
||||
}
|
||||
|
||||
public unsafe struct GameInfoNative
|
||||
{
|
||||
public ulong FileSize;
|
||||
public fixed byte TitleName[512];
|
||||
public fixed byte TitleId[32];
|
||||
public ulong TitleId;
|
||||
public fixed byte Developer[256];
|
||||
public fixed byte Version[16];
|
||||
public uint Version;
|
||||
public byte* ImageData;
|
||||
public uint ImageSize;
|
||||
|
||||
public GameInfoNative(ulong fileSize, string titleName, string titleId, string developer, string version, byte[] imageData)
|
||||
public GameInfoNative(ulong fileSize, string titleName, ulong titleId, string developer, uint version, byte[] imageData)
|
||||
{
|
||||
FileSize = fileSize;
|
||||
TitleId = titleId;
|
||||
Version = version;
|
||||
|
||||
fixed (byte* titleNamePtr = TitleName)
|
||||
fixed (byte* titleIdPtr = TitleId)
|
||||
fixed (byte* developerPtr = Developer)
|
||||
fixed (byte* versionPtr = Version)
|
||||
fixed (byte* titleNamePtr = TitleName)
|
||||
{
|
||||
CopyStringToFixedArray(titleName, titleNamePtr, 512);
|
||||
CopyStringToFixedArray(titleId, titleIdPtr, 32);
|
||||
CopyStringToFixedArray(developer, developerPtr, 256);
|
||||
CopyStringToFixedArray(version, versionPtr, 16);
|
||||
}
|
||||
|
||||
if (imageData == null || imageData.Length > 4096 * 4096)
|
||||
{
|
||||
ImageSize = 0;
|
||||
// throw new ArgumentException("Image data must not exceed 4 MB.");
|
||||
ImageSize = (uint)0;
|
||||
ImageData = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
ImageSize = (uint)imageData.Length;
|
||||
|
||||
ImageData = (byte*)Marshal.AllocHGlobal(imageData.Length);
|
||||
|
||||
Marshal.Copy(imageData, 0, (IntPtr)ImageData, imageData.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// Free allocated memory for ImageData
|
||||
// Don't forget to free the allocated memory
|
||||
public void Dispose()
|
||||
{
|
||||
if (ImageData != null)
|
||||
@ -1639,13 +1557,17 @@ namespace Ryujinx.Headless.SDL2
|
||||
ImageData = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void CopyStringToFixedArray(string source, byte* destination, int length)
|
||||
{
|
||||
var span = new Span<byte>(destination, length);
|
||||
Encoding.UTF8.GetBytes(source, span);
|
||||
}
|
||||
|
||||
private static unsafe void CopyStringToFixedArray(string source, byte* destination, int length)
|
||||
{
|
||||
var span = new Span<byte>(destination, length);
|
||||
span.Clear();
|
||||
Encoding.UTF8.GetBytes(source, span);
|
||||
private static void CopyArrayToFixedArray(byte[] source, byte* destination, int maxLength)
|
||||
{
|
||||
var span = new Span<byte>(destination, maxLength);
|
||||
source.AsSpan().CopyTo(span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -186,7 +186,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
}
|
||||
|
||||
// WindowHandle = SDL_GetWindowFromID(1);
|
||||
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", 0, 0, Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
|
||||
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
|
||||
|
||||
if (WindowHandle == IntPtr.Zero)
|
||||
{
|
||||
@ -217,10 +217,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
Width = evnt.window.data1;
|
||||
Height = evnt.window.data2;
|
||||
if (Renderer?.Window != null)
|
||||
{
|
||||
Renderer.Window.SetSize(Width, Height);
|
||||
}
|
||||
Renderer?.Window.SetSize(Width, Height);
|
||||
MouseDriver.SetClientSize(Width, Height);
|
||||
}
|
||||
break;
|
||||
@ -450,29 +447,33 @@ namespace Ryujinx.Headless.SDL2
|
||||
};
|
||||
renderLoopThread.Start();
|
||||
|
||||
Thread nvidiaStutterWorkaround = null;
|
||||
if (Renderer is OpenGLRenderer)
|
||||
{
|
||||
nvidiaStutterWorkaround = new Thread(NvidiaStutterWorkaround)
|
||||
{
|
||||
Name = "GUI.NvidiaStutterWorkaround",
|
||||
};
|
||||
nvidiaStutterWorkaround.Start();
|
||||
}
|
||||
|
||||
MainLoop();
|
||||
|
||||
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
|
||||
// We only need to wait for all commands submitted during the main gpu loop to be processed.
|
||||
_gpuDoneEvent.WaitOne();
|
||||
_gpuDoneEvent.Dispose();
|
||||
nvidiaStutterWorkaround?.Join();
|
||||
|
||||
Exit();
|
||||
}
|
||||
|
||||
public void DisplayInputDialog(SoftwareKeyboardUiArgs args, Action<string> onTextEntered)
|
||||
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
|
||||
{
|
||||
// SDL2 doesn't support input dialogs
|
||||
// Trying to use Objective-C on iDevices
|
||||
if (OperatingSystem.IsIOS())
|
||||
{
|
||||
AlertHelper.ShowAlertWithTextInput(args.HeaderText, args.SubtitleText, args.GuideText, (inputText) =>
|
||||
{
|
||||
onTextEntered?.Invoke(inputText);
|
||||
});
|
||||
} else {
|
||||
onTextEntered?.Invoke("");
|
||||
}
|
||||
userText = "Ryujinx";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DisplayMessageDialog(string title, string message)
|
||||
|
@ -46,7 +46,7 @@ namespace Ryujinx.Memory
|
||||
|
||||
private const IntPtr TASK_NULL = 0;
|
||||
private static readonly IntPtr _selfTask;
|
||||
private static readonly int DEFAULT_CHUNK_SIZE = 1024 * 1024;
|
||||
private static readonly int DEFAULT_CHUNK_SIZE = 16 * 1024 * 1024;
|
||||
|
||||
static MachJitWorkaround()
|
||||
{
|
||||
@ -211,4 +211,4 @@ namespace Ryujinx.Memory
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -81,10 +81,57 @@ namespace Ryujinx.Ui.Applet
|
||||
return okPressed;
|
||||
}
|
||||
|
||||
public void DisplayInputDialog(SoftwareKeyboardUiArgs args, Action<string> onTextEntered)
|
||||
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
|
||||
{
|
||||
onTextEntered?.Invoke("MeloNX");
|
||||
return;
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool okPressed = false;
|
||||
bool error = false;
|
||||
string inputText = args.InitialText ?? "";
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
var swkbdDialog = new SwkbdAppletDialog(_parent)
|
||||
{
|
||||
Title = "Software Keyboard",
|
||||
Text = args.HeaderText,
|
||||
SecondaryText = args.SubtitleText,
|
||||
};
|
||||
|
||||
swkbdDialog.InputEntry.Text = inputText;
|
||||
swkbdDialog.InputEntry.PlaceholderText = args.GuideText;
|
||||
swkbdDialog.OkButton.Label = args.SubmitText;
|
||||
|
||||
swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
|
||||
swkbdDialog.SetInputValidation(args.KeyboardMode);
|
||||
|
||||
if (swkbdDialog.Run() == (int)ResponseType.Ok)
|
||||
{
|
||||
inputText = swkbdDialog.InputEntry.Text;
|
||||
okPressed = true;
|
||||
}
|
||||
|
||||
swkbdDialog.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = true;
|
||||
|
||||
GtkDialog.CreateErrorDialog($"Error displaying Software Keyboard: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
userText = error ? null : inputText;
|
||||
|
||||
return error || okPressed;
|
||||
}
|
||||
|
||||
public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value)
|
||||
|
Loading…
x
Reference in New Issue
Block a user