Compare commits

...

36 Commits

Author SHA1 Message Date
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
44 changed files with 1721 additions and 723 deletions

64
.gitignore vendored
View File

@ -176,3 +176,67 @@ PublishProfiles/
# Glade backup files
*.glade~
src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib
# SWIFT GITIGNORE
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

View File

@ -5,6 +5,7 @@
Before you begin, ensure you have the following installed:
- [**.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**
## Compilation Steps
@ -60,11 +61,19 @@ Double-click to open it in **Xcode**.
### 5. Connect Your Device
Ensure your **iPhone/iPad** is **connected** and **recognized** in Xcode.
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.
---

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.
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.

View File

@ -14,17 +14,17 @@
<p align="center">
MeloNX is an iOS Nintendo Switch emulator based on Ryujinx, written primarily in C#. Designed to bring accurate performance and a user-friendly interface to iOS, MeloNX makes Switch games accessible on Apple devices.
Developed from the ground up, MeloNX is open-source and available on Github under the <a href="https://github.com/MeloNX-Emu/MeloNX/blob/master/LICENSE.txt" target="_blank">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>
# Compatibility
MeloNX works on iPhone X and later and iPad 7th Gen and later. Check out the Compatibility on the <a href="https://melonx.org/compatibility/" target="_blank">website</a>.
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
## FAQ
- MeloNX is made for iOS 17+, iOS 15 - 16 is supported but will have issues.
- 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.
@ -60,6 +60,8 @@ If having Issues installing firmware (Make sure your Keys are installed first)
### 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).
@ -108,7 +110,7 @@ If having Issues installing firmware (Make sure your Keys are installed first)
## License
This software is licensed under the terms of the [MIT license](LICENSE.txt).
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.

View File

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

View File

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

View File

@ -25,6 +25,7 @@
/* Begin PBXBuildFile section */
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; };
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
/* End PBXBuildFile section */
@ -82,7 +83,7 @@
4E80A99D2CD6F54700029585 /* MeloNXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4E80A9A72CD6F54700029585 /* MeloNXUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4E80AA622CD7122800029585 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = System/Library/Frameworks/GameController.framework; sourceTree = SDKROOT; };
5650564A2D2A758600C8BB1E /* dotnet.xcconfig.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>"; };
/* End PBXFileReference section */
@ -197,6 +198,7 @@
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */,
4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */,
4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -220,7 +222,7 @@
4E80A9842CD6F54500029585 = {
isa = PBXGroup;
children = (
5650564A2D2A758600C8BB1E /* dotnet.xcconfig.example */,
5650564A2D2A758600C8BB1E /* dotnet.xcconfig */,
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */,
4E80A98F2CD6F54500029585 /* MeloNX */,
4E80A9A02CD6F54700029585 /* MeloNXTests */,
@ -258,7 +260,7 @@
buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */;
buildPhases = (
);
buildToolPath = "$(DOTNET_PATH)";
buildToolPath = /usr/local/share/dotnet/dotnet;
buildWorkingDirectory = "$(SRCROOT)/../..";
dependencies = (
);
@ -656,10 +658,27 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
INFOPLIST_KEY_GCSupportsGameMode = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
@ -710,8 +729,40 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = 1.1.0;
MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@ -755,10 +806,27 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MeloNX/Info.plist;
INFOPLIST_KEY_GCSupportsControllerUserInteraction = YES;
INFOPLIST_KEY_GCSupportsGameMode = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
@ -809,8 +877,40 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = 1.1.0;
MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;

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>1</integer>
</dict>
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</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>1</integer>
</dict>
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</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>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>3</integer>
</dict>
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>4</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

@ -14,8 +14,6 @@
#include <SDL2/SDL.h>
#include <SDL2/SDL_syswm.h>
#import "utils.h"
#ifdef __cplusplus
extern "C" {
@ -31,8 +29,21 @@ struct GameInfo {
unsigned int ImageSize;
};
struct DlcNcaListItem {
char Path[256];
unsigned long TitleId;
};
struct DlcNcaList {
bool success;
unsigned int size;
struct DlcNcaListItem* items;
};
extern struct GameInfo get_game_info(int, char*);
extern struct DlcNcaList get_dlc_nca_list(const char* titleIdPtr, const char* pathPtr);
void install_firmware(const char* inputPtr);
char* installed_firmware_version();

View File

@ -5,15 +5,49 @@
// 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 {
var flags: Int = 0
let pageSize = sysconf(_SC_PAGESIZE)
let code: [UInt32] = [0x52800540, 0xD65F03C0]
csops(getpid(), 0, &flags, sizeof(flags))
return (Int32(flags) & CS_DEBUGGED) != 0;
}
func sizeof<T>(_ value: T) -> Int {
return MemoryLayout<T>.size
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

@ -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,82 +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 (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

@ -78,7 +78,7 @@ class VirtualController {
}
}
static func rumble(lowFreq: Float, highFreq: Float) {
static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
do {
// Low-frequency haptic pattern
let lowFreqPattern = try CHHapticPattern(events: [
@ -96,9 +96,23 @@ class VirtualController {
], relativeTime: 0.2, duration: 0.2)
], parameters: [])
// Create and start the haptic engine
let engine = try CHHapticEngine()
try engine.start()
// Mutable engine
var engine = engine
// If no engine passed, use device engine
if engine == nil {
// Create and start the haptic engine
if hapticEngine == nil {
hapticEngine = try CHHapticEngine()
try hapticEngine?.start()
}
engine = hapticEngine
}
guard let engine else {
return print("Error creating haptic patterns: hapticEngine is nil")
}
// Create and play the low-frequency player
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
@ -113,6 +127,8 @@ class VirtualController {
}
}
private static var hapticEngine: CHHapticEngine?
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }

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,26 +28,6 @@ struct iOSNav<Content: View>: View {
}
}
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)"
}
}
}
class Ryujinx {
private var isRunning = false
@ -60,6 +40,8 @@ class Ryujinx {
@Published var emulationUIView = UIView()
@Published var games: [Game] = []
@Published var defMLContentSize: CGFloat?
var shouldMetal: Bool {
metalLayer == nil
}
@ -93,6 +75,9 @@ class Ryujinx {
var dfsIntegrityChecks: Bool
var disablePTC: Bool
var disablevsync: Bool
var language: SystemLanguage
var regioncode: SystemRegionCode
var handHeldController: Bool
init(gamepath: String,
@ -116,7 +101,10 @@ class Ryujinx {
expandRam: Bool = false,
dfsIntegrityChecks: Bool = false,
disablePTC: Bool = false,
disablevsync: Bool = false
disablevsync: Bool = false,
language: SystemLanguage = .americanEnglish,
regioncode: SystemRegionCode = .usa,
handHeldController: Bool = false
) {
self.gamepath = gamepath
self.inputids = inputids
@ -140,6 +128,9 @@ class Ryujinx {
self.dfsIntegrityChecks = dfsIntegrityChecks
self.disablePTC = disablePTC
self.disablevsync = disablevsync
self.language = language
self.regioncode = regioncode
self.handHeldController = handHeldController
}
}
@ -234,7 +225,7 @@ class Ryujinx {
print(error)
}
}
return games
} catch {
print("Error loading games from roms folder: \(error)")
@ -261,6 +252,10 @@ class Ryujinx {
// We don't need this. Ryujinx should handle it fine :3
// this also causes crashes in some games :3
args.append(contentsOf: ["--system-language", config.language.rawValue])
args.append(contentsOf: ["--system-region", config.regioncode.rawValue])
args.append(contentsOf: ["--aspect-ratio", config.aspectRatio.rawValue])
if config.nintendoinput {
@ -275,7 +270,7 @@ class Ryujinx {
args.append("--disable-vsync")
}
if config.hypervisor {
args.append("--use-hypervisor")
}
@ -294,7 +289,8 @@ class Ryujinx {
}
if config.ignoreMissingServices {
args.append(contentsOf: ["--ignore-missing-services", String(config.maxAnisotropy)])
// args.append(contentsOf: ["--ignore-missing-services"])
args.append("--ignore-missing-services")
}
if config.maxAnisotropy != 0 {
@ -317,21 +313,25 @@ class Ryujinx {
}
if config.debuglogs {
args.append(contentsOf: ["--enable-debug-logs"])
args.append("--enable-debug-logs")
}
if config.tracelogs {
args.append(contentsOf: ["--enable-trace-logs"])
args.append("--enable-trace-logs")
}
// List the input ids
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 {
config.inputids.prefix(4).enumerated().forEach { index, inputId in
args.append(contentsOf: ["--input-id-\(index + 1)", inputId])
config.inputids.prefix(8).enumerated().forEach { index, inputId in
if config.handHeldController {
args.append(contentsOf: ["\(index == 0 ? "--input-id-handheld" : "--input-id-\(index + 1)")", inputId])
} else {
args.append(contentsOf: ["--input-id-\(index + 1)", inputId])
}
}
}
@ -372,7 +372,30 @@ class Ryujinx {
self.firmwareversion = version
}
}
func getDlcNcaList(titleId: String, path: String) -> [DownloadableContentNca] {
guard let titleIdCString = titleId.cString(using: .utf8),
let pathCString = path.cString(using: .utf8)
else {
print("Invalid path")
return []
}
let listPointer = get_dlc_nca_list(titleIdCString, pathCString)
print("DLC parcing success: \(listPointer.success)")
guard listPointer.success else { return [] }
let list = Array(UnsafeBufferPointer(start: listPointer.items, count: Int(listPointer.size)))
return list.map { item in
.init(fullPath: withUnsafePointer(to: item.Path) {
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
String(cString: $0)
}
}, titleId: item.TitleId, enabled: true)
}
}
private func generateGamepadId(joystickIndex: Int32) -> String? {
let guid = SDL_JoystickGetDeviceGUID(joystickIndex)
@ -461,23 +484,26 @@ class Ryujinx {
func repeatuntilfindLayer() {
DispatchQueue.global(qos: .background).async {
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 {

View File

@ -9,7 +9,7 @@ import SwiftUI
import UniformTypeIdentifiers
public struct Game: Identifiable, Equatable, Hashable {
public var id = UUID()
public var id: URL { fileURL }
var containerFolder: URL
var fileType: UTType

View File

@ -26,6 +26,7 @@ struct ContentView: View {
@State private var controllersList: [Controller] = []
@State private var currentControllers: [Controller] = []
@State var onscreencontroller: Controller = Controller(id: "", name: "")
@State var nativeControllers: [GCController: NativeController] = [:]
@State private var isVirtualControllerActive: Bool = false
@AppStorage("isVirtualController") var isVCA: Bool = true
@ -42,7 +43,7 @@ struct ContentView: View {
@AppStorage("quit") var quit: Bool = false
@State var quits: Bool = false
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = false
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true
// Loading Animation
@State private var clumpOffset: CGFloat = -100
@ -50,31 +51,23 @@ struct ContentView: View {
private let animationDuration: Double = 1.0
@State private var isAnimating = false
@State var isLoading = true
// MARK: - Initialization
init() {
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
_config = State(initialValue: defaultConfig)
let defaultSettings: [MoltenVKSettings] = [
// MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"),
// MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2"),
// Metal Private API isn't needed and causes more stutters
let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings.
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
// MoltenVKSettings(string: "MVK_CONFIG_LOG_LEVEL", value: "0"),
// MVK_CONFIG_LOG_LEVEL
//MVK_DEBUG
// Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64 or 192 depending on what i decide)
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "1024"),
// 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)
print("JIT Enabled: \(isJITEnabled())")
initializeSDL()
}
@ -90,20 +83,6 @@ struct ContentView: View {
} else {
ZStack {
emulationView
.onAppear() {
// This is fro the old exiting game feature that didn't work properly. will look into it and figure out a better alternative
/*
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
timer.invalidate()
quits = quit
if quits {
quit = false
timer.invalidate()
}
}
*/
}
}
}
} else {
@ -115,10 +94,10 @@ struct ContentView: View {
isAnimating = false
}
} else {
VStack {
}
EmulationView()
.onAppear() {
isAnimating = false
}
}
}
} else {
@ -152,6 +131,7 @@ struct ContentView: View {
queue: .main) { notification in
if let controller = notification.object as? GCController {
print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller)
refreshControllersList()
}
}
@ -163,6 +143,8 @@ struct ContentView: View {
queue: .main) { notification in
if let controller = notification.object as? GCController {
print("Controller disconnected: \(controller.productCategory)")
nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil
refreshControllersList()
}
}
@ -193,14 +175,12 @@ struct ContentView: View {
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
ZStack(alignment: .leading) {
// Background track
Rectangle()
.cornerRadius(10)
.frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
.foregroundColor(.gray.opacity(0.3))
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
// Animated loading bar
Rectangle()
.cornerRadius(10)
.frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
@ -268,15 +248,9 @@ struct ContentView: View {
))
let isJIT = isJITEnabled()
if !isJIT, useTrollStore {
askForJIT()
if !isJIT {
useTrollStore ? askForJIT() : enableJITEB()
}
if !isJIT, jitStreamerEB {
enableJITEB()
}
}
}
@ -285,7 +259,7 @@ struct ContentView: View {
private func initializeSDL() {
setMoltenVKSettings()
SDL_SetMainReady() // Sets SDL Ready
SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true (Check out SDL2 Documentation here)
SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true
SDL_Init(SdlInitFlags) // Initialises SDL2
initialize()
}
@ -306,8 +280,9 @@ struct ContentView: View {
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 == 1 {
@ -322,27 +297,6 @@ struct ContentView: View {
}
}
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) {
@ -397,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

@ -129,6 +129,8 @@ struct ControllerView: View {
struct ShoulderButtonsViewLeft: View {
@State var width: CGFloat = 160
@State var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack {
ButtonView(button: .leftTrigger)
@ -142,6 +144,9 @@ struct ShoulderButtonsViewLeft: View {
width *= 1.2
height *= 1.2
}
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
}
}
}
@ -149,6 +154,8 @@ struct ShoulderButtonsViewLeft: View {
struct ShoulderButtonsViewRight: View {
@State var width: CGFloat = 160
@State var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack {
ButtonView(button: .rightShoulder)
@ -162,12 +169,16 @@ struct ShoulderButtonsViewRight: View {
width *= 1.2
height *= 1.2
}
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
}
}
}
struct DPadView: View {
@State var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack {
ButtonView(button: .dPadUp)
@ -184,12 +195,16 @@ struct DPadView: View {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
size *= CGFloat(controllerScale)
}
}
}
struct ABXYView: View {
@State var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack {
ButtonView(button: .X)
@ -206,6 +221,8 @@ struct ABXYView: View {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
size *= CGFloat(controllerScale)
}
}
}
@ -218,6 +235,7 @@ struct ButtonView: View {
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@ -256,6 +274,9 @@ struct ButtonView: View {
width *= 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
@ObservedObject public var joystickMonitor = JoystickMonitor()
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var dragDiameter: CGFloat {
var selfs = CGFloat(160)
selfs *= controllerScale
if UIDevice.current.systemName.contains("iPadOS") {
return selfs * 1.2
}
return selfs
}
private let shape: JoystickShape = .circle

View File

@ -11,16 +11,18 @@ import SwiftUI
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(airplay: true).ignoresSafeArea()))
Air.play(AnyView(MetalView().ignoresSafeArea()))
}
} else {
MetalView(airplay: false) // The Emulation View
MetalView() // The Emulation View
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
}
@ -31,6 +33,7 @@ struct EmulationView: View {
ControllerView() // Virtual Controller
}
if ssb {
Group {
VStack {

View File

@ -10,7 +10,7 @@ import MetalKit
struct MetalView: UIViewRepresentable {
var airplay: Bool // just in case :3
var airplay: Bool = Air.shared.connected // just in case :3
func makeUIView(context: Context) -> UIView {
let metalLayer = Ryujinx.shared.metalLayer!

View File

@ -14,45 +14,52 @@ struct GameInfoSheet: View {
var body: some View {
iOSNav {
VStack {
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")
}
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()
}
} else {
Image(systemName: "questionmark.circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 150, height: 150)
.padding()
}
VStack(alignment: .leading) {
VStack(alignment: .leading) {
Text("**\(game.titleName)** | \(game.titleId.capitalized)")
Text(game.developer)
.font(.caption)
.foregroundStyle(.secondary)
VStack(alignment: .center) {
Text("**\(game.titleName)** | \(game.titleId.capitalized)")
.multilineTextAlignment(.center)
Text(game.developer)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 3)
}
.padding(.vertical, 3)
VStack(alignment: .leading, spacing: 5) {
Text("Information")
.font(.title2)
.bold()
Text("**Version:** \(game.version)")
Text("**Title ID:** \(game.titleId)")
.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
@ -60,15 +67,32 @@ struct GameInfoSheet: View {
Text("Copy Title ID")
}
}
Text("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes")
Text("**File Type:** .\(getFileType(game.fileURL))")
Text("**Game URL:** \(trimGameURL(game.fileURL))")
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")
}
Spacer()
.headerProminence(.increased)
}
.padding(.horizontal, 5)
.navigationTitle(game.titleName)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@ -103,10 +127,6 @@ struct GameInfoSheet: View {
}
func getFileType(_ url: URL) -> String {
let path = url.path
if let range = path.range(of: ".") {
return String(path[range.upperBound...])
}
return "Unknown"
url.pathExtension
}
}

View File

@ -27,6 +27,8 @@ struct GameLibraryView: View {
@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(
@ -37,121 +39,107 @@ struct GameLibraryView: View {
var filteredGames: [Game] {
if searchText.isEmpty {
return Ryujinx.shared.games
return Ryujinx.shared.games.filter { game in
!realRecentGames.contains(where: { $0.fileURL == game.fileURL })
}
}
return Ryujinx.shared.games.filter {
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
$0.developer.localizedCaseInsensitiveContains(searchText)
}
}
var realRecentGames: [Game] {
let games = Ryujinx.shared.games
return recentGames.compactMap { recentGame in
games.first(where: { $0.fileURL == recentGame.fileURL })
}
}
var body: some View {
iOSNav {
ScrollView {
LazyVStack(alignment: .leading, spacing: 20) {
if !isSearching {
Text("Games")
.font(.system(size: 34, weight: .bold))
.padding(.horizontal)
.padding(.top, 12)
List {
if Ryujinx.shared.games.isEmpty {
VStack(spacing: 16) {
Image(systemName: "gamecontroller.fill")
.font(.system(size: 64))
.foregroundColor(.secondary.opacity(0.7))
.padding(.top, 60)
Text("No Games Found")
.font(.title2.bold())
.foregroundColor(.primary)
Text("Add ROM, Keys and Firmware to get started")
.font(.subheadline)
.foregroundColor(.secondary)
}
if Ryujinx.shared.games.isEmpty {
VStack(spacing: 16) {
Image(systemName: "gamecontroller.fill")
.font(.system(size: 64))
.foregroundColor(.secondary.opacity(0.7))
.padding(.top, 60)
Text("No Games Found")
.font(.title2.bold())
.foregroundColor(.primary)
Text("Add ROM, Keys and Firmware to get started")
.font(.subheadline)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else {
if !isSearching && !realRecentGames.isEmpty {
Section {
ForEach(realRecentGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
removeFromRecentGames(game)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
} header: {
Text("Recent")
}
Section {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
}
} header: {
Text("Others")
}
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else {
if !isSearching && !recentGames.isEmpty {
VStack(alignment: .leading, spacing: 12) {
Text("Recent")
.font(.title2.bold())
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16) {
ForEach(recentGames) { game in
RecentGameCard(game: game, startemu: $startemu)
.onTapGesture {
addToRecentGames(game)
startemu = game
}
}
}
.padding(.horizontal)
}
}
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, games: games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
.onTapGesture {
addToRecentGames(game)
}
}
}
}
} else {
LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
.onTapGesture {
addToRecentGames(game)
}
}
}
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
}
}
}
.onAppear {
loadRecentGames()
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmwareversion = (firmware == "" ? "0" : firmware)
}
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
switch result {
case .success(let url):
do {
let fun = url.startAccessingSecurityScopedResource()
let path = url.path
Ryujinx.shared.installFirmware(firmwarePath: path)
firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
if fun {
url.stopAccessingSecurityScopedResource()
}
}
case .failure(let error):
print(error)
}
}
}
.navigationTitle("Games")
.navigationBarTitleDisplayMode(.large)
.onAppear {
loadRecentGames()
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmwareversion = (firmware == "" ? "0" : firmware)
}
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
switch result {
case .success(let url):
do {
let fun = url.startAccessingSecurityScopedResource()
let path = url.path
Ryujinx.shared.installFirmware(firmwarePath: path)
firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
if fun {
url.stopAccessingSecurityScopedResource()
}
}
case .failure(let error):
print(error)
}
}
.toolbar {
ToolbarItem(placement: .topBarLeading) {
ToolbarItem(placement: .topBarTrailing) {
Button {
isSelectingGameFile.toggle()
} label: {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .topBarLeading) {
Menu {
Text("Firmware Version: \(firmwareversion)")
@ -213,13 +201,17 @@ struct GameLibraryView: View {
}
}
}
.onChange(of: startemu) { game in
guard let game else { return }
addToRecentGames(game)
}
}
.background(Color(.systemGroupedBackground))
.searchable(text: $searchText)
.animation(.easeInOut, value: searchText)
.onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty
}
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .folder]) { result in
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .folder, .nsp, .xci]) { result in
switch result {
case .success(let url):
guard url.startAccessingSecurityScopedResource() else {
@ -277,6 +269,12 @@ struct GameLibraryView: View {
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
@ -292,10 +290,9 @@ struct GameLibraryView: View {
}
}
private func addToRecentGames(_ game: Game) {
recentGames.removeAll { $0.id == game.id }
recentGames.removeAll { $0.titleId == game.titleId }
recentGames.insert(game, at: 0)
if recentGames.count > 5 {
@ -304,7 +301,12 @@ struct GameLibraryView: View {
saveRecentGames()
}
private func removeFromRecentGames(_ game: Game) {
recentGames.removeAll { $0.titleId == game.titleId }
saveRecentGames()
}
private func saveRecentGames() {
do {
let encoder = JSONEncoder()
@ -325,8 +327,7 @@ struct GameLibraryView: View {
}
}
// MARK: - Delete Game Function
// MARK: - Delete Game Function
func deleteGame(game: Game) {
let fileManager = FileManager.default
do {
@ -339,7 +340,7 @@ struct GameLibraryView: View {
}
}
// MARK: -Game Model
// MARK: - Game Model
extension Game: Codable {
enum CodingKeys: String, CodingKey {
case titleName, titleId, developer, version, fileURL
@ -368,64 +369,21 @@ extension Game: Codable {
}
}
// MARK: -Recent Game Card
struct RecentGameCard: View {
let game: Game
@Binding var startemu: Game?
@Environment(\.colorScheme) var colorScheme
var body: some View {
Button(action: {
startemu = game
}) {
VStack(alignment: .leading, spacing: 8) {
if let icon = game.icon {
Image(uiImage: icon)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 140, height: 140)
.cornerRadius(12)
} else {
ZStack {
RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ?
Color(.systemGray5) : Color(.systemGray6))
.frame(width: 140, height: 140)
Image(systemName: "gamecontroller.fill")
.font(.system(size: 40))
.foregroundColor(.gray)
}
}
VStack(alignment: .leading, spacing: 2) {
Text(game.titleName)
.font(.subheadline.bold())
.lineLimit(1)
Text(game.developer)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
}
.padding(.horizontal, 4)
}
}
.buttonStyle(.plain)
}
}
// MARK: -Game List Item
// MARK: - Game List Item
struct GameListRow: View {
let game: Game
@Binding var startemu: Game?
@Binding var games: [Game] // Add this binding
@Binding var isViewingGameInfo: Bool
@Binding var isSelectingGameUpdate: Bool
@Binding var isSelectingGameDLC: Bool
@Binding var gameInfo: Game?
@State var gametoDelete: Game?
@State var showGameDeleteConfirmation: Bool = false
@Environment(\.colorScheme) var colorScheme
@AppStorage("portal") var gamepo = false
var body: some View {
Button(action: {
startemu = game
@ -442,7 +400,7 @@ struct GameListRow: View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(colorScheme == .dark ?
Color(.systemGray5) : Color(.systemGray6))
Color(.systemGray5) : Color(.systemGray6))
.frame(width: 45, height: 45)
Image(systemName: "gamecontroller.fill")
@ -469,36 +427,54 @@ struct GameListRow: View {
.foregroundColor(.accentColor)
.opacity(0.8)
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(.systemBackground))
.contextMenu {
Section {
Button {
startemu = game
} label: {
Label("Play Now", systemImage: "play.fill")
}
Button {
gameInfo = game
isViewingGameInfo.toggle()
} label: {
Label("Game Info", systemImage: "info.circle")
}
}
.contextMenu {
Section {
Button {
startemu = game
} label: {
Label("Play Now", systemImage: "play.fill")
}
Section {
Button(role: .destructive) {
gametoDelete = game
showGameDeleteConfirmation.toggle()
} label: {
Label("Delete", systemImage: "trash")
Button {
gameInfo = game
isViewingGameInfo.toggle()
if game.titleName.lowercased() == "portal" {
gamepo = true
} else if game.titleName.lowercased() == "portal 2" {
gamepo = true
}
} label: {
Label("Game Info", systemImage: "info.circle")
}
}
Section {
Button {
gameInfo = game
isSelectingGameUpdate.toggle()
} label: {
Label("Game Update Manager", systemImage: "chevron.up.circle")
}
Button {
gameInfo = game
isSelectingGameDLC.toggle()
} label: {
Label("Game DLC Manager", systemImage: "plus.viewfinder")
}
}
Section {
Button(role: .destructive) {
gametoDelete = game
showGameDeleteConfirmation.toggle()
} label: {
Label("Delete", systemImage: "trash")
}
}
}
.buttonStyle(.plain)
.confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
Button("Delete", role: .destructive) {
if let game = gametoDelete {
@ -521,4 +497,3 @@ struct GameListRow: View {
}
}
}

View File

@ -40,10 +40,13 @@ struct SettingsView: View {
@AppStorage("oldWindowCode") var windowCode: Bool = false
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State private var showResolutionInfo = false
@State private var showAnisotropicInfo = false
@State private var showControllerInfo = false
@State private var searchText = ""
@AppStorage("portal") var gamepo = false
var filteredMemoryModes: [(String, String)] {
guard !searchText.isEmpty else { return memoryManagerModes }
@ -272,17 +275,56 @@ struct SettingsView: View {
// Input Settings
Section {
Toggle(isOn: $config.listinputids) {
labelWithIcon("List Input IDs", iconName: "list.bullet")
}
.tint(.blue)
Toggle(isOn: $config.macroHLE) {
labelWithIcon("Player 1 to Handheld Input", iconName: "formfitting.gamecontroller")
}.tint(.blue)
Toggle(isOn: $ryuDemo) {
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
}
.tint(.blue)
.disabled(true)
VStack(alignment: .leading, spacing: 10) {
HStack {
labelWithIcon("On-Screen Controller Scale", iconName: "magnifyingglass")
.font(.headline)
Spacer()
Button {
showControllerInfo.toggle()
} label: {
Image(systemName: "info.circle")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help("Learn more about On-Screen Controller Scale")
.alert(isPresented: $showControllerInfo) {
Alert(
title: Text("On-Screen Controller Scale"),
message: Text("Adjust the On-Screen Controller size."),
dismissButton: .default(Text("OK"))
)
}
}
Slider(value: $controllerScale, in: 0.1...3.0, step: 0.05) {
Text("Resolution Scale")
} minimumValueLabel: {
Text("0.1x")
.font(.footnote)
.foregroundColor(.secondary)
} maximumValueLabel: {
Text("3.0x")
.font(.footnote)
.foregroundColor(.secondary)
}
Text("\(controllerScale, specifier: "%.2f")x")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.vertical, 8)
} header: {
Text("Input Settings")
.font(.title3.weight(.semibold))
@ -292,6 +334,35 @@ struct SettingsView: View {
Text("Configure input devices and on-screen controls for easier navigation and play.")
}
// Language and Region Settings
Section {
Picker(selection: $config.language) {
ForEach(SystemLanguage.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Language", iconName: "character.bubble")
}
Picker(selection: $config.regioncode) {
ForEach(SystemRegionCode.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
labelWithIcon("Region", iconName: "globe")
}
// globe
} header: {
Text("Language and Region Settings")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Configure the System Language and the Region.")
}
// CPU Mode
Section {
if filteredMemoryModes.isEmpty {
@ -314,16 +385,16 @@ struct SettingsView: View {
if let cpuInfo = getCPUInfo(), cpuInfo.hasPrefix("Apple M") {
if #available (iOS 16.4, *) {
Toggle(isOn: .constant(false)) {
labelWithIcon("Hypervisor", iconName: "bolt.fill")
labelWithIcon("Hypervisor", iconName: "bolt")
}
.tint(.blue)
.disabled(true)
.onAppear() {
print("CPU Info: \(cpuInfo)")
}
} else if getEntitlementValue("com.apple.private.hypervisor") {
} else if checkAppEntitlement("com.apple.private.hypervisor") {
Toggle(isOn: $config.hypervisor) {
labelWithIcon("Hypervisor", iconName: "bolt.fill")
labelWithIcon("Hypervisor", iconName: "bolt")
}
.tint(.blue)
.onAppear() {
@ -440,16 +511,43 @@ struct SettingsView: View {
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.")
}
// Advanced
// 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")
if #unavailable(iOS 17) {
Toggle(isOn: $windowCode) {
labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle")
}
.tint(.blue)
labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
labelWithIcon("Device: \(getDeviceModel())", iconName: iconName)
labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill")
labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo")
} header: {
Text("Information")
.font(.title3.weight(.semibold))
.textCase(nil)
.headerProminence(.increased)
} footer: {
Text("Shows info about Memory, Entitlement and JIT.")
}
// Advanced
Section {
Toggle(isOn: $windowCode) {
labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle")
}
.tint(.blue)
DisclosureGroup {
@ -502,9 +600,9 @@ struct SettingsView: View {
.headerProminence(.increased)
} footer: {
if #available(iOS 17, *) {
Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing).")
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\"")
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" : "")")
}
}
@ -533,6 +631,18 @@ struct SettingsView: View {
}
}
func getDeviceModel() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
return identifier
}
func saveSettings() {
#if targetEnvironment(simulator)

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

@ -15,6 +15,17 @@
</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>
@ -25,6 +36,11 @@
<array>
<string>LaunchGameIntent</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>processing</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UTExportedTypeDeclarations</key>

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

@ -26,10 +26,16 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native
{
return $"lib{libraryName}.so.{version}";
}
else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) // TODO: ffmpeg on ios
else if (OperatingSystem.IsMacOS())
{
return $"lib{libraryName}.{version}.dylib";
}
else if (OperatingSystem.IsIOS())
{
string libName = $"lib{libraryName}.{version}.dylib";
Console.WriteLine($"[iOS] Required firmware library: {libName}");
return libName;
}
else
{
throw new NotImplementedException($"Unsupported OS for FFmpeg: {RuntimeInformation.RuntimeIdentifier}");

View File

@ -9,10 +9,10 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
[SupportedOSPlatform("ios")]
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);
[LibraryImport("MoltenVK.framework/MoltenVK")]
[LibraryImport("libMoltenVK.dylib")]
private static partial Result vkSetMoltenVKConfigurationMVK(IntPtr unusedInstance, in MVKConfiguration config, in IntPtr configSize);
public static void Initialize()

View File

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

View File

@ -115,6 +115,7 @@ namespace Ryujinx.Headless.SDL2
private static bool _enableMouse;
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
[UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")]
public static unsafe int MainExternal(int argCount, IntPtr* pArgs)
@ -141,6 +142,95 @@ namespace Ryujinx.Headless.SDL2
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "get_dlc_nca_list")]
public static unsafe DlcNcaList GetDlcNcaList(IntPtr titleIdPtr, IntPtr pathPtr)
{
var titleId = Marshal.PtrToStringAnsi(titleIdPtr);
var containerPath = Marshal.PtrToStringAnsi(pathPtr);
if (!File.Exists(containerPath))
{
return new DlcNcaList { success = false };
}
using FileStream containerFile = File.OpenRead(containerPath);
PartitionFileSystem pfs = new();
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
bool containsDlc = false;
_virtualFileSystem.ImportTickets(pfs);
// TreeIter? parentIter = null;
List<DlcNcaListItem> listItems = new();
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath);
if (nca == null)
{
continue;
}
if (nca.Header.ContentType == NcaContentType.PublicData)
{
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != titleId)
{
break;
}
Logger.Warning?.Print(LogClass.Application, $"ContainerPath: {containerPath}");
Logger.Warning?.Print(LogClass.Application, $"TitleId: {nca.Header.TitleId}");
Logger.Warning?.Print(LogClass.Application, $"fileEntry.FullPath: {fileEntry.FullPath}");
// parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
// ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
DlcNcaListItem item = new();
CopyStringToFixedArray(fileEntry.FullPath, item.Path, 256);
item.TitleId = nca.Header.TitleId;
listItems.Add(item);
containsDlc = true;
}
}
if (!containsDlc)
{
return new DlcNcaList { success = false };
// GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
}
var list = new DlcNcaList { success = true, size = (uint) listItems.Count };
DlcNcaListItem[] items = listItems.ToArray();
fixed (DlcNcaListItem* p = &items[0])
{
list.items = p;
}
return list;
}
private static Nca TryCreateNca(IStorage ncaStorage, string containerPath)
{
try
{
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
}
catch (Exception exception)
{
// ignored
}
return null;
}
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
public static unsafe int GetFPS()
@ -229,7 +319,7 @@ namespace Ryujinx.Headless.SDL2
var result = Parser.Default.ParseArguments<Options>(args)
.WithParsed(options =>
{
Load(options); // Load is called with the parsed options
Load(options);
})
.WithNotParsed(errors => errors.Output());
@ -304,7 +394,6 @@ namespace Ryujinx.Headless.SDL2
if (_window != null)
{
_window.Exit();
_emulationContext.Dispose();
_emulationContext = null;
@ -317,10 +406,14 @@ namespace Ryujinx.Headless.SDL2
if (_virtualFileSystem == null) {
_virtualFileSystem = VirtualFileSystem.CreateInstance();
}
var extension = Marshal.PtrToStringAnsi(extensionPtr);
var stream = OpenFile(descriptor);
var gameInfo = GetGameInfo(stream, extension);
if (gameInfo == null) {
return new GameInfoNative(0, "", "", "", "", new byte[0]);
}
return new GameInfoNative(
(ulong)gameInfo.FileSize,
@ -719,7 +812,8 @@ namespace Ryujinx.Headless.SDL2
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))
{
@ -1485,6 +1579,19 @@ namespace Ryujinx.Headless.SDL2
public byte[]? Icon;
}
public unsafe struct DlcNcaListItem
{
public fixed byte Path[256];
public ulong TitleId;
}
public unsafe struct DlcNcaList
{
public bool success;
public uint size;
public unsafe DlcNcaListItem* items;
}
public unsafe struct GameInfoNative
{
public ulong FileSize;
@ -1532,14 +1639,13 @@ namespace Ryujinx.Headless.SDL2
ImageData = null;
}
}
private static void CopyStringToFixedArray(string source, byte* destination, int length)
{
var span = new Span<byte>(destination, length);
span.Clear();
Encoding.UTF8.GetBytes(source, span);
}
}
private static unsafe void CopyStringToFixedArray(string source, byte* destination, int length)
{
var span = new Span<byte>(destination, length);
span.Clear();
Encoding.UTF8.GetBytes(source, span);
}
}
}