1
0
forked from MeloNX/MeloNX

Compare commits

..

88 Commits

Author SHA1 Message Date
0cd32a1e01 Update repo.json 2025-04-04 04:56:05 +00:00
d64a9f3c3d Repo Template
Reviewed-on: #1
2025-03-05 22:37:13 +00:00
83c4a95f4b Repo Template
Other things could/should be added, this is just barebone for now
2025-03-05 22:36:24 +00:00
e372f6eb35 Merge pull request 'Update ReadMe to have accurate information' (#14) from TechGuy/MeloNX:XC-ios-ht into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#14
2025-03-04 07:15:10 +00:00
76b82243a7 Update README.md 2025-03-04 07:09:00 +00:00
Stossy11
e924da52ec add back SDL Window option 2025-03-01 21:31:33 +11:00
Stossy11
ea2eff15c6 New gitignore 2025-03-01 21:06:09 +11:00
Stossy11
c6ff0b60bf Adds pull #12 and changes version number 2025-03-01 21:03:07 +11:00
Stossy11
edc56316cc add iOS 15 support back, fix the need for SDL Window and more 2025-03-01 20:54:57 +11:00
Stossy11
8df465a959 implement an all-swift approach for detecting JIT (adds support for detecting JIT on iOS 18.4+), adds an easter egg, and more 2025-03-01 20:54:57 +11:00
527ac3fb23 Update README.md 2025-02-20 20:54:03 +00:00
8e60f6dc50 Update README.md 2025-02-19 07:42:45 +00:00
Stossy11
3b99631dfb Implement JIT Cache Regions and Add Handheld Input 2025-02-19 11:16:50 +11:00
cb33b04f2b Merge pull request 'GCController wrapper' (#6) from XITRIX/MeloNX:Controller-fix into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#6
2025-02-17 01:20:48 +00:00
500f3d5b9e Fix for virtual controller detach | pass gamepad haptic engine 2025-02-17 00:39:04 +01:00
ac4e5d394e Hide non wrapped controllers 2025-02-17 00:39:03 +01:00
f2d078f80b GCController wrapper 2025-02-17 00:39:03 +01:00
004a81fa60 Merge pull request 'DLC manager implemented' (#11) from XITRIX/MeloNX:DLC-support into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#11
2025-02-16 23:33:30 +00:00
ddf634ecb6 DLC manager implemented 2025-02-17 00:28:05 +01:00
Stossy11
cce876c6f5 Change version number 2025-02-17 07:40:56 +11:00
ebfb39c132 Merge pull request 'Correct device req + Add Xcode to compiling req' (#10) from C4ndyF1sh/MeloNX:device-req-and-compiling-req into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#10
2025-02-16 20:15:56 +00:00
b3bb9cefcf Merge pull request 'Update manager fix' (#8) from XITRIX/MeloNX:Update-manager-fix into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#8
2025-02-16 20:14:58 +00:00
8c54134699 Update Compile.md 2025-02-16 17:57:59 +00:00
e8537df246 Update README.md 2025-02-16 17:55:44 +00:00
8c6dd455f2 Game update path fix 2025-02-16 15:46:46 +01:00
2a7cfa5650 GameInfo UI enhancement 2025-02-16 13:52:12 +01:00
df2b17ddd6 UI enhancements 2025-02-16 13:41:57 +01:00
757fb1f6d1 Update manager fix 2025-02-16 13:41:57 +01:00
Stossy11
e741039304 Add Entitlement Checker and Memory 2025-02-16 17:29:09 +11:00
Stossy11
fd0ce75f67 Fix whatever happened 2025-02-16 16:22:20 +11:00
Stossy11
0e80bd3d51 Add game update manager 2025-02-16 14:01:31 +11:00
Stossy11
f95281899c a bunch of changes 2025-02-16 12:17:12 +11:00
802a8d7bae Merge pull request 'Added title update functionality' (#7) from XITRIX/MeloNX:Title-update into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#7
2025-02-16 00:04:42 +00:00
7277e1fa9b Add title update functionality 2025-02-15 20:34:36 +01:00
27312d4f31 Null pointer fix 2025-02-15 20:34:36 +01:00
4ffb0ff617 Update Compile.md 2025-02-15 01:37:00 +00:00
a358dcdfc4 Update Compile.md 2025-02-15 01:35:48 +00:00
08ee9b18ea Update LICENSE.txt 2025-02-13 12:15:27 +00:00
aadc258187 Update LICENSE.txt 2025-02-13 12:13:53 +00:00
1c75d22190 Update README.md to reflect the new LICENSE 2025-02-13 11:38:35 +00:00
57c297369a Add New License 2025-02-13 11:37:34 +00:00
56544db198 Move Around Steps a little 2025-02-13 04:37:28 +00:00
6ec2ad2841 Add Update Guide 2025-02-13 04:36:35 +00:00
9d6c7d9900 Merge pull request 'Fix broken link to compatibility' (#5) from Vishram1123/MeloNX:XC-ios-ht into XC-ios-ht
Reviewed-on: MeloNX/MeloNX#5
2025-02-13 03:18:12 +00:00
9ddc6a969c Fix broken link to compatibility 2025-02-13 02:20:20 +00:00
Stossy11
1b69c0bdc6 Set Version to 1.1.0 2025-02-13 12:56:20 +11:00
Stossy11
18d98755f6 Add Disable vSync 2025-02-13 12:47:53 +11:00
Stossy11
c6de4abce3 Fix vertical controllers 2025-02-13 10:00:01 +11:00
Stossy11
e5c5e8572e Fix Keyboard and add Disable PTC mode 2025-02-13 10:00:01 +11:00
c0e8570293 Update README.md 2025-02-12 21:17:11 +00:00
c8a3124cca Update Compile.md 2025-02-12 13:04:56 +00:00
2c389c899a Update Compile.md 2025-02-12 12:34:44 +00:00
11571aca6e add xcode-select --switch to Compile.md 2025-02-12 10:23:48 +00:00
Stossy11
e04e689bc4 Fix spelling mistake 2025-02-12 21:06:31 +11:00
5c903626cc Update Site Workflow 2025-02-12 09:20:17 +00:00
9ca187a8c4 Fix Icon 2025-02-12 07:55:02 +00:00
cac3853d96 Update README.md 2025-02-12 07:49:51 +00:00
fff70a2dba Update README.md 2025-02-12 07:49:01 +00:00
4da30e332c Update Compile.md 2025-02-12 07:41:05 +00:00
Stossy11
114ba3eb57 Remove func MainThread and add SDL Window code back 2025-02-12 18:19:51 +11:00
Stossy11
839ddab589 small UI chanes 2025-02-12 18:19:51 +11:00
Bella
00a06c4dc8
Replace GitHub Actions workflow with Gitea workflow for notifying API on release 2025-02-12 19:55:06 +13:00
efbeebafcb (hopefully this work) Add: Action for adding release to site 2025-02-12 06:00:59 +00:00
Stossy11
b85758ba88 FIx starting the emulation on iOS 16 and below. 2025-02-12 08:39:37 +11:00
Stossy11
46196daf39 Fix Keyboard issues 2025-02-11 20:43:21 +11:00
Stossy11
eb4a4593ea Add Software Keyboard, Edit maxSets and more 2025-02-11 20:22:50 +11:00
Stossy11
c3ade6f5cd Add Built in JitStreamer-EB implementation, rework the shortcut and more 2025-02-10 23:27:06 +11:00
Stossy11
007cb026a4 Add Intent to Launch Game and change how DRM works 2025-02-10 17:46:24 +11:00
Stossy11
4f3e49a90c DRM Update 2025-02-10 10:54:59 +11:00
Stossy11
2d5f1d8015 Add Airplay 2025-02-09 16:31:26 +11:00
Stossy11
f57d24706b Add new SwiftUI Emulation View 2025-02-09 15:49:10 +11:00
Bella
a2c3f6d624
Add games, delete games and game info view 2025-02-09 00:15:37 +13:00
Stossy11
9c014e6f87 Fix missing chunkSize 2025-02-08 20:44:12 +11:00
Stossy11
c8db129402 Add comments to explain stuff and other stuff 2025-02-07 10:33:43 +11:00
Stossy11
0b6518d7e3 re-add MVK Config Prefill but make it default to true 2025-02-02 00:40:05 +11:00
Stossy11
cb114fbb68 Edit MoltenVK and remove "MVK: Pre-Fill Metal Command Buffers" due to not being needed anymore 2025-02-02 00:36:33 +11:00
Stossy11
f2ea6448dc make toggle not able to be dragged 2025-02-01 18:19:07 +11:00
Stossy11
9fa29efaf4 Add Aspect Ratio, Clean up some code 2025-02-01 17:02:13 +11:00
Stossy11
a166494e33 Fix Controllers, Add Auto Controller, Fix touch screen, Rewrite SDL Controller Detection. 2025-02-01 16:37:58 +11:00
1d16bf0c94 Add MVK options into settings instead of preset settings 2025-01-20 21:23:54 -08:00
Stossy11
7008ce4f23 Disable MVK_CONFIG_USE_METAL_PRIVATE_API, as it isn't needed and causes more overhead 2025-01-21 01:13:35 +11:00
Stossy11
52fd0bf79b Add detection for Hypervisor 2025-01-21 00:31:41 +11:00
Stossy11
0cc5476d87 Fix settings 2025-01-21 00:21:34 +11:00
7dde0d254a Add multiple missing settings 2025-01-20 05:11:01 -08:00
Stossy11
11305c2aac Sign Hypervisor 2025-01-20 23:50:57 +11:00
Stossy11
9ce29d6ad1 Hypervisor 2025-01-20 23:22:42 +11:00
Stossy11
5ee90c81e9 Fix some stuff :3 2025-01-20 22:10:12 +11:00
Stossy11
26fe33703d Fix LibMoltenVK 2025-01-20 19:42:33 +11:00
85 changed files with 3846 additions and 3027 deletions

View File

@ -0,0 +1,21 @@
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
View File

@ -176,3 +176,67 @@ PublishProfiles/
# Glade backup files # Glade backup files
*.glade~ *.glade~
src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib 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

View File

@ -1,33 +1,81 @@
# How to compile MeloNX using macOS # Compiling MeloNX on macOS
## Prerequisites ## Prerequisites
- [.NET 8.0](<https://dotnet.microsoft.com/en-us/download/dotnet/8.0>)
- A computer with macOS
## Compiling Before you begin, ensure you have the following installed:
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
```
2. Open the Xcode project, stored at MeloNX/src/MeloNX - [**.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**
3. Make sure `Ryujinx.SDL2.Headless.dylib` is set to `Embed & Sign` in the General settings for the Xcode project ## Compilation Steps
4. Signing & Capabilities
Change your 'Team' to your Developer Account (free or paid) and change Bundle Identifier to
`com.*your name*.MeloNX`
6. Build and Run ### 1. Clone the Repository and Build Ryujinx
`CMD+R`
7. Check the [post-setup guide](<https://github.com/melonx-emu/melonx/tree/XC-ios-ht/postsetup.md>) Open a terminal and run:
## If you don't have a paid developer account ```sh
Make sure these entitlements are removed if you don't have a paid Apple Developer account git clone https://git.743378673.xyz/MeloNX/MeloNX.git
cd MeloNX
./compile.sh
``` ```
Extended Virtual Addressing 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.)
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
```

View File

@ -1,9 +1,15 @@
MIT License MeloNX License
Copyright (c) Ryujinx Team and Contributors Copyright (c) MeloNX Team and Contributors
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: 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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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 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. 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
View File

@ -1,22 +1,123 @@
<p align="center"> <p align="center">
<a href="https://github.com/MeloNX-Emu/MeloNX"> <a href="https://melonx.org">
<img src="https://github.com/MeloNX-Emu/melonx-emu.github.io/blob/main/favicon.png?raw=true" alt="MeloNX Logo" width="120"> <img src="https://melonx.org/static/imgs/MeloNX.svg" alt="MeloNX Logo" width="120">
</a> </a>
</p> </p>
<h1 align="center">MeloNX</h1>
<p align="center"> <p align="center">
MeloNX enables Nintendo Switch game emulation on iOS using the Ryujinx iOS code base. MeloNX enables Nintendo Switch game emulation on iOS using the Ryujinx iOS code base.
</p> </p>
<p align="center"> <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. 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">MIT license</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">MeloNX license (Based on MIT)</a>. <br
</p> </p>
## Compatibility # Compatibility
MeloNX works on iPhone X and later and iPad 7th Gen and later. A lot of games work. 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>.
## Usage # Usage
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). ## 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.

25
repo.json Normal file
View File

@ -0,0 +1,25 @@
{
"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": {}
}

View File

@ -7,6 +7,7 @@ namespace ARMeilleure.Memory
public const int DefaultGranularity = 65536; // Mapping granularity in Windows. public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
public IJitMemoryBlock Block { get; } public IJitMemoryBlock Block { get; }
public IJitMemoryAllocator Allocator { get; }
public IntPtr Pointer => Block.Pointer; public IntPtr Pointer => Block.Pointer;
@ -21,6 +22,7 @@ namespace ARMeilleure.Memory
granularity = DefaultGranularity; granularity = DefaultGranularity;
} }
Allocator = allocator;
Block = allocator.Reserve(maxSize); Block = allocator.Reserve(maxSize);
_maxSize = maxSize; _maxSize = maxSize;
_sizeGranularity = granularity; _sizeGranularity = granularity;

View File

@ -3,6 +3,7 @@ using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory; using ARMeilleure.Memory;
using ARMeilleure.Native; using ARMeilleure.Native;
using Ryujinx.Memory; using Ryujinx.Memory;
using Ryujinx.Common.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -15,11 +16,11 @@ namespace ARMeilleure.Translation.Cache
static partial class JitCache static partial class JitCache
{ {
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize(); private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
private static readonly int _pageMask = _pageSize - 8; private static readonly int _pageMask = _pageSize - 4;
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 128 * 1024 * 1024; private const int CacheSize = 128 * 1024 * 1024;
private const int CacheSizeIOS = 64 * 1024 * 1024; private const int CacheSizeIOS = 128 * 1024 * 1024;
private static ReservedRegion _jitRegion; private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator; private static JitCacheInvalidation _jitCacheInvalidator;
@ -31,6 +32,10 @@ namespace ARMeilleure.Translation.Cache
private static readonly object _lock = new(); private static readonly object _lock = new();
private static bool _initialized; private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize); public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize);
@ -49,7 +54,11 @@ namespace ARMeilleure.Translation.Cache
return; return;
} }
_jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize)); var firstRegion = new ReservedRegion(allocator, CacheSize);
_jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS()) if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
{ {
@ -60,7 +69,9 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize)); JitUnwindWindows.InstallFunctionTableHandler(
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
);
} }
_initialized = true; _initialized = true;
@ -73,7 +84,9 @@ namespace ARMeilleure.Translation.Cache
{ {
while (_deferredRxProtect.TryDequeue(out var result)) while (_deferredRxProtect.TryDequeue(out var result))
{ {
ReprotectAsExecutable(result.funcOffset, result.length); ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
ReprotectAsExecutable(targetRegion, result.funcOffset, result.length);
} }
} }
@ -87,21 +100,14 @@ namespace ARMeilleure.Translation.Cache
int funcOffset = Allocate(code.Length, deferProtect); int funcOffset = Allocate(code.Length, deferProtect);
IntPtr funcPtr = _jitRegion.Pointer + funcOffset; ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
IntPtr funcPtr = targetRegion.Pointer + funcOffset;
if (OperatingSystem.IsIOS()) if (OperatingSystem.IsIOS())
{ {
Marshal.Copy(code, 0, funcPtr, code.Length); Marshal.Copy(code, 0, funcPtr, code.Length);
if (deferProtect) ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
{ JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
_deferredRxProtect.Enqueue((funcOffset, code.Length));
}
else
{
ReprotectAsExecutable(funcOffset, code.Length);
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
}
} }
else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64) else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@ -115,9 +121,9 @@ namespace ARMeilleure.Translation.Cache
} }
else else
{ {
ReprotectAsWritable(funcOffset, code.Length); ReprotectAsWritable(targetRegion, funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length); Marshal.Copy(code, 0, funcPtr, code.Length);
ReprotectAsExecutable(funcOffset, code.Length); ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{ {
@ -139,41 +145,50 @@ namespace ARMeilleure.Translation.Cache
{ {
if (OperatingSystem.IsIOS()) if (OperatingSystem.IsIOS())
{ {
return; // return;
} }
lock (_lock) lock (_lock)
{ {
Debug.Assert(_initialized); foreach (var region in _jitRegions)
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{ {
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size)); if (pointer.ToInt64() < region.Pointer.ToInt64() ||
_cacheEntries.RemoveAt(entryIndex); 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;
} }
} }
} }
private static void ReprotectAsWritable(int offset, int size) private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static void ReprotectAsExecutable(int offset, int size) private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
{ {
int endOffs = offset + size; int endOffs = offset + size;
int regionStart = offset & ~_pageMask; int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask; int regionEnd = (endOffs + _pageMask) & ~_pageMask;
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart)); region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
} }
private static int Allocate(int codeSize, bool deferProtect = false) private static int Allocate(int codeSize, bool deferProtect = false)
@ -187,20 +202,35 @@ namespace ARMeilleure.Translation.Cache
alignment = 0x4000; alignment = 0x4000;
} }
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment); for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
//DEBUG: Show JIT Memory Allocation
//Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}");
if (allocOffset < 0)
{ {
throw new OutOfMemoryException("JIT Cache exhausted."); int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
if (allocOffset >= 0)
{
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
_activeRegionIndex = i;
return allocOffset;
}
} }
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize); int exhaustedRegion = _activeRegionIndex;
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
return allocOffset; 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;
} }
private static int AlignCodeSize(int codeSize, bool deferProtect = false) private static int AlignCodeSize(int codeSize, bool deferProtect = false)

View File

@ -25,11 +25,9 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; }; 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
4E4854022D138D7600A446A6 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; }; 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
4EA894EB2D3E3DC700FABB01 /* Ryujinx.Headless.SDL2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */; }; CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -57,13 +55,22 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase 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 */ = { 4E80AA092CD6FAA800029585 /* Embed Libraries */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
dstPath = ""; dstPath = "";
dstSubfolderSpec = 10; dstSubfolderSpec = 10;
files = ( files = (
4EA894EC2D3E3DC700FABB01 /* Ryujinx.Headless.SDL2.dylib in Embed Libraries */,
); );
name = "Embed Libraries"; name = "Embed Libraries";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -71,19 +78,19 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference 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; }; 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; }; 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; }; 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; }; 4E80AA622CD7122800029585 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = System/Library/Frameworks/GameController.framework; sourceTree = SDKROOT; };
5650564A2D2A758600C8BB1E /* dotnet.xcconfig.example */ = {isa = PBXFileReference; lastKnownFileType = text; path = dotnet.xcconfig.example; sourceTree = "<group>"; }; 5650564A2D2A758600C8BB1E /* dotnet.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = dotnet.xcconfig; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
565056492D2A756A00C8BB1E /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { CA8F9C2D2D3F5A3A00D7E586 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
Info.plist, Info.plist,
); );
target = 4E80A98C2CD6F54500029585 /* MeloNX */; target = 4E80A98C2CD6F54500029585 /* MeloNX */;
@ -91,14 +98,17 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ /* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */
5650564D2D2A75B300C8BB1E /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = { CA0AE31D2D3EECBC00F6D350 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = {
isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;
attributesByRelativePath = { attributesByRelativePath = {
"Dependencies/Dynamic Libraries/Hypervisor.framework" = ( "Dependencies/Dynamic Libraries/Hypervisor.framework" = (
CodeSignOnCopy, CodeSignOnCopy,
RemoveHeadersOnCopy, RemoveHeadersOnCopy,
); );
"Dependencies/Dynamic Libraries/SoftwareKeyboard.framework" = ( "Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
CodeSignOnCopy,
);
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework" = (
CodeSignOnCopy, CodeSignOnCopy,
RemoveHeadersOnCopy, RemoveHeadersOnCopy,
); );
@ -158,7 +168,8 @@
"Dependencies/Dynamic Libraries/libavcodec.dylib", "Dependencies/Dynamic Libraries/libavcodec.dylib",
"Dependencies/Dynamic Libraries/libavutil.dylib", "Dependencies/Dynamic Libraries/libavutil.dylib",
"Dependencies/Dynamic Libraries/libMoltenVK.dylib", "Dependencies/Dynamic Libraries/libMoltenVK.dylib",
"Dependencies/Dynamic Libraries/SoftwareKeyboard.framework", "Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework",
Dependencies/XCFrameworks/libavcodec.xcframework, Dependencies/XCFrameworks/libavcodec.xcframework,
Dependencies/XCFrameworks/libavfilter.xcframework, Dependencies/XCFrameworks/libavfilter.xcframework,
Dependencies/XCFrameworks/libavformat.xcframework, Dependencies/XCFrameworks/libavformat.xcframework,
@ -174,7 +185,7 @@
/* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ /* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
4E80A98F2CD6F54500029585 /* MeloNX */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (565056492D2A756A00C8BB1E /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 5650564D2D2A75B300C8BB1E /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = MeloNX; sourceTree = "<group>"; }; 4E80A98F2CD6F54500029585 /* MeloNX */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CA8F9C2D2D3F5A3A00D7E586 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, CA0AE31D2D3EECBC00F6D350 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = MeloNX; sourceTree = "<group>"; };
4E80A9A02CD6F54700029585 /* MeloNXTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MeloNXTests; sourceTree = "<group>"; }; 4E80A9A02CD6F54700029585 /* MeloNXTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MeloNXTests; sourceTree = "<group>"; };
4E80A9AA2CD6F54700029585 /* MeloNXUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MeloNXUITests; sourceTree = "<group>"; }; 4E80A9AA2CD6F54700029585 /* MeloNXUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MeloNXUITests; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
@ -184,10 +195,10 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4E4854022D138D7600A446A6 /* GameController.framework in Frameworks */,
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */, 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */, 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4EA894EB2D3E3DC700FABB01 /* Ryujinx.Headless.SDL2.dylib in Frameworks */, 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -211,7 +222,7 @@
4E80A9842CD6F54500029585 = { 4E80A9842CD6F54500029585 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5650564A2D2A758600C8BB1E /* dotnet.xcconfig.example */, 5650564A2D2A758600C8BB1E /* dotnet.xcconfig */,
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */, BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */,
4E80A98F2CD6F54500029585 /* MeloNX */, 4E80A98F2CD6F54500029585 /* MeloNX */,
4E80A9A02CD6F54700029585 /* MeloNXTests */, 4E80A9A02CD6F54700029585 /* MeloNXTests */,
@ -234,6 +245,7 @@
4E80AA192CD700F500029585 /* Frameworks */ = { 4E80AA192CD700F500029585 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4E7023A52D5A98E2002C7183 /* UIKit.framework */,
4E80AA622CD7122800029585 /* GameController.framework */, 4E80AA622CD7122800029585 /* GameController.framework */,
); );
name = Frameworks; name = Frameworks;
@ -248,7 +260,7 @@
buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */; buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */;
buildPhases = ( buildPhases = (
); );
buildToolPath = "$(DOTNET_PATH)"; buildToolPath = /usr/local/share/dotnet/dotnet;
buildWorkingDirectory = "$(SRCROOT)/../.."; buildWorkingDirectory = "$(SRCROOT)/../..";
dependencies = ( dependencies = (
); );
@ -269,6 +281,7 @@
4E80A98A2CD6F54500029585 /* Frameworks */, 4E80A98A2CD6F54500029585 /* Frameworks */,
4E80A98B2CD6F54500029585 /* Resources */, 4E80A98B2CD6F54500029585 /* Resources */,
4E80AA092CD6FAA800029585 /* Embed Libraries */, 4E80AA092CD6FAA800029585 /* Embed Libraries */,
4E50F49E2D5CC28B0080F1D1 /* Embed Watch Content */,
); );
buildRules = ( buildRules = (
); );
@ -339,7 +352,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = 1; BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1610; LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1610; LastUpgradeCheck = 1610;
TargetAttributes = { TargetAttributes = {
4E80A98C2CD6F54500029585 = { 4E80A98C2CD6F54500029585 = {
@ -393,7 +406,6 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5650564B2D2A758600C8BB1E /* dotnet.xcconfig.example in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -635,18 +647,47 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(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; GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist; INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
INFOPLIST_KEY_GCSupportsGameMode = YES; INFOPLIST_KEY_GCSupportsGameMode = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES; INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -668,7 +709,6 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
@ -679,7 +719,6 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/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",
@ -722,123 +761,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/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 = 0.0.8; MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -871,18 +795,47 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(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; GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist; INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
INFOPLIST_KEY_GCSupportsGameMode = YES; INFOPLIST_KEY_GCSupportsGameMode = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES; INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -904,7 +857,6 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
@ -915,7 +867,6 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/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",
@ -958,123 +909,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/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 = 0.0.8; MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "1b46f7a56d6f994a826e31441c25b929398800cf38b3e9be23ae6e0ef8fc32c7", "originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b",
"pins" : [ "pins" : [
{ {
"identity" : "swiftsvg", "identity" : "swiftsvg",

View File

@ -1,24 +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>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>

View File

@ -1,40 +0,0 @@
<?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>

View File

@ -1,42 +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>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>

View File

@ -1,24 +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>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>

View File

@ -0,0 +1,58 @@
//
// 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
}

View File

@ -5,11 +5,15 @@
// Created by Stossy11 on 3/11/2024. // Created by Stossy11 on 3/11/2024.
// //
#define DRM 0
#define CS_DEBUGGED 0x10000000
#ifndef RyujinxHeader #ifndef RyujinxHeader
#define RyujinxHeader #define RyujinxHeader
#import "SDL2/SDL.h" #include <SDL2/SDL.h>
#include <SDL2/SDL_syswm.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -18,15 +22,28 @@ extern "C" {
struct GameInfo { struct GameInfo {
long FileSize; long FileSize;
char TitleName[512]; char TitleName[512];
long TitleId; char TitleId[32];
char Developer[256]; char Developer[256];
int Version; char Version[16];
unsigned char* ImageData; unsigned char* ImageData;
unsigned int ImageSize; 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 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); void install_firmware(const char* inputPtr);
char* installed_firmware_version(); char* installed_firmware_version();
@ -39,8 +56,6 @@ int get_current_fps();
void initialize(); void initialize();
const char* get_game_controllers();
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -0,0 +1,53 @@
//
// 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_, &region, &regionSize, 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
}

View File

@ -0,0 +1,78 @@
//
// 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)
}
}
}

View File

@ -1,27 +0,0 @@
#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);

View File

@ -1,91 +0,0 @@
#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
}
}

View File

@ -0,0 +1,237 @@
//
// 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
}
}

View File

@ -20,12 +20,10 @@ class VirtualController {
} }
private func setupVirtualController() { private func setupVirtualController() {
// Initialize SDL if not already initialized
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 { if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER)) SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
} }
// Create virtual controller
var joystickDesc = SDL_VirtualJoystickDesc( var joystickDesc = SDL_VirtualJoystickDesc(
version: UInt16(SDL_VIRTUAL_JOYSTICK_DESC_VERSION), version: UInt16(SDL_VIRTUAL_JOYSTICK_DESC_VERSION),
type: Uint16(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), type: Uint16(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue),
@ -80,7 +78,7 @@ class VirtualController {
} }
} }
static func rumble(lowFreq: Float, highFreq: Float) { static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
do { do {
// Low-frequency haptic pattern // Low-frequency haptic pattern
let lowFreqPattern = try CHHapticPattern(events: [ let lowFreqPattern = try CHHapticPattern(events: [
@ -98,9 +96,23 @@ class VirtualController {
], relativeTime: 0.2, duration: 0.2) ], relativeTime: 0.2, duration: 0.2)
], parameters: []) ], parameters: [])
// Create and start the haptic engine // Mutable engine
let engine = try CHHapticEngine() var engine = engine
try engine.start()
// 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 play the low-frequency player // Create and play the low-frequency player
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern) let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
@ -115,6 +127,8 @@ class VirtualController {
} }
} }
private static var hapticEngine: CHHapticEngine?
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) { func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return } guard controller != nil else { return }

View File

@ -1,80 +0,0 @@
//
// 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
}
}

View File

@ -8,31 +8,92 @@
import Foundation import Foundation
import GameController import GameController
import UIKit import UIKit
import SwiftUI
var theWindow: UIWindow? = nil var theWindow: UIWindow? = nil
extension UIWindow { 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() { @objc func wdb_makeKeyAndVisible() {
if #available(iOS 13.0, *) { let enabled = UserDefaults.standard.bool(forKey: "oldWindowCode")
if #unavailable(iOS 17.0), enabled {
self.windowScene = (UIApplication.shared.connectedScenes.first! as! UIWindowScene) self.windowScene = (UIApplication.shared.connectedScenes.first! as! UIWindowScene)
} }
self.wdb_makeKeyAndVisible() self.wdb_makeKeyAndVisible()
theWindow = self theWindow = self
if #available(iOS 17, *) {
if UserDefaults.standard.bool(forKey: "isVirtualController") { Ryujinx.shared.repeatuntilfindLayer()
if let window = theWindow { } else if UserDefaults.standard.bool(forKey: "isVirtualController") && enabled {
waitForController()
}
}
}
// MARK: - iOS 16 and below Only
waitforcontroller()
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
} }
} }
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() { func patchMakeKeyAndVisible() {
let uiwindowClass = UIWindow.self let uiwindowClass = UIWindow.self
if let m1 = class_getInstanceMethod(uiwindowClass, #selector(UIWindow.makeKeyAndVisible)), if let m1 = class_getInstanceMethod(uiwindowClass, #selector(UIWindow.makeKeyAndVisible)),

View File

@ -0,0 +1,18 @@
//
// 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()
}
}

View File

@ -0,0 +1,28 @@
//
// 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)"
}
}
}

View File

@ -0,0 +1,52 @@
//
// 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"
}
}
}

View File

@ -0,0 +1,31 @@
//
// 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"
}
}
}

View File

@ -28,17 +28,29 @@ struct iOSNav<Content: View>: View {
} }
} }
class Ryujinx { class Ryujinx {
private var isRunning = false private var isRunning = false
let virtualController = VirtualController() let virtualController = VirtualController()
@Published var controllerMap: [Controller] = [] @Published var controllerMap: [Controller] = []
@State var firmwareversion = "0" @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
}
static let shared = Ryujinx() static let shared = Ryujinx()
private init() {} private init() {
self.games = loadGames()
}
public struct Configuration : Codable, Equatable { public struct Configuration : Codable, Equatable {
var gamepath: String var gamepath: String
@ -49,13 +61,23 @@ class Ryujinx {
var nintendoinput: Bool var nintendoinput: Bool
var enableInternet: Bool var enableInternet: Bool
var listinputids: Bool var listinputids: Bool
var fullscreen: Bool var aspectRatio: AspectRatio
var memoryManagerMode: String var memoryManagerMode: String
var disableShaderCache: Bool var disableShaderCache: Bool
var hypervisor: Bool var hypervisor: Bool
var disableDockedMode: Bool var disableDockedMode: Bool
var enableTextureRecompression: Bool var enableTextureRecompression: Bool
var additionalArgs: [String] 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, init(gamepath: String,
@ -63,8 +85,8 @@ class Ryujinx {
debuglogs: Bool = false, debuglogs: Bool = false,
tracelogs: Bool = false, tracelogs: Bool = false,
listinputids: Bool = false, listinputids: Bool = false,
fullscreen: Bool = true, aspectRatio: AspectRatio = .fixed16x9,
memoryManagerMode: String = "HostMapped", memoryManagerMode: String = "HostMappedUnsafe",
disableShaderCache: Bool = false, disableShaderCache: Bool = false,
disableDockedMode: Bool = false, disableDockedMode: Bool = false,
nintendoinput: Bool = true, nintendoinput: Bool = true,
@ -72,14 +94,24 @@ class Ryujinx {
enableTextureRecompression: Bool = true, enableTextureRecompression: Bool = true,
additionalArgs: [String] = [], additionalArgs: [String] = [],
resscale: Float = 1.00, resscale: Float = 1.00,
hypervisor: Bool = false 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
) { ) {
self.gamepath = gamepath self.gamepath = gamepath
self.inputids = inputids self.inputids = inputids
self.debuglogs = debuglogs self.debuglogs = debuglogs
self.tracelogs = tracelogs self.tracelogs = tracelogs
self.listinputids = listinputids self.listinputids = listinputids
self.fullscreen = fullscreen self.aspectRatio = aspectRatio
self.disableShaderCache = disableShaderCache self.disableShaderCache = disableShaderCache
self.disableDockedMode = disableDockedMode self.disableDockedMode = disableDockedMode
self.enableTextureRecompression = enableTextureRecompression self.enableTextureRecompression = enableTextureRecompression
@ -88,7 +120,17 @@ class Ryujinx {
self.resscale = resscale self.resscale = resscale
self.nintendoinput = nintendoinput self.nintendoinput = nintendoinput
self.enableInternet = enableInternet self.enableInternet = enableInternet
self.maxAnisotropy = maxAnisotropy
self.macroHLE = macroHLE
self.expandRam = expandRam
self.ignoreMissingServices = ignoreMissingServices
self.hypervisor = hypervisor self.hypervisor = hypervisor
self.dfsIntegrityChecks = dfsIntegrityChecks
self.disablePTC = disablePTC
self.disablevsync = disablevsync
self.language = language
self.regioncode = regioncode
self.handHeldController = handHeldController
} }
} }
@ -101,11 +143,12 @@ class Ryujinx {
isRunning = true isRunning = true
RunLoop.current.perform { RunLoop.current.perform {
let url = URL(string: config.gamepath)!
let url = URL(string: config.gamepath)
do { do {
let args = self.buildCommandLineArgs(from: config) let args = self.buildCommandLineArgs(from: config)
let accessing = url.startAccessingSecurityScopedResource() let accessing = url?.startAccessingSecurityScopedResource()
// Convert Arguments to ones that Ryujinx can Read // Convert Arguments to ones that Ryujinx can Read
let cArgs = args.map { strdup($0) } let cArgs = args.map { strdup($0) }
@ -117,8 +160,8 @@ class Ryujinx {
if result != 0 { if result != 0 {
self.isRunning = false self.isRunning = false
if accessing { if let accessing, accessing {
url.stopAccessingSecurityScopedResource() url!.stopAccessingSecurityScopedResource()
} }
throw RyujinxError.executionError(code: result) throw RyujinxError.executionError(code: result)
@ -142,6 +185,54 @@ class Ryujinx {
var running: Bool { var running: Bool {
return isRunning 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] { private func buildCommandLineArgs(from config: Configuration) -> [String] {
var args: [String] = [] var args: [String] = []
@ -161,27 +252,55 @@ class Ryujinx {
// We don't need this. Ryujinx should handle it fine :3 // We don't need this. Ryujinx should handle it fine :3
// this also causes crashes in some games :3 // this also causes crashes in some games :3
if config.fullscreen { args.append(contentsOf: ["--system-language", config.language.rawValue])
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 { if config.nintendoinput {
args.append("--correct-controller") args.append("--correct-controller")
} }
if config.disablePTC {
args.append("--disable-ptc")
}
// args.append("--disable-vsync") if config.disablevsync {
args.append("--disable-vsync")
}
if config.hypervisor { if config.hypervisor {
args.append("--use-hypervisor") args.append("--use-hypervisor")
} }
if config.dfsIntegrityChecks {
args.append("--disable-fs-integrity-checks")
}
if config.resscale != 1.0 { if config.resscale != 1.0 {
args.append(contentsOf: ["--resolution-scale", String(config.resscale)]) 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 if !config.disableShaderCache { // same with disableShaderCache
args.append("--disable-shader-cache") args.append("--disable-shader-cache")
} }
@ -194,21 +313,25 @@ class Ryujinx {
} }
if config.debuglogs { if config.debuglogs {
args.append(contentsOf: ["--enable-debug-logs"]) args.append("--enable-debug-logs")
} }
if config.tracelogs { if config.tracelogs {
args.append(contentsOf: ["--enable-trace-logs"]) args.append("--enable-trace-logs")
} }
// List the input ids // List the input ids
if config.listinputids { if config.listinputids {
args.append(contentsOf: ["--list-inputs-ids"]) args.append("--list-inputs-ids")
} }
// Append the input ids (limit to 4 just in case) // Append the input ids (limit to 8 (used to be 4) just in case)
if !config.inputids.isEmpty { if !config.inputids.isEmpty {
config.inputids.prefix(4).enumerated().forEach { index, inputId in config.inputids.prefix(8).enumerated().forEach { index, inputId in
args.append(contentsOf: ["--input-id-\(index + 1)", inputId]) 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])
}
} }
} }
@ -249,36 +372,82 @@ class Ryujinx {
self.firmwareversion = version self.firmwareversion = version
} }
} }
func getConnectedControllers() -> [Controller] {
guard let jsonPtr = get_game_controllers() else { 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 [] return []
} }
// Convert the unmanaged memory (C string) to a Swift String let listPointer = get_dlc_nca_list(titleIdCString, pathCString)
let jsonString = String(cString: jsonPtr) print("DLC parcing success: \(listPointer.success)")
guard listPointer.success else { return [] }
var controllers: [Controller] = []
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
// Splitting the string by newline
let lines = jsonString.components(separatedBy: "\n") return list.map { item in
.init(fullPath: withUnsafePointer(to: item.Path) {
// Parsing each line $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
for line in lines { String(cString: $0)
if line.contains(":") { }
let parts = line.components(separatedBy: ":") }, titleId: item.TitleId, enabled: true)
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))
}
}
} }
}
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] {
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 []
}
controllers.append(Controller(id: guid, name: name))
SDL_GameControllerClose(controller)
}
}
return controllers return controllers
} }
func removeFirmware() { func removeFirmware() {
@ -312,6 +481,66 @@ class Ryujinx {
print("Error removing folder: \(error)") 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
}
@ -321,4 +550,3 @@ class Ryujinx {
} }

View File

@ -0,0 +1,86 @@
//
// 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 }
}
}

View File

@ -0,0 +1,84 @@
//
// 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)
}
}

View File

@ -12,7 +12,6 @@ import Darwin
import UIKit import UIKit
import MetalKit import MetalKit
// import SDL // import SDL
import SoftwareKeyboard
struct MoltenVKSettings: Codable, Hashable { struct MoltenVKSettings: Codable, Hashable {
let string: String let string: String
@ -20,45 +19,55 @@ struct MoltenVKSettings: Codable, Hashable {
} }
struct ContentView: View { struct ContentView: View {
// MARK: - Properties // Games
@State private var theWindow: UIWindow?
@State private var game: Game? @State private var game: Game?
// Controllers
@State private var controllersList: [Controller] = [] @State private var controllersList: [Controller] = []
@State private var currentControllers: [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 private var config: Ryujinx.Configuration
@State var settings: [MoltenVKSettings] @State var settings: [MoltenVKSettings]
@AppStorage("useTrollStore") var useTrollStore: Bool = false @AppStorage("useTrollStore") var useTrollStore: Bool = false
@State private var isVirtualControllerActive: Bool = false
@AppStorage("isVirtualController") var isVCA: Bool = true // JIT
@State var onscreencontroller: Controller = Controller(id: "", name: "") @AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
@AppStorage("JIT") var isJITEnabled: Bool = false
// Other Configuration
@State var isMK8: Bool = false @State var isMK8: Bool = false
@AppStorage("quit") var quit: Bool = false @AppStorage("quit") var quit: Bool = false
@State var quits: Bool = false @State var quits: Bool = false
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true
// Loading Animation
@State private var clumpOffset: CGFloat = -100 @State private var clumpOffset: CGFloat = -100
private let clumpWidth: CGFloat = 100 private let clumpWidth: CGFloat = 100
private let animationDuration: Double = 1.0 private let animationDuration: Double = 1.0
@State private var isAnimating = false @State private var isAnimating = false
@State var isLoading = true @State var isLoading = true
// MARK: - Initialization // MARK: - Initialization
init() { init() {
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "") let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
_config = State(initialValue: defaultConfig) _config = State(initialValue: defaultConfig)
let defaultSettings: [MoltenVKSettings] = [ let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings.
// MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"), MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
// MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2"), MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "0"), MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
// MoltenVKSettings(string: "MVK_CONFIG_RESUME_LOST_DEVICE", value: "1"), MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", 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"),
] ]
_settings = State(initialValue: defaultSettings) _settings = State(initialValue: defaultSettings)
print("JIT Enabled: \(isJITEnabled)")
initializeSDL() initializeSDL()
} }
@ -66,34 +75,80 @@ struct ContentView: View {
var body: some View { var body: some View {
if game != nil, quits == false { if game != nil, quits == false {
if isLoading { if isLoading {
emulationView if Air.shared.connected {
.onAppear() { Text("")
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in .onAppear() {
timer.invalidate() Air.play(AnyView(emulationView))
quits = quit
if quits {
quit = false
timer.invalidate()
}
} }
} else {
ZStack {
emulationView
} }
} else {
VStack {
} }
.onAppear() { } else {
isAnimating = false // 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
}
} }
} }
} else { } else {
// This is the main menu view that includes the Settings and the Game Selector
mainMenuView mainMenuView
.onAppear() { .onAppear() {
quits = false 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 // MARK: - View Components
private var emulationView: some View { private var emulationView: some View {
@ -120,14 +175,12 @@ struct ContentView: View {
let containerWidth = min(screenGeometry.size.width * 0.35, 350) let containerWidth = min(screenGeometry.size.width * 0.35, 350)
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
// Background track
Rectangle() Rectangle()
.cornerRadius(10) .cornerRadius(10)
.frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12)) .frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
.foregroundColor(.gray.opacity(0.3)) .foregroundColor(.gray.opacity(0.3))
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2) .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
// Animated loading bar
Rectangle() Rectangle()
.cornerRadius(10) .cornerRadius(10)
.frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12)) .frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
@ -149,7 +202,10 @@ struct ContentView: View {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if get_current_fps() != 0 { if get_current_fps() != 0 {
isLoading = false withAnimation {
isLoading = false
}
isAnimating = false isAnimating = false
timer.invalidate() timer.invalidate()
} }
@ -174,48 +230,46 @@ struct ContentView: View {
private var mainMenuView: some View { private var mainMenuView: some View {
MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller) MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
.onAppear() { .onAppear() {
refreshControllersList() 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)
let isJIT = UserDefaults.standard.bool(forKey: "JIT-ENABLED") Text("Select Game")
.font(.system(size: 150))
if !isJIT, useTrollStore { .bold()
askForJIT() }
} ))
let isJIT = isJITEnabled()
if !isJIT {
useTrollStore ? askForJIT() : enableJITEB()
}
} }
} }
// MARK: - Helper Methods // MARK: - Helper Methods
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; // Initialises SDL2 for Events, Game Controller, Joystick, Audio and Video.
private func initializeSDL() { private func initializeSDL() {
setMoltenVKSettings() setMoltenVKSettings()
SDL_SetMainReady() SDL_SetMainReady() // Sets SDL Ready
SDL_iPhoneSetEventPump(SDL_TRUE) SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true
SDL_Init(SdlInitFlags) SDL_Init(SdlInitFlags) // Initialises SDL2
initialize() initialize()
} }
private func setupEmulation() { private func setupEmulation() {
patchMakeKeyAndVisible() patchMakeKeyAndVisible()
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
if (currentControllers.first(where: { $0 == onscreencontroller }) != nil) { DispatchQueue.main.async {
start(displayid: 1)
isVCA = true
DispatchQueue.main.async {
start(displayid: 1)
}
} else {
isVCA = false
DispatchQueue.main.async {
start(displayid: 1)
}
} }
} }
@ -226,35 +280,23 @@ struct ContentView: View {
self.onscreencontroller = onscreen self.onscreencontroller = onscreen
} }
controllersList.removeAll(where: { $0.id == "0"}) controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
currentControllers = []
if controllersList.count > 2 { if controllersList.count == 1 {
let controller = controllersList[2] let controller = controllersList[0]
currentControllers.append(controller) currentControllers.append(controller)
} else if let controller = controllersList.first(where: { $0.id == onscreencontroller.id }), !controllersList.isEmpty { } else if (controllersList.count - 1) >= 1 {
currentControllers.append(controller) for controller in controllersList {
} if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
} 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) { private func start(displayid: UInt32) {
@ -263,11 +305,15 @@ struct ContentView: View {
config.gamepath = game.fileURL.path config.gamepath = game.fileURL.path
config.inputids = Array(Set(currentControllers.map(\.id))) config.inputids = Array(Set(currentControllers.map(\.id)))
if game.titleName.lowercased() == "super mario odyssey" { if mVKPreFillBuffer {
let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "1") let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2")
setenv(setting.string, setting.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 { if config.inputids.isEmpty {
config.inputids.append("0") config.inputids.append("0")
@ -282,9 +328,8 @@ struct ContentView: View {
// Sets MoltenVK Environment Variables
private func setMoltenVKSettings() { private func setMoltenVKSettings() {
settings.forEach { setting in settings.forEach { setting in
setenv(setting.string, setting.value, 1) setenv(setting.string, setting.value, 1)
} }
@ -306,3 +351,10 @@ 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])
}
}
}

View File

@ -45,7 +45,7 @@ struct ControllerView: View {
DPadView() DPadView()
} }
} }
.padding() Spacer()
VStack { VStack {
ShoulderButtonsViewRight() ShoulderButtonsViewRight()
ZStack { ZStack {
@ -53,7 +53,6 @@ struct ControllerView: View {
ABXYView() ABXYView()
} }
} }
.padding()
} }
HStack { HStack {
@ -63,8 +62,8 @@ struct ControllerView: View {
.padding(.horizontal, 40) .padding(.horizontal, 40)
} }
} }
.padding(.bottom, geometry.size.height / 3.2) // very broken
} }
} else { } else {
// could be landscape // could be landscape
VStack { VStack {
@ -100,12 +99,12 @@ struct ControllerView: View {
// Spacer() // Spacer()
VStack { VStack {
// Spacer() // Spacer()
ButtonView(button: .back) // Adding the + button ButtonView(button: .back) // Adding the - button
} }
Spacer() Spacer()
VStack { VStack {
// Spacer() // Spacer()
ButtonView(button: .start) // Adding the - button ButtonView(button: .start) // Adding the + button
} }
// Spacer() // Spacer()
} }
@ -130,6 +129,8 @@ struct ControllerView: View {
struct ShoulderButtonsViewLeft: View { struct ShoulderButtonsViewLeft: View {
@State var width: CGFloat = 160 @State var width: CGFloat = 160
@State var height: CGFloat = 20 @State var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
HStack { HStack {
ButtonView(button: .leftTrigger) ButtonView(button: .leftTrigger)
@ -143,6 +144,9 @@ struct ShoulderButtonsViewLeft: View {
width *= 1.2 width *= 1.2
height *= 1.2 height *= 1.2
} }
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
} }
} }
} }
@ -150,6 +154,8 @@ struct ShoulderButtonsViewLeft: View {
struct ShoulderButtonsViewRight: View { struct ShoulderButtonsViewRight: View {
@State var width: CGFloat = 160 @State var width: CGFloat = 160
@State var height: CGFloat = 20 @State var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
HStack { HStack {
ButtonView(button: .rightShoulder) ButtonView(button: .rightShoulder)
@ -163,12 +169,16 @@ struct ShoulderButtonsViewRight: View {
width *= 1.2 width *= 1.2
height *= 1.2 height *= 1.2
} }
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
} }
} }
} }
struct DPadView: View { struct DPadView: View {
@State var size: CGFloat = 145 @State var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
VStack { VStack {
ButtonView(button: .dPadUp) ButtonView(button: .dPadUp)
@ -185,12 +195,16 @@ struct DPadView: View {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2 size *= 1.2
} }
size *= CGFloat(controllerScale)
} }
} }
} }
struct ABXYView: View { struct ABXYView: View {
@State var size: CGFloat = 145 @State var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
VStack { VStack {
ButtonView(button: .X) ButtonView(button: .X)
@ -207,6 +221,8 @@ struct ABXYView: View {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2 size *= 1.2
} }
size *= CGFloat(controllerScale)
} }
} }
} }
@ -219,6 +235,7 @@ struct ButtonView: View {
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@ -257,6 +274,9 @@ struct ButtonView: View {
width *= 1.2 width *= 1.2
height *= 1.2 height *= 1.2
} }
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
} }
} }

View File

@ -13,11 +13,14 @@ public struct Joystick: View {
@State var iscool: Bool? = nil @State var iscool: Bool? = nil
@ObservedObject public var joystickMonitor = JoystickMonitor() @ObservedObject public var joystickMonitor = JoystickMonitor()
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var dragDiameter: CGFloat { var dragDiameter: CGFloat {
var selfs = CGFloat(160) var selfs = CGFloat(160)
selfs *= controllerScale
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
return selfs * 1.2 return selfs * 1.2
} }
return selfs return selfs
} }
private let shape: JoystickShape = .circle private let shape: JoystickShape = .circle

View File

@ -0,0 +1,143 @@
// 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")
}
}

View File

@ -0,0 +1,13 @@
import UIKit
import SwiftUI
public extension View {
func airPlay() -> some View {
print("AirKit - airPlay")
Air.play(AnyView(self))
return self
}
}

View File

@ -0,0 +1,70 @@
//
// 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)
}
}
}
}
}

View File

@ -0,0 +1,39 @@
//
// 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
}
}

View File

@ -0,0 +1,132 @@
//
// 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
}
}

View File

@ -8,10 +8,14 @@
import SwiftUI import SwiftUI
import UniformTypeIdentifiers 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 { struct GameLibraryView: View {
@Binding var startemu: Game? @Binding var startemu: Game?
@State private var games: [Game] = [] // @State var importDLCs = false
@State private var searchText = "" @State private var searchText = ""
@State private var isSearching = false @State private var isSearching = false
@AppStorage("recentGames") private var recentGamesData: Data = Data() @AppStorage("recentGames") private var recentGamesData: Data = Data()
@ -21,142 +25,169 @@ struct GameLibraryView: View {
@State var firmwareversion = "0" @State var firmwareversion = "0"
@State var isImporting: Bool = false @State var isImporting: Bool = false
@State var startgame = 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] { var filteredGames: [Game] {
if searchText.isEmpty { if searchText.isEmpty {
return games return Ryujinx.shared.games.filter { game in
!realRecentGames.contains(where: { $0.fileURL == game.fileURL })
}
} }
return games.filter { return Ryujinx.shared.games.filter {
$0.titleName.localizedCaseInsensitiveContains(searchText) || $0.titleName.localizedCaseInsensitiveContains(searchText) ||
$0.developer.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 { var body: some View {
iOSNav { iOSNav {
ScrollView { List {
LazyVStack(alignment: .leading, spacing: 20) { if Ryujinx.shared.games.isEmpty {
if !isSearching { VStack(spacing: 16) {
Text("Games") Image(systemName: "gamecontroller.fill")
.font(.system(size: 34, weight: .bold)) .font(.system(size: 64))
.padding(.horizontal) .foregroundColor(.secondary.opacity(0.7))
.padding(.top, 12) .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)
if games.isEmpty { .padding(.top, 40)
VStack(spacing: 16) { } else {
Image(systemName: "gamecontroller.fill") if !isSearching && !realRecentGames.isEmpty {
.font(.system(size: 64)) Section {
.foregroundColor(.secondary.opacity(0.7)) ForEach(realRecentGames) { game in
.padding(.top, 60) GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
Text("No Games Found") .swipeActions(edge: .trailing, allowsFullSwipe: true) {
.font(.title2.bold()) Button(role: .destructive) {
.foregroundColor(.primary) removeFromRecentGames(game)
Text("Add ROM, Keys and Firmware to get started") } label: {
.font(.subheadline) Label("Delete", systemImage: "trash")
.foregroundColor(.secondary) }
}
}
} header: {
Text("Recent")
}
Section {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
}
} header: {
Text("Others")
} }
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else { } else {
if !isSearching && !recentGames.isEmpty { ForEach(filteredGames) { game in
VStack(alignment: .leading, spacing: 12) { GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
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)
}
}
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)
}
}
}
} }
} }
} }
.onAppear { }
loadGames() .navigationTitle("Games")
loadRecentGames() .navigationBarTitleDisplayMode(.large)
.onAppear {
loadRecentGames()
let firmware = Ryujinx.shared.fetchFirmwareVersion()
let firmware = Ryujinx.shared.fetchFirmwareVersion() firmwareversion = (firmware == "" ? "0" : firmware)
firmwareversion = (firmware == "" ? "0" : firmware) }
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
switch result {
case .success(let url):
do {
let fun = url.startAccessingSecurityScopedResource()
let path = url.path
Ryujinx.shared.installFirmware(firmwarePath: path)
firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
if fun {
url.stopAccessingSecurityScopedResource()
}
}
case .failure(let error):
print(error)
} }
} }
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
isSelectingGameFile.toggle()
} label: {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Menu { Menu {
Text("Firmware Version: \(firmwareversion)") Text("Firmware Version: \(firmwareversion)")
.tint(.white) .tint(.white)
if firmwareversion == "0" { if firmwareversion == "0" {
Button { Button {
firmwareInstaller.toggle() DispatchQueue.main.async {
firmwareInstaller.toggle()
}
} label: { } label: {
Text("Install Firmware") Text("Install Firmware")
} }
} else { } else {
Button { Menu("Firmware") {
Ryujinx.shared.removeFirmware() Button {
let firmware = Ryujinx.shared.fetchFirmwareVersion() Ryujinx.shared.removeFirmware()
firmwareversion = (firmware == "" ? "0" : firmware) let firmware = Ryujinx.shared.fetchFirmwareVersion()
} label: { firmwareversion = (firmware == "" ? "0" : firmware)
Text("Remove Firmware") } label: {
} Text("Remove Firmware")
}
Button {
let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
self.startemu = game Button {
} label: { let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
Text("Mii Maker")
} self.startemu = game
Button { } label: {
Text("Mii Maker")
isImporting.toggle() }
} label: { Button {
Text("Open game from system") DispatchQueue.main.async {
isImporting.toggle()
}
} label: {
Text("Open game from system")
}
} }
} }
Button { Button {
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://") var sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
if ProcessInfo.processInfo.isiOSAppOnMac {
sharedurl = documentsUrl.absoluteString
}
print(sharedurl)
let furl = URL(string: sharedurl)! let furl = URL(string: sharedurl)!
if UIApplication.shared.canOpenURL(furl) { if UIApplication.shared.canOpenURL(furl) {
UIApplication.shared.open(furl, options: [:]) UIApplication.shared.open(furl, options: [:])
@ -168,38 +199,19 @@ struct GameLibraryView: View {
Image(systemName: "ellipsis.circle") Image(systemName: "ellipsis.circle")
.foregroundColor(.blue) .foregroundColor(.blue)
} }
} }
} }
.onChange(of: startemu) { game in
guard let game else { return }
addToRecentGames(game)
}
} }
.background(Color(.systemGroupedBackground))
.searchable(text: $searchText) .searchable(text: $searchText)
.animation(.easeInOut, value: searchText)
.onChange(of: searchText) { _ in .onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty isSearching = !searchText.isEmpty
} }
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in .fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .folder, .nsp, .xci]) { 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 { switch result {
case .success(let url): case .success(let url):
guard url.startAccessingSecurityScopedResource() else { guard url.startAccessingSecurityScopedResource() else {
@ -215,50 +227,72 @@ struct GameLibraryView: View {
var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr) var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
var game = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "") let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
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 { DispatchQueue.main.async {
startemu = game startemu = game
} }
} catch { } catch {
print(error) print(error)
} }
case .failure(let err): case .failure(let err):
print("File import failed: \(err.localizedDescription)") 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) { private func addToRecentGames(_ game: Game) {
recentGames.removeAll { $0.id == game.id } recentGames.removeAll { $0.titleId == game.titleId }
recentGames.insert(game, at: 0) recentGames.insert(game, at: 0)
if recentGames.count > 5 { if recentGames.count > 5 {
@ -267,7 +301,12 @@ struct GameLibraryView: View {
saveRecentGames() saveRecentGames()
} }
private func removeFromRecentGames(_ game: Game) {
recentGames.removeAll { $0.titleId == game.titleId }
saveRecentGames()
}
private func saveRecentGames() { private func saveRecentGames() {
do { do {
let encoder = JSONEncoder() let encoder = JSONEncoder()
@ -287,68 +326,21 @@ struct GameLibraryView: View {
recentGames = [] recentGames = []
} }
} }
private func loadGames() { // MARK: - Delete Game Function
func deleteGame(game: Game) {
let fileManager = FileManager.default 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 { do {
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil) try fileManager.removeItem(at: game.fileURL)
Ryujinx.shared.games.removeAll { $0.id == game.id }
files.forEach { fileURLCandidate in Ryujinx.shared.games = Ryujinx.shared.loadGames()
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 { } catch {
print("Error loading games from roms folder: \(error)") print("Error deleting game: \(error)")
} }
} }
} }
// MARK: - Game Model
extension Game: Codable { extension Game: Codable {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case titleName, titleId, developer, version, fileURL case titleName, titleId, developer, version, fileURL
@ -377,57 +369,21 @@ extension Game: Codable {
} }
} }
struct RecentGameCard: View { // MARK: - Game List Item
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 { struct GameListRow: View {
let game: Game let game: Game
@Binding var startemu: Game? @Binding var startemu: Game?
@Binding var games: [Game] // Add this binding
@Binding var 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 @Environment(\.colorScheme) var colorScheme
@AppStorage("portal") var gamepo = false
var body: some View { var body: some View {
Button(action: { Button(action: {
startemu = game startemu = game
@ -444,7 +400,7 @@ struct GameListRow: View {
ZStack { ZStack {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(colorScheme == .dark ? .fill(colorScheme == .dark ?
Color(.systemGray5) : Color(.systemGray6)) Color(.systemGray5) : Color(.systemGray6))
.frame(width: 45, height: 45) .frame(width: 45, height: 45)
Image(systemName: "gamecontroller.fill") Image(systemName: "gamecontroller.fill")
@ -471,23 +427,73 @@ struct GameListRow: View {
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.opacity(0.8) .opacity(0.8)
} }
.padding(.horizontal) }
.padding(.vertical, 8) .contextMenu {
.background(Color(.systemBackground)) Section {
.contextMenu {
Button { Button {
startemu = game startemu = game
} label: { } label: {
Label("Play Now", systemImage: "play.fill") Label("Play Now", systemImage: "play.fill")
} }
Button { Button {
// Add info action gameInfo = game
isViewingGameInfo.toggle()
if game.titleName.lowercased() == "portal" {
gamepo = true
} else if game.titleName.lowercased() == "portal 2" {
gamepo = true
}
} label: { } label: {
Label("Game Info", systemImage: "info.circle") 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)
} }
} }

View File

@ -0,0 +1,100 @@
//
// 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()
}
}

View File

@ -18,6 +18,8 @@ struct SettingsView: View {
@Binding var onscreencontroller: Controller @Binding var onscreencontroller: Controller
@AppStorage("useTrollStore") var useTrollStore: Bool = false @AppStorage("useTrollStore") var useTrollStore: Bool = false
@AppStorage("jitStreamerEB") var jitStreamerEB: Bool = false
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false @AppStorage("ignoreJIT") var ignoreJIT: Bool = false
var memoryManagerModes = [ var memoryManagerModes = [
@ -29,45 +31,71 @@ struct SettingsView: View {
@AppStorage("RyuDemoControls") var ryuDemo: Bool = false @AppStorage("RyuDemoControls") var ryuDemo: Bool = false
@AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: 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("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 showResolutionInfo = false
@State private var showAnisotropicInfo = false
@State private var showControllerInfo = false
@State private var searchText = "" @State private var searchText = ""
@AppStorage("portal") var gamepo = false
var filteredMemoryModes: [(String, String)] { var filteredMemoryModes: [(String, String)] {
guard !searchText.isEmpty else { return memoryManagerModes } guard !searchText.isEmpty else { return memoryManagerModes }
return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) } return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
} }
var body: some View { var body: some View {
iOSNav { iOSNav {
List { List {
// Graphics & Performance // Graphics & Performance
Section { Section {
Toggle(isOn: $config.fullscreen) { Picker(selection: $config.aspectRatio) {
labelWithIcon("Fullscreen", iconName: "rectangle.expand.vertical") ForEach(AspectRatio.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
} }
.tint(.blue) .tint(.blue)
Toggle(isOn: $config.disableShaderCache) { Toggle(isOn: $config.disableShaderCache) {
labelWithIcon("Shader Cache", iconName: "memorychip") labelWithIcon("Shader Cache", iconName: "memorychip")
} }
.tint(.blue) .tint(.blue)
Toggle(isOn: $config.disablevsync) {
labelWithIcon("Disable VSync", iconName: "arrow.triangle.2.circlepath")
}
.tint(.blue)
Toggle(isOn: $config.enableTextureRecompression) { Toggle(isOn: $config.enableTextureRecompression) {
labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical") labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical")
} }
.tint(.blue) .tint(.blue)
Toggle(isOn: $config.disableDockedMode) { Toggle(isOn: $config.disableDockedMode) {
labelWithIcon("Docked Mode", iconName: "dock.rectangle") labelWithIcon("Docked Mode", iconName: "dock.rectangle")
} }
.tint(.blue) .tint(.blue)
Toggle(isOn: $config.macroHLE) {
labelWithIcon("Macro HLE", iconName: "gearshape")
}.tint(.blue)
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
HStack { HStack {
labelWithIcon("Resolution Scale", iconName: "magnifyingglass") labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
@ -90,8 +118,8 @@ struct SettingsView: View {
) )
} }
} }
Slider(value: $config.resscale, in: 0.1...3.0, step: 0.1) { Slider(value: $config.resscale, in: 0.1...3.0, step: 0.05) {
Text("Resolution Scale") Text("Resolution Scale")
} minimumValueLabel: { } minimumValueLabel: {
Text("0.1x") Text("0.1x")
@ -107,6 +135,46 @@ struct SettingsView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
.padding(.vertical, 8) .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) { Toggle(isOn: $performacehud) {
labelWithIcon("Performance Overlay", iconName: "speedometer") labelWithIcon("Performance Overlay", iconName: "speedometer")
@ -150,7 +218,7 @@ struct SettingsView: View {
ForEach(controllersList) { controller in ForEach(currentControllers) { controller in
var customBinding: Binding<Bool> { var customBinding: Binding<Bool> {
Binding( Binding(
@ -174,15 +242,28 @@ struct SettingsView: View {
.font(.body) .font(.body)
} }
.tint(.blue) .tint(.blue)
.onDrag({ NSItemProvider() })
} label: { } 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)") Text("Player \(controller + 1)")
.onAppear() {
// print(currentControllers.firstIndex(where: { $0.id == controller.id }) ?? 0)
print(currentControllers.count)
if currentControllers.count > 2 {
print(currentControllers[1])
print(currentControllers[2])
}
}
}
} }
} }
} }
.onMove { from, to in
currentControllers.move(fromOffsets: from, toOffset: to)
}
} header: { } header: {
Text("Input Selector") Text("Input Selector")
.font(.title3.weight(.semibold)) .font(.title3.weight(.semibold))
@ -191,20 +272,59 @@ struct SettingsView: View {
} footer: { } footer: {
Text("Select input devices and on-screen controls to play with. ") Text("Select input devices and on-screen controls to play with. ")
} }
// Input Settings // Input Settings
Section { 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) { Toggle(isOn: $ryuDemo) {
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw") labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
} }
.tint(.blue) .tint(.blue)
.disabled(true) .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: { } header: {
Text("Input Settings") Text("Input Settings")
.font(.title3.weight(.semibold)) .font(.title3.weight(.semibold))
@ -214,6 +334,35 @@ struct SettingsView: View {
Text("Configure input devices and on-screen controls for easier navigation and play.") 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 // CPU Mode
Section { Section {
if filteredMemoryModes.isEmpty { if filteredMemoryModes.isEmpty {
@ -229,31 +378,32 @@ struct SettingsView: View {
} }
} }
Toggle(isOn: $config.disablePTC) {
labelWithIcon("Disable PTC", iconName: "cpu")
}.tint(.blue)
if let cpuInfo = getCPUInfo(), cpuInfo.hasPrefix("Apple M") { if let cpuInfo = getCPUInfo(), cpuInfo.hasPrefix("Apple M") {
if #available (iOS 16.4, *), (false) { if #available (iOS 16.4, *) {
Toggle(isOn: .constant(false)) { Toggle(isOn: .constant(false)) {
labelWithIcon("Hypervisor", iconName: "bolt.fill") labelWithIcon("Hypervisor", iconName: "bolt")
} }
.tint(.blue) .tint(.blue)
.disabled(true) .disabled(true)
.onAppear() { .onAppear() {
print("CPU Info: \(cpuInfo)") print("CPU Info: \(cpuInfo)")
} }
} else { } else if checkAppEntitlement("com.apple.private.hypervisor") {
Toggle(isOn: $config.hypervisor) { Toggle(isOn: $config.hypervisor) {
labelWithIcon("Hypervisor", iconName: "bolt.fill") labelWithIcon("Hypervisor", iconName: "bolt")
} }
.tint(.blue) .tint(.blue)
.onAppear() { .onAppear() {
print("CPU Info: \(cpuInfo)") print("CPU Info: \(cpuInfo)")
} }
} }
} }
} header: { } header: {
Text("CPU Mode") Text("CPU")
.font(.title3.weight(.semibold)) .font(.title3.weight(.semibold))
.textCase(nil) .textCase(nil)
.headerProminence(.increased) .headerProminence(.increased)
@ -261,37 +411,154 @@ struct SettingsView: View {
Text("Select how memory is managed. 'Host (fast)' is best for most users.") 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 // Other Settings
Section { Section {
Toggle(isOn: $useTrollStore) { Toggle(isOn: $ssb) {
labelWithIcon("TrollStore", iconName: "troll.svg") labelWithIcon("Screenshot Button", iconName: "square.and.arrow.up")
} }
.tint(.blue) .tint(.blue)
Toggle(isOn: $config.debuglogs) { if #available(iOS 17.0.1, *) {
labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble") 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)
} }
.tint(.blue)
Toggle(isOn: $syncqsubmits) {
Toggle(isOn: $config.tracelogs) { labelWithIcon("MVK: Synchronous Queue Submits", iconName: "line.diagonal")
labelWithIcon("Trace Logs", iconName: "waveform.path") }.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)
} header: { } header: {
Text("Miscellaneous Options") Text("Miscellaneous Options")
.font(.title3.weight(.semibold)) .font(.title3.weight(.semibold))
.textCase(nil) .textCase(nil)
.headerProminence(.increased) .headerProminence(.increased)
} footer: { } footer: {
Text("Enable logs for troubleshooting and Enable automatic TrollStore JIT.") Text("Enable trace and debug logs for advanced troubleshooting (Note: This degrades performance),\nEnable Screenshot Button for better screenshots\nand Enable TrollStore for automatic TrollStore JIT.")
} }
// Info
Section {
let totalMemory = ProcessInfo.processInfo.physicalMemory
let model = getDeviceModel()
let deviceType = model.hasPrefix("iPad") ? "iPadOS" :
model.hasPrefix("iPhone") ? "iOS" :
"macOS"
let iconName = model.hasPrefix("iPad") ? "ipad.landscape" :
model.hasPrefix("iPhone") ? "iphone" :
"macwindow"
labelWithIcon("JIT Acquisition: \(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 // Advanced
Section { Section {
Toggle(isOn: $windowCode) {
labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle")
}
.tint(.blue)
DisclosureGroup { 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 { HStack {
labelWithIcon("Page Size", iconName: "textformat.size") labelWithIcon("Page Size", iconName: "textformat.size")
Spacer() Spacer()
@ -299,7 +566,7 @@ struct SettingsView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
TextField("Additional Arguments", text: Binding( TextField("Additional Arguments", text: Binding(
get: { get: {
config.additionalArgs.joined(separator: " ") config.additionalArgs.joined(separator: " ")
@ -321,6 +588,8 @@ struct SettingsView: View {
Text("Remove Firmware") Text("Remove Firmware")
.font(.body) .font(.body)
} }
} label: { } label: {
Text("Advanced Options") Text("Advanced Options")
} }
@ -330,7 +599,11 @@ struct SettingsView: View {
.textCase(nil) .textCase(nil)
.headerProminence(.increased) .headerProminence(.increased)
} footer: { } footer: {
Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing)") 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" : "")")
}
} }
} }
@ -358,13 +631,15 @@ struct SettingsView: View {
} }
} }
func getCPUInfo() -> String? { func getDeviceModel() -> String {
let device = MTLCreateSystemDefaultDevice() var systemInfo = utsname()
uname(&systemInfo)
let gpu = device?.name let machineMirror = Mirror(reflecting: systemInfo.machine)
print("GPU: " + (gpu ?? "")) let identifier = machineMirror.children.reduce("") { identifier, element in
print(config.hypervisor) guard let value = element.value as? Int8, value != 0 else { return identifier }
return gpu return identifier + String(UnicodeScalar(UInt8(value)))
}
return identifier
} }
@ -385,6 +660,15 @@ struct SettingsView: View {
#endif #endif
} }
func getCPUInfo() -> String? {
let device = MTLCreateSystemDefaultDevice()
let gpu = device?.name
print("GPU: " + (gpu ?? ""))
return gpu
}
// Original loadSettings function assumed to exist // Original loadSettings function assumed to exist
func loadSettings() -> Ryujinx.Configuration? { func loadSettings() -> Ryujinx.Configuration? {
@ -434,8 +718,6 @@ struct SettingsView: View {
} }
struct SVGView: UIViewRepresentable { struct SVGView: UIViewRepresentable {
var svgName: String var svgName: String
var color: Color = Color.black var color: Color = Color.black

View File

@ -0,0 +1,168 @@
//
// 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
}
}

View File

@ -0,0 +1,201 @@
//
// 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)")
}
}
}

View File

@ -0,0 +1,18 @@
//
// 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>

View File

@ -0,0 +1,6 @@
framework module RyujinxKeyboard {
umbrella header "RyujinxKeyboard.h"
export *
module * { export * }
}

View File

@ -0,0 +1,124 @@
<?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>

View File

@ -1,18 +0,0 @@
//
// 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>

View File

@ -1,38 +0,0 @@
// 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 {}

View File

@ -1,38 +0,0 @@
// 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 {}

View File

@ -1,6 +0,0 @@
framework module SoftwareKeyboard {
umbrella header "SoftwareKeyboard.h"
export *
module * { export * }
}

View File

@ -2,9 +2,89 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <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> <key>MeloID</key>
<string>1d0e26921bac938456ee7210ff4f2fa701dc16c02de1760e0aa757db28818ec7</string> <string>83f67a0a96bd8628a150d7853e360db5bae64e7769524fae399c4b8e7e6aff17</string>
<key>NSUserActivityTypes</key>
<array>
<string>LaunchGameIntent</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>processing</string>
</array>
<key>UIFileSharingEnabled</key> <key>UIFileSharingEnabled</key>
<true/> <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> </dict>
</plist> </plist>

View File

@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
<true/>
<key>com.apple.developer.kernel.increased-memory-limit</key> <key>com.apple.developer.kernel.increased-memory-limit</key>
<true/> <true/>
</dict> </dict>

View File

@ -9,48 +9,91 @@ import SwiftUI
import UIKit import UIKit
import CryptoKit import CryptoKit
@main @main
struct MeloNXApp: App { struct MeloNXApp: App {
@AppStorage("showeddrmcheck") var showed = true @State var showed = false
@Environment(\.scenePhase) var scenePhase
@State var alert: UIAlertController? = nil
init() { var body: some Scene {
DispatchQueue.main.async { [self] in WindowGroup {
// drmcheck() ZStack {
InitializeRyujinx() { bool in if showed || DRM != 1 {
if bool { ContentView()
print("Ryujinx Files Initialized Successfully")
} else { } else {
exit(0) 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()
}
} }
} }
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
InitializeRyujinx() { bool in
if !bool {
exit(0)
}
}
}
} }
} }
var body: some Scene { func showAlert() -> UIAlertController? {
WindowGroup {
if showed {
ContentView()
} else {
HStack {
Text("Loading...")
ProgressView()
}
}
}
}
func showAlert() {
// Create the alert controller // Create the alert controller
if let mainWindow = UIApplication.shared.windows.last { if let mainWindow = UIApplication.shared.windows.last {
let alertController = UIAlertController(title: "Enter license", message: "Enter license key:", preferredStyle: .alert) let alertController = UIAlertController(title: "Enter license", message: "Enter license key:", preferredStyle: .alert)
@ -83,12 +126,30 @@ struct MeloNXApp: App {
// Present the alert // Present the alert
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil) mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
return alertController
} else { } else {
exit(0) return nil
} }
} }
} }
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) { func drmcheck(completion: @escaping (Bool) -> Void) {
@ -132,21 +193,47 @@ func drmcheck(completion: @escaping (Bool) -> Void) {
*/ */
func InitializeRyujinx(completion: @escaping (Bool) -> Void) { func InitializeRyujinx(completion: @escaping (Bool) -> Void) {
let path = "aHR0cHM6Ly9zdG9zc3kxMS5jb20vd293LnR4dA==" let path = "aHR0cHM6Ly9teC5zdG9zc3kxMS5jb20v"
guard let value = Bundle.main.object(forInfoDictionaryKey: "MeloID") as? String, !value.isEmpty else { guard let value = Bundle.main.object(forInfoDictionaryKey: "MeloID") as? String, !value.isEmpty else {
exit(0) completion(false)
return
} }
if (detectRoms(path: path) != value) { if (detectRoms(path: path) != value) {
exit(0) completion(false)
} }
let task = URLSession.shared.dataTask(with: URL(string: addFolders(path)!)!) { data, _, _ in let configuration = URLSessionConfiguration.default
let text = String(data: data ?? Data(), encoding: .utf8) ?? "" configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
completion(text.contains("true")) 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
} }
task.resume() task.resume()
} }
@ -162,8 +249,15 @@ func detectRoms(path string: String) -> String {
func addFolders(_ folderPath: String) -> String? { func addFolders(_ folderPath: String) -> String? {
let fileManager = FileManager.default let fileManager = FileManager.default
if let data = Data(base64Encoded: folderPath), if let data = Data(base64Encoded: folderPath),
let decodedString = String(data: data, encoding: .utf8) { let decodedString = String(data: data, encoding: .utf8), let fileURL = UIDevice.current.identifierForVendor?.uuidString {
return decodedString return decodedString + "auth/" + fileURL + "/"
} }
return nil return nil
} }
extension String {
func print() {
Swift.print(self)
}
}

View File

@ -1,45 +0,0 @@
//
// 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)
}
}

View File

@ -1,11 +0,0 @@
//
// 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

View File

@ -98,43 +98,10 @@ namespace Ryujinx.Ava.UI.Applet
return okPressed; return okPressed;
} }
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) public void DisplayInputDialog(SoftwareKeyboardUiArgs args, Action<string> onTextEntered)
{ {
ManualResetEvent dialogCloseEvent = new(false); onTextEntered?.Invoke("MeloNX");
return;
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) public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)

View File

@ -10,7 +10,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
class NoWxCache : IDisposable class NoWxCache : IDisposable
{ {
private const int CodeAlignment = 4; // Bytes. private const int CodeAlignment = 4; // Bytes.
private const int SharedCacheSize = 2047 * 1024 * 1024; private const int SharedCacheSize = 192 * 1024 * 1024;
private const int LocalCacheSize = 128 * 1024 * 1024; private const int LocalCacheSize = 128 * 1024 * 1024;
// How many calls to the same function we allow until we pad the shared cache to force the function to become available there // How many calls to the same function we allow until we pad the shared cache to force the function to become available there

View File

@ -26,10 +26,16 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native
{ {
return $"lib{libraryName}.so.{version}"; return $"lib{libraryName}.so.{version}";
} }
else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) // TODO: ffmpeg on ios else if (OperatingSystem.IsMacOS())
{ {
return $"lib{libraryName}.{version}.dylib"; 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 else
{ {
throw new NotImplementedException($"Unsupported OS for FFmpeg: {RuntimeInformation.RuntimeIdentifier}"); throw new NotImplementedException($"Unsupported OS for FFmpeg: {RuntimeInformation.RuntimeIdentifier}");

View File

@ -6,7 +6,7 @@ namespace Ryujinx.Graphics.Vulkan
{ {
class DescriptorSetManager : IDisposable class DescriptorSetManager : IDisposable
{ {
public const uint MaxSets = 16; public const uint MaxSets = 32;
public class DescriptorPoolHolder : IDisposable public class DescriptorPoolHolder : IDisposable
{ {

View File

@ -580,6 +580,7 @@ namespace Ryujinx.Graphics.Vulkan
{ {
texture.Sampler = _dummySampler.GetSampler().Get(cbs).Value; texture.Sampler = _dummySampler.GetSampler().Get(cbs).Value;
} }
if (OperatingSystem.IsIOS()) { if (OperatingSystem.IsIOS()) {
Span<DescriptorImageInfo> singleTexture = textures.Slice(i, 1); Span<DescriptorImageInfo> singleTexture = textures.Slice(i, 1);
dsc.UpdateImages(0, binding + i, singleTexture, DescriptorType.CombinedImageSampler); dsc.UpdateImages(0, binding + i, singleTexture, DescriptorType.CombinedImageSampler);

View File

@ -9,10 +9,10 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
[SupportedOSPlatform("ios")] [SupportedOSPlatform("ios")]
public static partial class MVKInitialization public static partial class MVKInitialization
{ {
[LibraryImport("MoltenVK.framework/MoltenVK")] [LibraryImport("libMoltenVK.dylib")]
private static partial Result vkGetMoltenVKConfigurationMVK(IntPtr unusedInstance, out MVKConfiguration config, in IntPtr configSize); private static partial Result vkGetMoltenVKConfigurationMVK(IntPtr unusedInstance, out MVKConfiguration config, in IntPtr configSize);
[LibraryImport("MoltenVK.framework/MoltenVK")] [LibraryImport("libMoltenVK.dylib")]
private static partial Result vkSetMoltenVKConfigurationMVK(IntPtr unusedInstance, in MVKConfiguration config, in IntPtr configSize); private static partial Result vkSetMoltenVKConfigurationMVK(IntPtr unusedInstance, in MVKConfiguration config, in IntPtr configSize);
public static void Initialize() public static void Initialize()
@ -24,7 +24,8 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
config.UseMetalArgumentBuffers = true; config.UseMetalArgumentBuffers = true;
config.SemaphoreSupportStyle = MVKVkSemaphoreSupportStyle.MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_SINGLE_QUEUE; config.SemaphoreSupportStyle = MVKVkSemaphoreSupportStyle.MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE_SINGLE_QUEUE;
config.SynchronousQueueSubmits = false;
config.MaxActiveMetalCommandBuffersPerQueue = 1024;
config.ResumeLostDevice = true; config.ResumeLostDevice = true;

View File

@ -1255,7 +1255,7 @@ namespace Ryujinx.Graphics.Vulkan
int vbSize = vertexBuffer.Buffer.Size; int vbSize = vertexBuffer.Buffer.Size;
if (Gd.Vendor == Vendor.Amd && !Gd.IsMoltenVk && vertexBuffer.Stride > 0) if ((Gd.Vendor == Vendor.Amd || !OperatingSystem.IsIOSVersionAtLeast(17)) && !Gd.IsMoltenVk && vertexBuffer.Stride > 0)
{ {
// AMD has a bug where if offset + stride * count is greater than // AMD has a bug where if offset + stride * count is greater than
// the size, then the last attribute will have the wrong value. // the size, then the last attribute will have the wrong value.

View File

@ -601,7 +601,7 @@ namespace Ryujinx.Graphics.Vulkan
if (supportsExtDynamicState) if (supportsExtDynamicState)
{ {
dynamicStates[8] = DynamicState.VertexInputBindingStrideExt; // dynamicStates[8] = DynamicState.VertexInputBindingStrideExt;
} }
var pipelineDynamicStateCreateInfo = new PipelineDynamicStateCreateInfo var pipelineDynamicStateCreateInfo = new PipelineDynamicStateCreateInfo

View File

@ -14,6 +14,8 @@ using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Applets namespace Ryujinx.HLE.HOS.Applets
{ {
@ -51,10 +53,10 @@ namespace Ryujinx.HLE.HOS.Applets
private byte[] _transferMemory; private byte[] _transferMemory;
private string _textValue = ""; public string _textValue = "";
private int _cursorBegin = 0; private int _cursorBegin = 0;
private Encoding _encoding = Encoding.Unicode; private Encoding _encoding = Encoding.Unicode;
private KeyboardResult _lastResult = KeyboardResult.NotSet; public KeyboardResult _lastResult = KeyboardResult.NotSet;
private IDynamicTextInputHandler _dynamicTextInputHandler = null; private IDynamicTextInputHandler _dynamicTextInputHandler = null;
private SoftwareKeyboardRenderer _keyboardRenderer = null; private SoftwareKeyboardRenderer _keyboardRenderer = null;
@ -180,9 +182,6 @@ namespace Ryujinx.HLE.HOS.Applets
return _keyboardRenderer?.DrawTo(destination, position) ?? false; 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() private void ExecuteForegroundKeyboard()
{ {
@ -223,26 +222,8 @@ namespace Ryujinx.HLE.HOS.Applets
InitialText = initialText, InitialText = initialText,
}; };
IntPtr userInputPtr; _textValue = DefaultInputText;
_lastResult = KeyboardResult.Cancel;
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 else
{ {
@ -259,37 +240,40 @@ namespace Ryujinx.HLE.HOS.Applets
StringLengthMax = _keyboardForegroundConfig.StringLengthMax, StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
InitialText = initialText, InitialText = initialText,
}; };
_device.UiHandler.DisplayInputDialog(args, inputText =>
{
Console.WriteLine($"User entered: {inputText}");
_textValue = inputText ?? initialText ?? DefaultInputText;
_lastResult = !string.IsNullOrEmpty(inputText) ? KeyboardResult.Accept : KeyboardResult.Cancel;
_lastResult = _device.UiHandler.DisplayInputDialog(args, out _textValue) ? KeyboardResult.Accept : KeyboardResult.Cancel; while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin)
_textValue ??= initialText ?? DefaultInputText; {
} _textValue = string.Join(" ", _textValue, _textValue);
}
// Ensure the text meets the minimum length requirement // Truncate the text if it exceeds the maximum length
while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin) if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax)
{ {
_textValue = string.Join(" ", _textValue, _textValue); _textValue = _textValue[.._keyboardForegroundConfig.StringLengthMax];
} }
// Truncate the text if it exceeds the maximum length // Handle text validation if required
if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax) if (_keyboardForegroundConfig.CheckText)
{ {
_textValue = _textValue[.._keyboardForegroundConfig.StringLengthMax]; // Submit text for validation
} _foregroundState = SoftwareKeyboardState.ValidationPending;
PushForegroundResponse(true);
}
else
{
// Submit text as complete
_foregroundState = SoftwareKeyboardState.Complete;
PushForegroundResponse(false);
// Handle text validation if required AppletStateChanged?.Invoke(this, null);
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);
} }
} }

View File

@ -1,4 +1,4 @@
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.Cpu.AppleHv; using Ryujinx.Cpu.AppleHv;
@ -49,7 +49,7 @@ namespace Ryujinx.HLE.HOS
bool isArm64Host = RuntimeInformation.ProcessArchitecture == Architecture.Arm64; bool isArm64Host = RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
if ((OperatingSystem.IsMacOS() || OperatingSystem.IsIOSVersionAtLeast(16, 3, 2)) && isArm64Host && for64Bit && context.Device.Configuration.UseHypervisor) if ((OperatingSystem.IsMacOS() || (!OperatingSystem.IsIOSVersionAtLeast(16, 4))) && isArm64Host && for64Bit && context.Device.Configuration.UseHypervisor)
{ {
var cpuEngine = new HvEngine(_tickSource); var cpuEngine = new HvEngine(_tickSource);
var memoryManager = new HvMemoryManager(context.Memory, addressSpaceSize, invalidAccessHandler); var memoryManager = new HvMemoryManager(context.Memory, addressSpaceSize, invalidAccessHandler);

View File

@ -1,5 +1,6 @@
using Ryujinx.HLE.HOS.Applets; using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using System;
namespace Ryujinx.HLE.Ui namespace Ryujinx.HLE.Ui
{ {
@ -10,7 +11,7 @@ namespace Ryujinx.HLE.Ui
/// </summary> /// </summary>
/// <param name="userText">Text that the user entered. Set to `null` on internal errors</param> /// <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> /// <returns>True when OK is pressed, False otherwise. Also returns True on internal errors</returns>
bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText); public void DisplayInputDialog(SoftwareKeyboardUiArgs args, Action<string> onTextEntered);
/// <summary> /// <summary>
/// Displays a Message Dialog box to the user and blocks until it is closed. /// Displays a Message Dialog box to the user and blocks until it is closed.

View File

@ -0,0 +1,42 @@
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);
}
}
});
}
}
}

View File

@ -147,7 +147,7 @@ namespace Ryujinx.Headless.SDL2
[Option("audio-volume", Required = false, Default = 1.0f, HelpText = "The audio level (0 to 1).")] [Option("audio-volume", Required = false, Default = 1.0f, HelpText = "The audio level (0 to 1).")]
public float AudioVolume { get; set; } 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; } public bool UseHypervisor { get; set; }
[Option("lan-interface-id", Required = false, Default = "0", HelpText = "GUID for the network interface used by LAN.")] [Option("lan-interface-id", Required = false, Default = "0", HelpText = "GUID for the network interface used by LAN.")]

View File

@ -95,12 +95,6 @@ using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SDL2; using SDL2;
public class GamepadInfo
{
public string Id { get; set; }
public string Name { get; set; }
}
namespace Ryujinx.Headless.SDL2 namespace Ryujinx.Headless.SDL2
{ {
class Program class Program
@ -121,6 +115,7 @@ namespace Ryujinx.Headless.SDL2
private static bool _enableMouse; private static bool _enableMouse;
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
[UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")] [UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")]
public static unsafe int MainExternal(int argCount, IntPtr* pArgs) public static unsafe int MainExternal(int argCount, IntPtr* pArgs)
@ -147,6 +142,95 @@ namespace Ryujinx.Headless.SDL2
return 0; 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")] [UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
public static unsafe int GetFPS() public static unsafe int GetFPS()
@ -235,7 +319,7 @@ namespace Ryujinx.Headless.SDL2
var result = Parser.Default.ParseArguments<Options>(args) var result = Parser.Default.ParseArguments<Options>(args)
.WithParsed(options => .WithParsed(options =>
{ {
Load(options); Load(options);
}) })
.WithNotParsed(errors => errors.Output()); .WithNotParsed(errors => errors.Output());
@ -310,50 +394,11 @@ namespace Ryujinx.Headless.SDL2
if (_window != null) if (_window != null)
{ {
_window.Exit(); _window.Exit();
_emulationContext.Dispose(); _emulationContext.Dispose();
_emulationContext = null; _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")] [UnmanagedCallersOnly(EntryPoint = "get_game_info")]
public static GameInfoNative GetGameInfoNative(int descriptor, IntPtr extensionPtr) public static GameInfoNative GetGameInfoNative(int descriptor, IntPtr extensionPtr)
@ -361,12 +406,23 @@ namespace Ryujinx.Headless.SDL2
if (_virtualFileSystem == null) { if (_virtualFileSystem == null) {
_virtualFileSystem = VirtualFileSystem.CreateInstance(); _virtualFileSystem = VirtualFileSystem.CreateInstance();
} }
var extension = Marshal.PtrToStringAnsi(extensionPtr); var extension = Marshal.PtrToStringAnsi(extensionPtr);
var stream = OpenFile(descriptor); var stream = OpenFile(descriptor);
var gameInfo = GetGameInfo(stream, extension); var gameInfo = GetGameInfo(stream, extension);
if (gameInfo == null) {
return new GameInfoNative(0, "", "", "", "", new byte[0]);
}
return new GameInfoNative(0, gameInfo.TitleName, 0, gameInfo.Developer, 0, gameInfo.Icon); return new GameInfoNative(
(ulong)gameInfo.FileSize,
gameInfo.TitleName,
gameInfo.TitleId,
gameInfo.Developer,
gameInfo.Version,
gameInfo.Icon
);
} }
public static GameInfo? GetGameInfo(Stream gameStream, string extension) public static GameInfo? GetGameInfo(Stream gameStream, string extension)
@ -756,7 +812,8 @@ namespace Ryujinx.Headless.SDL2
if (File.Exists(titleUpdateMetadataPath)) if (File.Exists(titleUpdateMetadataPath))
{ {
// updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected; string updatePathRelative = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
updatePath = Path.Combine(AppDataManager.BaseDirPath, updatePathRelative);
if (File.Exists(updatePath)) if (File.Exists(updatePath))
{ {
@ -1283,6 +1340,19 @@ namespace Ryujinx.Headless.SDL2
renderer = new ThreadedRenderer(renderer); 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, HLEConfiguration configuration = new(_virtualFileSystem,
_libHacHorizonManager, _libHacHorizonManager,
_contentManager, _contentManager,
@ -1306,7 +1376,7 @@ namespace Ryujinx.Headless.SDL2
options.IgnoreMissingServices, options.IgnoreMissingServices,
options.AspectRatio, options.AspectRatio,
options.AudioVolume, options.AudioVolume,
options.UseHypervisor, AppleHV,
options.MultiplayerLanInterfaceId, options.MultiplayerLanInterfaceId,
Common.Configuration.Multiplayer.MultiplayerMode.LdnMitm); Common.Configuration.Multiplayer.MultiplayerMode.LdnMitm);
@ -1509,46 +1579,58 @@ namespace Ryujinx.Headless.SDL2
public byte[]? Icon; 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 unsafe struct GameInfoNative
{ {
public ulong FileSize; public ulong FileSize;
public fixed byte TitleName[512]; public fixed byte TitleName[512];
public ulong TitleId; public fixed byte TitleId[32];
public fixed byte Developer[256]; public fixed byte Developer[256];
public uint Version; public fixed byte Version[16];
public byte* ImageData; public byte* ImageData;
public uint ImageSize; public uint ImageSize;
public GameInfoNative(ulong fileSize, string titleName, ulong titleId, string developer, uint version, byte[] imageData) public GameInfoNative(ulong fileSize, string titleName, string titleId, string developer, string version, byte[] imageData)
{ {
FileSize = fileSize; FileSize = fileSize;
TitleId = titleId;
Version = version;
fixed (byte* developerPtr = Developer)
fixed (byte* titleNamePtr = TitleName) fixed (byte* titleNamePtr = TitleName)
fixed (byte* titleIdPtr = TitleId)
fixed (byte* developerPtr = Developer)
fixed (byte* versionPtr = Version)
{ {
CopyStringToFixedArray(titleName, titleNamePtr, 512); CopyStringToFixedArray(titleName, titleNamePtr, 512);
CopyStringToFixedArray(titleId, titleIdPtr, 32);
CopyStringToFixedArray(developer, developerPtr, 256); CopyStringToFixedArray(developer, developerPtr, 256);
CopyStringToFixedArray(version, versionPtr, 16);
} }
if (imageData == null || imageData.Length > 4096 * 4096) if (imageData == null || imageData.Length > 4096 * 4096)
{ {
// throw new ArgumentException("Image data must not exceed 4 MB."); ImageSize = 0;
ImageSize = (uint)0;
ImageData = null; ImageData = null;
} }
else else
{ {
ImageSize = (uint)imageData.Length; ImageSize = (uint)imageData.Length;
ImageData = (byte*)Marshal.AllocHGlobal(imageData.Length); ImageData = (byte*)Marshal.AllocHGlobal(imageData.Length);
Marshal.Copy(imageData, 0, (IntPtr)ImageData, imageData.Length); Marshal.Copy(imageData, 0, (IntPtr)ImageData, imageData.Length);
} }
} }
// Don't forget to free the allocated memory // Free allocated memory for ImageData
public void Dispose() public void Dispose()
{ {
if (ImageData != null) if (ImageData != null)
@ -1557,17 +1639,13 @@ namespace Ryujinx.Headless.SDL2
ImageData = null; 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 void CopyArrayToFixedArray(byte[] source, byte* destination, int maxLength) private static unsafe void CopyStringToFixedArray(string source, byte* destination, int length)
{ {
var span = new Span<byte>(destination, maxLength); var span = new Span<byte>(destination, length);
source.AsSpan().CopyTo(span); span.Clear();
} Encoding.UTF8.GetBytes(source, span);
} }
} }
} }

View File

@ -186,7 +186,7 @@ namespace Ryujinx.Headless.SDL2
} }
// WindowHandle = SDL_GetWindowFromID(1); // WindowHandle = SDL_GetWindowFromID(1);
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()); WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", 0, 0, Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
if (WindowHandle == IntPtr.Zero) if (WindowHandle == IntPtr.Zero)
{ {
@ -217,7 +217,10 @@ namespace Ryujinx.Headless.SDL2
{ {
Width = evnt.window.data1; Width = evnt.window.data1;
Height = evnt.window.data2; Height = evnt.window.data2;
Renderer?.Window.SetSize(Width, Height); if (Renderer?.Window != null)
{
Renderer.Window.SetSize(Width, Height);
}
MouseDriver.SetClientSize(Width, Height); MouseDriver.SetClientSize(Width, Height);
} }
break; break;
@ -447,33 +450,29 @@ namespace Ryujinx.Headless.SDL2
}; };
renderLoopThread.Start(); renderLoopThread.Start();
Thread nvidiaStutterWorkaround = null;
if (Renderer is OpenGLRenderer)
{
nvidiaStutterWorkaround = new Thread(NvidiaStutterWorkaround)
{
Name = "GUI.NvidiaStutterWorkaround",
};
nvidiaStutterWorkaround.Start();
}
MainLoop(); MainLoop();
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose. // 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. // We only need to wait for all commands submitted during the main gpu loop to be processed.
_gpuDoneEvent.WaitOne(); _gpuDoneEvent.WaitOne();
_gpuDoneEvent.Dispose(); _gpuDoneEvent.Dispose();
nvidiaStutterWorkaround?.Join();
Exit(); Exit();
} }
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) public void DisplayInputDialog(SoftwareKeyboardUiArgs args, Action<string> onTextEntered)
{ {
// SDL2 doesn't support input dialogs // SDL2 doesn't support input dialogs
userText = "Ryujinx"; // Trying to use Objective-C on iDevices
if (OperatingSystem.IsIOS())
return true; {
AlertHelper.ShowAlertWithTextInput(args.HeaderText, args.SubtitleText, args.GuideText, (inputText) =>
{
onTextEntered?.Invoke(inputText);
});
} else {
onTextEntered?.Invoke("");
}
} }
public bool DisplayMessageDialog(string title, string message) public bool DisplayMessageDialog(string title, string message)

View File

@ -46,7 +46,7 @@ namespace Ryujinx.Memory
private const IntPtr TASK_NULL = 0; private const IntPtr TASK_NULL = 0;
private static readonly IntPtr _selfTask; private static readonly IntPtr _selfTask;
private static readonly int DEFAULT_CHUNK_SIZE = 16 * 1024 * 1024; private static readonly int DEFAULT_CHUNK_SIZE = 1024 * 1024;
static MachJitWorkaround() static MachJitWorkaround()
{ {
@ -211,4 +211,4 @@ namespace Ryujinx.Memory
} }
} }
} }
} }

View File

@ -81,57 +81,10 @@ namespace Ryujinx.Ui.Applet
return okPressed; return okPressed;
} }
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) public void DisplayInputDialog(SoftwareKeyboardUiArgs args, Action<string> onTextEntered)
{ {
ManualResetEvent dialogCloseEvent = new(false); onTextEntered?.Invoke("MeloNX");
return;
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) public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value)