forked from MeloNX/MeloNX
Compare commits
23 Commits
e741039304
...
e924da52ec
Author | SHA1 | Date | |
---|---|---|---|
|
e924da52ec | ||
|
ea2eff15c6 | ||
|
c6ff0b60bf | ||
|
edc56316cc | ||
|
8df465a959 | ||
527ac3fb23 | |||
8e60f6dc50 | |||
|
3b99631dfb | ||
cb33b04f2b | |||
500f3d5b9e | |||
ac4e5d394e | |||
f2d078f80b | |||
004a81fa60 | |||
ddf634ecb6 | |||
|
cce876c6f5 | ||
ebfb39c132 | |||
b3bb9cefcf | |||
8c54134699 | |||
e8537df246 | |||
8c6dd455f2 | |||
2a7cfa5650 | |||
df2b17ddd6 | |||
757fb1f6d1 |
64
.gitignore
vendored
64
.gitignore
vendored
@ -176,3 +176,67 @@ PublishProfiles/
|
|||||||
# Glade backup files
|
# Glade backup files
|
||||||
*.glade~
|
*.glade~
|
||||||
src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib
|
src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib
|
||||||
|
|
||||||
|
# SWIFT GITIGNORE
|
||||||
|
# Xcode
|
||||||
|
#
|
||||||
|
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||||
|
|
||||||
|
## User settings
|
||||||
|
xcuserdata/
|
||||||
|
|
||||||
|
## Obj-C/Swift specific
|
||||||
|
*.hmap
|
||||||
|
|
||||||
|
## App packaging
|
||||||
|
*.ipa
|
||||||
|
*.dSYM.zip
|
||||||
|
*.dSYM
|
||||||
|
|
||||||
|
## Playgrounds
|
||||||
|
timeline.xctimeline
|
||||||
|
playground.xcworkspace
|
||||||
|
|
||||||
|
# Swift Package Manager
|
||||||
|
#
|
||||||
|
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||||
|
# Packages/
|
||||||
|
# Package.pins
|
||||||
|
# Package.resolved
|
||||||
|
# *.xcodeproj
|
||||||
|
#
|
||||||
|
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||||
|
# hence it is not needed unless you have added a package configuration file to your project
|
||||||
|
# .swiftpm
|
||||||
|
|
||||||
|
.build/
|
||||||
|
|
||||||
|
# CocoaPods
|
||||||
|
#
|
||||||
|
# We recommend against adding the Pods directory to your .gitignore. However
|
||||||
|
# you should judge for yourself, the pros and cons are mentioned at:
|
||||||
|
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||||
|
#
|
||||||
|
# Pods/
|
||||||
|
#
|
||||||
|
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||||
|
# *.xcworkspace
|
||||||
|
|
||||||
|
# Carthage
|
||||||
|
#
|
||||||
|
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||||
|
# Carthage/Checkouts
|
||||||
|
|
||||||
|
Carthage/Build/
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
#
|
||||||
|
# It is recommended to not store the screenshots in the git repo.
|
||||||
|
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||||
|
# For more information about the recommended setup visit:
|
||||||
|
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||||
|
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots/**/*.png
|
||||||
|
fastlane/test_output
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
Before you begin, ensure you have the following installed:
|
Before you begin, ensure you have the following installed:
|
||||||
|
|
||||||
- [**.NET 8.0**](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
- [**.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**
|
- A Mac running **macOS**
|
||||||
|
|
||||||
## Compilation Steps
|
## Compilation Steps
|
||||||
|
@ -19,12 +19,12 @@
|
|||||||
|
|
||||||
# Compatibility
|
# 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
|
# Usage
|
||||||
|
|
||||||
## FAQ
|
## 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 Xcode or a Paid Apple Developer Account. SideStore support may come soon (SideStore Side Issue)
|
||||||
- MeloNX needs JIT
|
- MeloNX needs JIT
|
||||||
- Recommended Device: iPhone 15 Pro or newer.
|
- Recommended Device: iPhone 15 Pro or newer.
|
||||||
@ -60,6 +60,8 @@ If having Issues installing firmware (Make sure your Keys are installed first)
|
|||||||
|
|
||||||
### Xcode
|
### Xcode
|
||||||
|
|
||||||
|
**NOTE: These Xcode builds are nightly and may have unfinished features.**
|
||||||
|
|
||||||
1. **Compile Guide**
|
1. **Compile Guide**
|
||||||
- Visit the [guide here](https://git.743378673.xyz/MeloNX/MeloNX/src/branch/XC-ios-ht/Compile.md).
|
- Visit the [guide here](https://git.743378673.xyz/MeloNX/MeloNX/src/branch/XC-ios-ht/Compile.md).
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ namespace ARMeilleure.Memory
|
|||||||
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
|
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
|
||||||
|
|
||||||
public IJitMemoryBlock Block { get; }
|
public IJitMemoryBlock Block { get; }
|
||||||
|
public IJitMemoryAllocator Allocator { get; }
|
||||||
|
|
||||||
public IntPtr Pointer => Block.Pointer;
|
public IntPtr Pointer => Block.Pointer;
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ namespace ARMeilleure.Memory
|
|||||||
granularity = DefaultGranularity;
|
granularity = DefaultGranularity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Allocator = allocator;
|
||||||
Block = allocator.Reserve(maxSize);
|
Block = allocator.Reserve(maxSize);
|
||||||
_maxSize = maxSize;
|
_maxSize = maxSize;
|
||||||
_sizeGranularity = granularity;
|
_sizeGranularity = granularity;
|
||||||
|
@ -3,6 +3,7 @@ using ARMeilleure.CodeGen.Unwinding;
|
|||||||
using ARMeilleure.Memory;
|
using ARMeilleure.Memory;
|
||||||
using ARMeilleure.Native;
|
using ARMeilleure.Native;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -18,7 +19,7 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
private static readonly int _pageMask = _pageSize - 4;
|
private static readonly int _pageMask = _pageSize - 4;
|
||||||
|
|
||||||
private const int CodeAlignment = 4; // Bytes.
|
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 const int CacheSizeIOS = 128 * 1024 * 1024;
|
||||||
|
|
||||||
private static ReservedRegion _jitRegion;
|
private static ReservedRegion _jitRegion;
|
||||||
@ -31,6 +32,10 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
private static readonly object _lock = new();
|
private static readonly object _lock = new();
|
||||||
private static bool _initialized;
|
private static bool _initialized;
|
||||||
|
|
||||||
|
private static readonly List<ReservedRegion> _jitRegions = new();
|
||||||
|
|
||||||
|
private static int _activeRegionIndex = 0;
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
[SupportedOSPlatform("windows")]
|
||||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize);
|
public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize);
|
||||||
@ -49,7 +54,11 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize));
|
var firstRegion = new ReservedRegion(allocator, CacheSize);
|
||||||
|
|
||||||
|
|
||||||
|
_jitRegions.Add(firstRegion);
|
||||||
|
_activeRegionIndex = 0;
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
|
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
@ -60,7 +69,9 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
|
JitUnwindWindows.InstallFunctionTableHandler(
|
||||||
|
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
@ -73,7 +84,9 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
{
|
{
|
||||||
while (_deferredRxProtect.TryDequeue(out var result))
|
while (_deferredRxProtect.TryDequeue(out var result))
|
||||||
{
|
{
|
||||||
ReprotectAsExecutable(result.funcOffset, result.length);
|
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
||||||
|
|
||||||
|
ReprotectAsExecutable(targetRegion, result.funcOffset, result.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,21 +100,14 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
|
|
||||||
int funcOffset = Allocate(code.Length, deferProtect);
|
int funcOffset = Allocate(code.Length, deferProtect);
|
||||||
|
|
||||||
IntPtr funcPtr = _jitRegion.Pointer + funcOffset;
|
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
|
||||||
|
IntPtr funcPtr = targetRegion.Pointer + funcOffset;
|
||||||
|
|
||||||
if (OperatingSystem.IsIOS())
|
if (OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
Marshal.Copy(code, 0, funcPtr, code.Length);
|
Marshal.Copy(code, 0, funcPtr, code.Length);
|
||||||
if (deferProtect)
|
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
||||||
{
|
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
|
||||||
_deferredRxProtect.Enqueue((funcOffset, code.Length));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ReprotectAsExecutable(funcOffset, code.Length);
|
|
||||||
|
|
||||||
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
else if (OperatingSystem.IsMacOS()&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
{
|
{
|
||||||
@ -115,9 +121,9 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ReprotectAsWritable(funcOffset, code.Length);
|
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
|
||||||
Marshal.Copy(code, 0, funcPtr, code.Length);
|
Marshal.Copy(code, 0, funcPtr, code.Length);
|
||||||
ReprotectAsExecutable(funcOffset, code.Length);
|
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
|
||||||
{
|
{
|
||||||
@ -139,41 +145,50 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
{
|
{
|
||||||
if (OperatingSystem.IsIOS())
|
if (OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
return;
|
// return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
Debug.Assert(_initialized);
|
foreach (var region in _jitRegions)
|
||||||
|
|
||||||
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
|
|
||||||
|
|
||||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
|
||||||
{
|
{
|
||||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
|
||||||
_cacheEntries.RemoveAt(entryIndex);
|
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
|
||||||
|
|
||||||
|
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||||
|
{
|
||||||
|
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||||
|
_cacheEntries.RemoveAt(entryIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReprotectAsWritable(int offset, int size)
|
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
|
||||||
{
|
{
|
||||||
int endOffs = offset + size;
|
int endOffs = offset + size;
|
||||||
|
|
||||||
int regionStart = offset & ~_pageMask;
|
int regionStart = offset & ~_pageMask;
|
||||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||||
|
|
||||||
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReprotectAsExecutable(int offset, int size)
|
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
|
||||||
{
|
{
|
||||||
int endOffs = offset + size;
|
int endOffs = offset + size;
|
||||||
|
|
||||||
int regionStart = offset & ~_pageMask;
|
int regionStart = offset & ~_pageMask;
|
||||||
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
|
||||||
|
|
||||||
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int Allocate(int codeSize, bool deferProtect = false)
|
private static int Allocate(int codeSize, bool deferProtect = false)
|
||||||
@ -187,20 +202,35 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
alignment = 0x4000;
|
alignment = 0x4000;
|
||||||
}
|
}
|
||||||
|
|
||||||
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
|
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
|
||||||
|
|
||||||
//DEBUG: Show JIT Memory Allocation
|
|
||||||
|
|
||||||
//Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}");
|
|
||||||
|
|
||||||
if (allocOffset < 0)
|
|
||||||
{
|
{
|
||||||
throw new OutOfMemoryException("JIT Cache exhausted.");
|
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
|
||||||
|
|
||||||
|
if (allocOffset >= 0)
|
||||||
|
{
|
||||||
|
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||||
|
_activeRegionIndex = i;
|
||||||
|
return allocOffset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
int exhaustedRegion = _activeRegionIndex;
|
||||||
|
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
|
||||||
|
_jitRegions.Add(newRegion);
|
||||||
|
_activeRegionIndex = _jitRegions.Count - 1;
|
||||||
|
|
||||||
return allocOffset;
|
int newRegionNumber = _activeRegionIndex;
|
||||||
|
|
||||||
|
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
|
||||||
|
|
||||||
|
int allocOffsetNew = _cacheAllocator.Allocate(ref codeSize, alignment);
|
||||||
|
if (allocOffsetNew < 0)
|
||||||
|
{
|
||||||
|
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
|
||||||
|
}
|
||||||
|
|
||||||
|
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
|
||||||
|
return allocOffsetNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int AlignCodeSize(int codeSize, bool deferProtect = false)
|
private static int AlignCodeSize(int codeSize, bool deferProtect = false)
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
4E80A99D2CD6F54700029585 /* MeloNXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
4E80A99D2CD6F54700029585 /* MeloNXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
4E80A9A72CD6F54700029585 /* MeloNXUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
4E80A9A72CD6F54700029585 /* MeloNXUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeloNXUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
4E80AA622CD7122800029585 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = System/Library/Frameworks/GameController.framework; sourceTree = SDKROOT; };
|
4E80AA622CD7122800029585 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = System/Library/Frameworks/GameController.framework; sourceTree = SDKROOT; };
|
||||||
5650564A2D2A758600C8BB1E /* dotnet.xcconfig.example */ = {isa = PBXFileReference; lastKnownFileType = text; path = dotnet.xcconfig.example; sourceTree = "<group>"; };
|
5650564A2D2A758600C8BB1E /* dotnet.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = dotnet.xcconfig; sourceTree = "<group>"; };
|
||||||
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = Ryujinx.Headless.SDL2.dylib; path = "MeloNX/Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib"; sourceTree = "<group>"; };
|
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = Ryujinx.Headless.SDL2.dylib; path = "MeloNX/Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@ -222,7 +222,7 @@
|
|||||||
4E80A9842CD6F54500029585 = {
|
4E80A9842CD6F54500029585 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5650564A2D2A758600C8BB1E /* dotnet.xcconfig.example */,
|
5650564A2D2A758600C8BB1E /* dotnet.xcconfig */,
|
||||||
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */,
|
BD43C6282D1B2514003BBC42 /* Ryujinx.Headless.SDL2.dylib */,
|
||||||
4E80A98F2CD6F54500029585 /* MeloNX */,
|
4E80A98F2CD6F54500029585 /* MeloNX */,
|
||||||
4E80A9A02CD6F54700029585 /* MeloNXTests */,
|
4E80A9A02CD6F54700029585 /* MeloNXTests */,
|
||||||
@ -260,7 +260,7 @@
|
|||||||
buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */;
|
buildConfigurationList = BD43C61E2D1B23AB003BBC42 /* Build configuration list for PBXLegacyTarget "Ryujinx" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
);
|
);
|
||||||
buildToolPath = "$(DOTNET_PATH)";
|
buildToolPath = /usr/local/share/dotnet/dotnet;
|
||||||
buildWorkingDirectory = "$(SRCROOT)/../..";
|
buildWorkingDirectory = "$(SRCROOT)/../..";
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
@ -666,6 +666,14 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = fast;
|
GCC_OPTIMIZATION_LEVEL = fast;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -681,7 +689,7 @@
|
|||||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -737,8 +745,24 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(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_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@ -790,6 +814,14 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = fast;
|
GCC_OPTIMIZATION_LEVEL = fast;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -805,7 +837,7 @@
|
|||||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -861,8 +893,24 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(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_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
Binary file not shown.
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>4</integer>
|
|
||||||
</dict>
|
|
||||||
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>3</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>
|
|
@ -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>
|
|
@ -14,8 +14,6 @@
|
|||||||
|
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <SDL2/SDL_syswm.h>
|
#include <SDL2/SDL_syswm.h>
|
||||||
#import "utils.h"
|
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@ -31,8 +29,21 @@ struct GameInfo {
|
|||||||
unsigned int ImageSize;
|
unsigned int ImageSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct DlcNcaListItem {
|
||||||
|
char Path[256];
|
||||||
|
unsigned long TitleId;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DlcNcaList {
|
||||||
|
bool success;
|
||||||
|
unsigned int size;
|
||||||
|
struct DlcNcaListItem* items;
|
||||||
|
};
|
||||||
|
|
||||||
extern struct GameInfo get_game_info(int, char*);
|
extern struct GameInfo get_game_info(int, char*);
|
||||||
|
|
||||||
|
extern struct DlcNcaList get_dlc_nca_list(const char* titleIdPtr, const char* pathPtr);
|
||||||
|
|
||||||
void install_firmware(const char* inputPtr);
|
void install_firmware(const char* inputPtr);
|
||||||
|
|
||||||
char* installed_firmware_version();
|
char* installed_firmware_version();
|
||||||
@ -43,8 +54,6 @@ int main_ryujinx_sdl(int argc, char **argv);
|
|||||||
|
|
||||||
int get_current_fps();
|
int get_current_fps();
|
||||||
|
|
||||||
void set_title_update(const char* titleIdPtr, const char* updatePathPtr);
|
|
||||||
|
|
||||||
void initialize();
|
void initialize();
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -5,15 +5,49 @@
|
|||||||
// Created by Stossy11 on 10/02/2025.
|
// Created by Stossy11 on 10/02/2025.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
|
||||||
|
var region: vm_address_t = vm_address_t(UInt(bitPattern: address))
|
||||||
|
var regionSize: vm_size_t = 0
|
||||||
|
var info = vm_region_basic_info_64()
|
||||||
|
var infoCount = mach_msg_type_number_t(MemoryLayout<vm_region_basic_info_64>.size / MemoryLayout<integer_t>.size)
|
||||||
|
var objectName: mach_port_t = UInt32(MACH_PORT_NULL)
|
||||||
|
|
||||||
|
let result = withUnsafeMutablePointer(to: &info) {
|
||||||
|
$0.withMemoryRebound(to: integer_t.self, capacity: Int(infoCount)) {
|
||||||
|
vm_region_64(mach_task_self_, ®ion, ®ionSize, VM_REGION_BASIC_INFO_64, $0, &infoCount, &objectName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != KERN_SUCCESS {
|
||||||
|
print("Failed to reach \(address)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.protection & VM_PROT_EXECUTE != 0
|
||||||
|
}
|
||||||
|
|
||||||
func isJITEnabled() -> Bool {
|
func isJITEnabled() -> Bool {
|
||||||
var flags: Int = 0
|
let pageSize = sysconf(_SC_PAGESIZE)
|
||||||
|
let code: [UInt32] = [0x52800540, 0xD65F03C0]
|
||||||
|
|
||||||
csops(getpid(), 0, &flags, sizeof(flags))
|
guard let jitMemory = mmap(nil, pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0), jitMemory != MAP_FAILED else {
|
||||||
return (Int32(flags) & CS_DEBUGGED) != 0;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func sizeof<T>(_ value: T) -> Int {
|
defer {
|
||||||
return MemoryLayout<T>.size
|
munmap(jitMemory, pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
memcpy(jitMemory, code, code.count)
|
||||||
|
|
||||||
|
if mprotect(jitMemory, pageSize, PROT_READ | PROT_EXEC) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkMem = checkMemoryPermissions(at: jitMemory)
|
||||||
|
|
||||||
|
return checkMem
|
||||||
}
|
}
|
||||||
|
@ -1,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);
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -78,7 +78,7 @@ class VirtualController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func rumble(lowFreq: Float, highFreq: Float) {
|
static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
|
||||||
do {
|
do {
|
||||||
// Low-frequency haptic pattern
|
// Low-frequency haptic pattern
|
||||||
let lowFreqPattern = try CHHapticPattern(events: [
|
let lowFreqPattern = try CHHapticPattern(events: [
|
||||||
@ -96,9 +96,23 @@ class VirtualController {
|
|||||||
], relativeTime: 0.2, duration: 0.2)
|
], relativeTime: 0.2, duration: 0.2)
|
||||||
], parameters: [])
|
], parameters: [])
|
||||||
|
|
||||||
// Create and start the haptic engine
|
// Mutable engine
|
||||||
let engine = try CHHapticEngine()
|
var engine = engine
|
||||||
try engine.start()
|
|
||||||
|
// If no engine passed, use device engine
|
||||||
|
if engine == nil {
|
||||||
|
// Create and start the haptic engine
|
||||||
|
if hapticEngine == nil {
|
||||||
|
hapticEngine = try CHHapticEngine()
|
||||||
|
try hapticEngine?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
engine = hapticEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let engine else {
|
||||||
|
return print("Error creating haptic patterns: hapticEngine is nil")
|
||||||
|
}
|
||||||
|
|
||||||
// Create and play the low-frequency player
|
// Create and play the low-frequency player
|
||||||
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
|
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
|
||||||
@ -113,6 +127,8 @@ class VirtualController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static var hapticEngine: CHHapticEngine?
|
||||||
|
|
||||||
|
|
||||||
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
|
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
|
||||||
guard controller != nil else { return }
|
guard controller != nil else { return }
|
||||||
|
@ -77,6 +77,7 @@ class Ryujinx {
|
|||||||
var disablevsync: Bool
|
var disablevsync: Bool
|
||||||
var language: SystemLanguage
|
var language: SystemLanguage
|
||||||
var regioncode: SystemRegionCode
|
var regioncode: SystemRegionCode
|
||||||
|
var handHeldController: Bool
|
||||||
|
|
||||||
|
|
||||||
init(gamepath: String,
|
init(gamepath: String,
|
||||||
@ -102,7 +103,8 @@ class Ryujinx {
|
|||||||
disablePTC: Bool = false,
|
disablePTC: Bool = false,
|
||||||
disablevsync: Bool = false,
|
disablevsync: Bool = false,
|
||||||
language: SystemLanguage = .americanEnglish,
|
language: SystemLanguage = .americanEnglish,
|
||||||
regioncode: SystemRegionCode = .usa
|
regioncode: SystemRegionCode = .usa,
|
||||||
|
handHeldController: Bool = false
|
||||||
) {
|
) {
|
||||||
self.gamepath = gamepath
|
self.gamepath = gamepath
|
||||||
self.inputids = inputids
|
self.inputids = inputids
|
||||||
@ -128,6 +130,7 @@ class Ryujinx {
|
|||||||
self.disablevsync = disablevsync
|
self.disablevsync = disablevsync
|
||||||
self.language = language
|
self.language = language
|
||||||
self.regioncode = regioncode
|
self.regioncode = regioncode
|
||||||
|
self.handHeldController = handHeldController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,7 +225,7 @@ class Ryujinx {
|
|||||||
print(error)
|
print(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return games
|
return games
|
||||||
} catch {
|
} catch {
|
||||||
print("Error loading games from roms folder: \(error)")
|
print("Error loading games from roms folder: \(error)")
|
||||||
@ -321,10 +324,14 @@ class Ryujinx {
|
|||||||
args.append("--list-inputs-ids")
|
args.append("--list-inputs-ids")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the input ids (limit to 4 just in case)
|
// Append the input ids (limit to 8 (used to be 4) just in case)
|
||||||
if !config.inputids.isEmpty {
|
if !config.inputids.isEmpty {
|
||||||
config.inputids.prefix(4).enumerated().forEach { index, inputId in
|
config.inputids.prefix(8).enumerated().forEach { index, inputId in
|
||||||
args.append(contentsOf: ["--input-id-\(index + 1)", inputId])
|
if config.handHeldController {
|
||||||
|
args.append(contentsOf: ["\(index == 0 ? "--input-id-handheld" : "--input-id-\(index + 1)")", inputId])
|
||||||
|
} else {
|
||||||
|
args.append(contentsOf: ["--input-id-\(index + 1)", inputId])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,19 +372,30 @@ class Ryujinx {
|
|||||||
self.firmwareversion = version
|
self.firmwareversion = version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDlcNcaList(titleId: String, path: String) -> [DownloadableContentNca] {
|
||||||
func setTitleUpdate(titleId: String, updatePath: String) {
|
guard let titleIdCString = titleId.cString(using: .utf8),
|
||||||
guard let titleIdPtr = titleId.cString(using: .utf8),
|
let pathCString = path.cString(using: .utf8)
|
||||||
let updatePathPtr = updatePath.cString(using: .utf8)
|
|
||||||
else {
|
else {
|
||||||
print("Invalid firmware path")
|
print("Invalid path")
|
||||||
return
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
set_title_update(titleIdPtr, updatePathPtr)
|
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? {
|
private func generateGamepadId(joystickIndex: Int32) -> String? {
|
||||||
let guid = SDL_JoystickGetDeviceGUID(joystickIndex)
|
let guid = SDL_JoystickGetDeviceGUID(joystickIndex)
|
||||||
|
|
||||||
@ -466,23 +484,26 @@ class Ryujinx {
|
|||||||
|
|
||||||
|
|
||||||
func repeatuntilfindLayer() {
|
func repeatuntilfindLayer() {
|
||||||
DispatchQueue.global(qos: .background).async {
|
Task { @MainActor in
|
||||||
while self.metalLayer == nil {
|
while self.metalLayer == nil {
|
||||||
let layer = self.getMetalLayer(nil)
|
let layer = self.getMetalLayer(nil)
|
||||||
|
|
||||||
if layer != nil {
|
if layer != nil {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.metalLayer = layer
|
self.metalLayer = layer
|
||||||
}
|
}
|
||||||
|
self.metalLayer = layer
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.sleep(forTimeInterval: 0.1)
|
Thread.sleep(forTimeInterval: 0.1)
|
||||||
|
try await Task.sleep(nanoseconds: 100_000_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? {
|
func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? {
|
||||||
var window = window
|
var window = window
|
||||||
if window == nil {
|
if window == nil {
|
||||||
|
@ -9,7 +9,7 @@ import SwiftUI
|
|||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
public struct Game: Identifiable, Equatable, Hashable {
|
public struct Game: Identifiable, Equatable, Hashable {
|
||||||
public var id = UUID()
|
public var id: URL { fileURL }
|
||||||
|
|
||||||
var containerFolder: URL
|
var containerFolder: URL
|
||||||
var fileType: UTType
|
var fileType: UTType
|
||||||
|
@ -26,6 +26,7 @@ struct ContentView: View {
|
|||||||
@State private var controllersList: [Controller] = []
|
@State private var controllersList: [Controller] = []
|
||||||
@State private var currentControllers: [Controller] = []
|
@State private var currentControllers: [Controller] = []
|
||||||
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
||||||
|
@State var nativeControllers: [GCController: NativeController] = [:]
|
||||||
@State private var isVirtualControllerActive: Bool = false
|
@State private var isVirtualControllerActive: Bool = false
|
||||||
@AppStorage("isVirtualController") var isVCA: Bool = true
|
@AppStorage("isVirtualController") var isVCA: Bool = true
|
||||||
|
|
||||||
@ -50,31 +51,23 @@ struct ContentView: View {
|
|||||||
private let animationDuration: Double = 1.0
|
private let animationDuration: Double = 1.0
|
||||||
@State private var isAnimating = false
|
@State private var isAnimating = false
|
||||||
@State var isLoading = true
|
@State var isLoading = true
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
init() {
|
init() {
|
||||||
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
||||||
_config = State(initialValue: defaultConfig)
|
_config = State(initialValue: defaultConfig)
|
||||||
|
|
||||||
let defaultSettings: [MoltenVKSettings] = [
|
let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings.
|
||||||
// MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"),
|
|
||||||
// MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2"),
|
|
||||||
// Metal Private API isn't needed and causes more stutters
|
|
||||||
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
|
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
|
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
|
||||||
MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
|
MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
|
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
|
||||||
// MoltenVKSettings(string: "MVK_CONFIG_LOG_LEVEL", value: "0"),
|
// Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64)
|
||||||
// MVK_CONFIG_LOG_LEVEL
|
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "128"),
|
||||||
//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"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
_settings = State(initialValue: defaultSettings)
|
_settings = State(initialValue: defaultSettings)
|
||||||
|
|
||||||
print("JIT Enabled: \(isJITEnabled())")
|
|
||||||
|
|
||||||
initializeSDL()
|
initializeSDL()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,20 +83,6 @@ struct ContentView: View {
|
|||||||
} else {
|
} else {
|
||||||
ZStack {
|
ZStack {
|
||||||
emulationView
|
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 {
|
} else {
|
||||||
@ -115,10 +94,10 @@ struct ContentView: View {
|
|||||||
isAnimating = false
|
isAnimating = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
VStack {
|
EmulationView()
|
||||||
|
.onAppear() {
|
||||||
}
|
isAnimating = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -152,6 +131,7 @@ struct ContentView: View {
|
|||||||
queue: .main) { notification in
|
queue: .main) { notification in
|
||||||
if let controller = notification.object as? GCController {
|
if let controller = notification.object as? GCController {
|
||||||
print("Controller connected: \(controller.productCategory)")
|
print("Controller connected: \(controller.productCategory)")
|
||||||
|
nativeControllers[controller] = .init(controller)
|
||||||
refreshControllersList()
|
refreshControllersList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,6 +143,8 @@ struct ContentView: View {
|
|||||||
queue: .main) { notification in
|
queue: .main) { notification in
|
||||||
if let controller = notification.object as? GCController {
|
if let controller = notification.object as? GCController {
|
||||||
print("Controller disconnected: \(controller.productCategory)")
|
print("Controller disconnected: \(controller.productCategory)")
|
||||||
|
nativeControllers[controller]?.cleanup()
|
||||||
|
nativeControllers[controller] = nil
|
||||||
refreshControllersList()
|
refreshControllersList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,14 +175,12 @@ struct ContentView: View {
|
|||||||
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
|
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
|
||||||
|
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
// Background track
|
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
.frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
||||||
.foregroundColor(.gray.opacity(0.3))
|
.foregroundColor(.gray.opacity(0.3))
|
||||||
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
|
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
|
||||||
|
|
||||||
// Animated loading bar
|
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
.frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
||||||
@ -268,15 +248,9 @@ struct ContentView: View {
|
|||||||
))
|
))
|
||||||
|
|
||||||
let isJIT = isJITEnabled()
|
let isJIT = isJITEnabled()
|
||||||
|
if !isJIT {
|
||||||
if !isJIT, useTrollStore {
|
useTrollStore ? askForJIT() : enableJITEB()
|
||||||
askForJIT()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isJIT, jitStreamerEB {
|
|
||||||
enableJITEB()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,7 +259,7 @@ struct ContentView: View {
|
|||||||
private func initializeSDL() {
|
private func initializeSDL() {
|
||||||
setMoltenVKSettings()
|
setMoltenVKSettings()
|
||||||
SDL_SetMainReady() // Sets SDL Ready
|
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
|
SDL_Init(SdlInitFlags) // Initialises SDL2
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
@ -306,8 +280,9 @@ struct ContentView: View {
|
|||||||
self.onscreencontroller = onscreen
|
self.onscreencontroller = onscreen
|
||||||
}
|
}
|
||||||
|
|
||||||
controllersList.removeAll(where: { $0.id == "0"})
|
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
|
||||||
|
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
|
||||||
|
|
||||||
currentControllers = []
|
currentControllers = []
|
||||||
|
|
||||||
if controllersList.count == 1 {
|
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) {
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,45 +14,52 @@ struct GameInfoSheet: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
iOSNav {
|
||||||
VStack {
|
List {
|
||||||
if let icon = game.icon {
|
Section {}
|
||||||
Image(uiImage: icon)
|
header: {
|
||||||
.resizable()
|
VStack(alignment: .center) {
|
||||||
.aspectRatio(contentMode: .fit)
|
if let icon = game.icon {
|
||||||
.frame(width: 250, height: 250)
|
Image(uiImage: icon)
|
||||||
.cornerRadius(10)
|
.resizable()
|
||||||
.padding()
|
.aspectRatio(contentMode: .fit)
|
||||||
.contextMenu {
|
.frame(width: 250, height: 250)
|
||||||
Button {
|
.cornerRadius(10)
|
||||||
UIImageWriteToSavedPhotosAlbum(icon, nil, nil, nil)
|
.padding()
|
||||||
} label: {
|
.contextMenu {
|
||||||
Label("Save to Photos", systemImage: "square.and.arrow.down")
|
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 {
|
VStack(alignment: .center) {
|
||||||
Image(systemName: "questionmark.circle")
|
Text("**\(game.titleName)** | \(game.titleId.capitalized)")
|
||||||
.resizable()
|
.multilineTextAlignment(.center)
|
||||||
.aspectRatio(contentMode: .fit)
|
Text(game.developer)
|
||||||
.frame(width: 150, height: 150)
|
.font(.caption)
|
||||||
.padding()
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, 3)
|
||||||
VStack(alignment: .leading) {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text("**\(game.titleName)** | \(game.titleId.capitalized)")
|
|
||||||
Text(game.developer)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 3)
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
|
||||||
Text("Information")
|
Section {
|
||||||
.font(.title2)
|
HStack {
|
||||||
.bold()
|
Text("**Version**")
|
||||||
|
Spacer()
|
||||||
Text("**Version:** \(game.version)")
|
Text(game.version)
|
||||||
Text("**Title ID:** \(game.titleId)")
|
.foregroundStyle(Color.secondary)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Text("**Title ID**")
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = game.titleId
|
UIPasteboard.general.string = game.titleId
|
||||||
@ -60,15 +67,32 @@ struct GameInfoSheet: View {
|
|||||||
Text("Copy Title ID")
|
Text("Copy Title ID")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes")
|
Spacer()
|
||||||
Text("**File Type:** .\(getFileType(game.fileURL))")
|
Text(game.titleId)
|
||||||
Text("**Game URL:** \(trimGameURL(game.fileURL))")
|
.foregroundStyle(Color.secondary)
|
||||||
}
|
}
|
||||||
|
HStack {
|
||||||
|
Text("**Game Size**")
|
||||||
|
Spacer()
|
||||||
|
Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes")
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Text("**File Type**")
|
||||||
|
Spacer()
|
||||||
|
Text(getFileType(game.fileURL))
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("**Game URL**")
|
||||||
|
Text(trimGameURL(game.fileURL))
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Information")
|
||||||
}
|
}
|
||||||
|
.headerProminence(.increased)
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 5)
|
|
||||||
.navigationTitle(game.titleName)
|
.navigationTitle(game.titleName)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@ -103,10 +127,6 @@ struct GameInfoSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getFileType(_ url: URL) -> String {
|
func getFileType(_ url: URL) -> String {
|
||||||
let path = url.path
|
url.pathExtension
|
||||||
if let range = path.range(of: ".") {
|
|
||||||
return String(path[range.upperBound...])
|
|
||||||
}
|
|
||||||
return "Unknown"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ struct GameLibraryView: View {
|
|||||||
@State var isSelectingGameFile = false
|
@State var isSelectingGameFile = false
|
||||||
@State var isViewingGameInfo: Bool = false
|
@State var isViewingGameInfo: Bool = false
|
||||||
@State var isSelectingGameUpdate: Bool = false
|
@State var isSelectingGameUpdate: Bool = false
|
||||||
|
@State var isSelectingGameDLC: Bool = false
|
||||||
@State var gameInfo: Game?
|
@State var gameInfo: Game?
|
||||||
var games: Binding<[Game]> {
|
var games: Binding<[Game]> {
|
||||||
Binding(
|
Binding(
|
||||||
@ -38,121 +39,107 @@ struct GameLibraryView: View {
|
|||||||
|
|
||||||
var filteredGames: [Game] {
|
var filteredGames: [Game] {
|
||||||
if searchText.isEmpty {
|
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 {
|
return Ryujinx.shared.games.filter {
|
||||||
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
|
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
|
||||||
$0.developer.localizedCaseInsensitiveContains(searchText)
|
$0.developer.localizedCaseInsensitiveContains(searchText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var realRecentGames: [Game] {
|
||||||
|
let games = Ryujinx.shared.games
|
||||||
|
return recentGames.compactMap { recentGame in
|
||||||
|
games.first(where: { $0.fileURL == recentGame.fileURL })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
iOSNav {
|
||||||
ScrollView {
|
List {
|
||||||
LazyVStack(alignment: .leading, spacing: 20) {
|
if Ryujinx.shared.games.isEmpty {
|
||||||
if !isSearching {
|
VStack(spacing: 16) {
|
||||||
Text("Games")
|
Image(systemName: "gamecontroller.fill")
|
||||||
.font(.system(size: 34, weight: .bold))
|
.font(.system(size: 64))
|
||||||
.padding(.horizontal)
|
.foregroundColor(.secondary.opacity(0.7))
|
||||||
.padding(.top, 12)
|
.padding(.top, 60)
|
||||||
|
Text("No Games Found")
|
||||||
|
.font(.title2.bold())
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text("Add ROM, Keys and Firmware to get started")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
if Ryujinx.shared.games.isEmpty {
|
.padding(.top, 40)
|
||||||
VStack(spacing: 16) {
|
} else {
|
||||||
Image(systemName: "gamecontroller.fill")
|
if !isSearching && !realRecentGames.isEmpty {
|
||||||
.font(.system(size: 64))
|
Section {
|
||||||
.foregroundColor(.secondary.opacity(0.7))
|
ForEach(realRecentGames) { game in
|
||||||
.padding(.top, 60)
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
||||||
Text("No Games Found")
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
.font(.title2.bold())
|
Button(role: .destructive) {
|
||||||
.foregroundColor(.primary)
|
removeFromRecentGames(game)
|
||||||
Text("Add ROM, Keys and Firmware to get started")
|
} label: {
|
||||||
.font(.subheadline)
|
Label("Delete", systemImage: "trash")
|
||||||
.foregroundColor(.secondary)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Recent")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
ForEach(filteredGames) { game in
|
||||||
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Others")
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.top, 40)
|
|
||||||
} else {
|
} else {
|
||||||
if !isSearching && !recentGames.isEmpty {
|
ForEach(filteredGames) { game in
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
||||||
Text("Recent")
|
|
||||||
.font(.title2.bold())
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
LazyHStack(spacing: 16) {
|
|
||||||
ForEach(recentGames) { game in
|
|
||||||
RecentGameCard(game: game, startemu: $startemu)
|
|
||||||
.onTapGesture {
|
|
||||||
addToRecentGames(game)
|
|
||||||
startemu = game
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("All Games")
|
|
||||||
.font(.title2.bold())
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
LazyVStack(spacing: 2) {
|
|
||||||
ForEach(filteredGames) { game in
|
|
||||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
|
|
||||||
.onTapGesture {
|
|
||||||
addToRecentGames(game)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LazyVStack(spacing: 2) {
|
|
||||||
ForEach(filteredGames) { game in
|
|
||||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
|
|
||||||
.onTapGesture {
|
|
||||||
addToRecentGames(game)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
}
|
||||||
loadRecentGames()
|
.navigationTitle("Games")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
.onAppear {
|
||||||
firmwareversion = (firmware == "" ? "0" : firmware)
|
loadRecentGames()
|
||||||
}
|
|
||||||
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
|
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||||
switch result {
|
firmwareversion = (firmware == "" ? "0" : firmware)
|
||||||
case .success(let url):
|
}
|
||||||
do {
|
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
|
||||||
let fun = url.startAccessingSecurityScopedResource()
|
switch result {
|
||||||
let path = url.path
|
case .success(let url):
|
||||||
|
do {
|
||||||
Ryujinx.shared.installFirmware(firmwarePath: path)
|
let fun = url.startAccessingSecurityScopedResource()
|
||||||
|
let path = url.path
|
||||||
firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
|
|
||||||
if fun {
|
Ryujinx.shared.installFirmware(firmwarePath: path)
|
||||||
url.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
|
||||||
}
|
if fun {
|
||||||
case .failure(let error):
|
url.stopAccessingSecurityScopedResource()
|
||||||
print(error)
|
}
|
||||||
}
|
}
|
||||||
}
|
case .failure(let error):
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .topBarLeading) {
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
isSelectingGameFile.toggle()
|
isSelectingGameFile.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .topBarLeading) {
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
Menu {
|
Menu {
|
||||||
Text("Firmware Version: \(firmwareversion)")
|
Text("Firmware Version: \(firmwareversion)")
|
||||||
@ -214,9 +201,13 @@ struct GameLibraryView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: startemu) { game in
|
||||||
|
guard let game else { return }
|
||||||
|
addToRecentGames(game)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
.searchable(text: $searchText)
|
.searchable(text: $searchText)
|
||||||
|
.animation(.easeInOut, value: searchText)
|
||||||
.onChange(of: searchText) { _ in
|
.onChange(of: searchText) { _ in
|
||||||
isSearching = !searchText.isEmpty
|
isSearching = !searchText.isEmpty
|
||||||
}
|
}
|
||||||
@ -281,6 +272,9 @@ struct GameLibraryView: View {
|
|||||||
.sheet(isPresented: $isSelectingGameUpdate) {
|
.sheet(isPresented: $isSelectingGameUpdate) {
|
||||||
UpdateManagerSheet(game: $gameInfo)
|
UpdateManagerSheet(game: $gameInfo)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $isSelectingGameDLC) {
|
||||||
|
DLCManagerSheet(game: $gameInfo)
|
||||||
|
}
|
||||||
.sheet(isPresented: Binding(
|
.sheet(isPresented: Binding(
|
||||||
get: { isViewingGameInfo && gameInfo != nil },
|
get: { isViewingGameInfo && gameInfo != nil },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
@ -296,10 +290,9 @@ struct GameLibraryView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func addToRecentGames(_ game: Game) {
|
private func addToRecentGames(_ game: Game) {
|
||||||
recentGames.removeAll { $0.id == game.id }
|
recentGames.removeAll { $0.titleId == game.titleId }
|
||||||
|
|
||||||
recentGames.insert(game, at: 0)
|
recentGames.insert(game, at: 0)
|
||||||
|
|
||||||
if recentGames.count > 5 {
|
if recentGames.count > 5 {
|
||||||
@ -308,7 +301,12 @@ struct GameLibraryView: View {
|
|||||||
|
|
||||||
saveRecentGames()
|
saveRecentGames()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func removeFromRecentGames(_ game: Game) {
|
||||||
|
recentGames.removeAll { $0.titleId == game.titleId }
|
||||||
|
saveRecentGames()
|
||||||
|
}
|
||||||
|
|
||||||
private func saveRecentGames() {
|
private func saveRecentGames() {
|
||||||
do {
|
do {
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
@ -329,8 +327,7 @@ struct GameLibraryView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Delete Game Function
|
||||||
// MARK: - Delete Game Function
|
|
||||||
func deleteGame(game: Game) {
|
func deleteGame(game: Game) {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
do {
|
do {
|
||||||
@ -343,7 +340,7 @@ struct GameLibraryView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -Game Model
|
// MARK: - Game Model
|
||||||
extension Game: Codable {
|
extension Game: Codable {
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case titleName, titleId, developer, version, fileURL
|
case titleName, titleId, developer, version, fileURL
|
||||||
@ -372,65 +369,21 @@ extension Game: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -Recent Game Card
|
// MARK: - Game List Item
|
||||||
struct RecentGameCard: View {
|
|
||||||
let game: Game
|
|
||||||
@Binding var startemu: Game?
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: {
|
|
||||||
startemu = game
|
|
||||||
}) {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
if let icon = game.icon {
|
|
||||||
Image(uiImage: icon)
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
.frame(width: 140, height: 140)
|
|
||||||
.cornerRadius(12)
|
|
||||||
} else {
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(colorScheme == .dark ?
|
|
||||||
Color(.systemGray5) : Color(.systemGray6))
|
|
||||||
.frame(width: 140, height: 140)
|
|
||||||
|
|
||||||
Image(systemName: "gamecontroller.fill")
|
|
||||||
.font(.system(size: 40))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(game.titleName)
|
|
||||||
.font(.subheadline.bold())
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
Text(game.developer)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -Game List Item
|
|
||||||
struct GameListRow: View {
|
struct GameListRow: View {
|
||||||
let game: Game
|
let game: Game
|
||||||
@Binding var startemu: Game?
|
@Binding var startemu: Game?
|
||||||
@Binding var games: [Game] // Add this binding
|
@Binding var games: [Game] // Add this binding
|
||||||
@Binding var isViewingGameInfo: Bool
|
@Binding var isViewingGameInfo: Bool
|
||||||
@Binding var isSelectingGameUpdate: Bool
|
@Binding var isSelectingGameUpdate: Bool
|
||||||
|
@Binding var isSelectingGameDLC: Bool
|
||||||
@Binding var gameInfo: Game?
|
@Binding var gameInfo: Game?
|
||||||
@State var gametoDelete: Game?
|
@State var gametoDelete: Game?
|
||||||
@State var showGameDeleteConfirmation: Bool = false
|
@State var showGameDeleteConfirmation: Bool = false
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
@AppStorage("portal") var gamepo = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
startemu = game
|
startemu = game
|
||||||
@ -447,7 +400,7 @@ struct GameListRow: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.fill(colorScheme == .dark ?
|
.fill(colorScheme == .dark ?
|
||||||
Color(.systemGray5) : Color(.systemGray6))
|
Color(.systemGray5) : Color(.systemGray6))
|
||||||
.frame(width: 45, height: 45)
|
.frame(width: 45, height: 45)
|
||||||
|
|
||||||
Image(systemName: "gamecontroller.fill")
|
Image(systemName: "gamecontroller.fill")
|
||||||
@ -474,43 +427,54 @@ struct GameListRow: View {
|
|||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
.opacity(0.8)
|
.opacity(0.8)
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
}
|
||||||
.padding(.vertical, 8)
|
.contextMenu {
|
||||||
.background(Color(.systemBackground))
|
Section {
|
||||||
.contextMenu {
|
Button {
|
||||||
Section {
|
startemu = game
|
||||||
Button {
|
} label: {
|
||||||
startemu = game
|
Label("Play Now", systemImage: "play.fill")
|
||||||
} label: {
|
|
||||||
Label("Play Now", systemImage: "play.fill")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
gameInfo = game
|
|
||||||
isViewingGameInfo.toggle()
|
|
||||||
} label: {
|
|
||||||
Label("Game Info", systemImage: "info.circle")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
gameInfo = game
|
|
||||||
isSelectingGameUpdate.toggle()
|
|
||||||
} label: {
|
|
||||||
Label("Game Update Manager", systemImage: "chevron.up.circle")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Button {
|
||||||
Button(role: .destructive) {
|
gameInfo = game
|
||||||
gametoDelete = game
|
isViewingGameInfo.toggle()
|
||||||
showGameDeleteConfirmation.toggle()
|
|
||||||
} label: {
|
if game.titleName.lowercased() == "portal" {
|
||||||
Label("Delete", systemImage: "trash")
|
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) {
|
.confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
if let game = gametoDelete {
|
if let game = gametoDelete {
|
||||||
@ -533,4 +497,3 @@ struct GameListRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ struct SettingsView: View {
|
|||||||
@State private var showAnisotropicInfo = false
|
@State private var showAnisotropicInfo = false
|
||||||
@State private var showControllerInfo = false
|
@State private var showControllerInfo = false
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
|
@AppStorage("portal") var gamepo = false
|
||||||
|
|
||||||
var filteredMemoryModes: [(String, String)] {
|
var filteredMemoryModes: [(String, String)] {
|
||||||
guard !searchText.isEmpty else { return memoryManagerModes }
|
guard !searchText.isEmpty else { return memoryManagerModes }
|
||||||
@ -272,42 +273,12 @@ struct SettingsView: View {
|
|||||||
Text("Select input devices and on-screen controls to play with. ")
|
Text("Select input devices and on-screen controls to play with. ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input Settings
|
// Input Settings
|
||||||
Section {
|
Section {
|
||||||
|
Toggle(isOn: $config.macroHLE) {
|
||||||
Toggle(isOn: $config.listinputids) {
|
labelWithIcon("Player 1 to Handheld Input", iconName: "formfitting.gamecontroller")
|
||||||
labelWithIcon("List Input IDs", iconName: "list.bullet")
|
}.tint(.blue)
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
|
|
||||||
Toggle(isOn: $ryuDemo) {
|
Toggle(isOn: $ryuDemo) {
|
||||||
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
|
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
|
||||||
@ -363,6 +334,35 @@ struct SettingsView: View {
|
|||||||
Text("Configure input devices and on-screen controls for easier navigation and play.")
|
Text("Configure input devices and on-screen controls for easier navigation and play.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Language and Region Settings
|
||||||
|
Section {
|
||||||
|
Picker(selection: $config.language) {
|
||||||
|
ForEach(SystemLanguage.allCases, id: \.self) { ratio in
|
||||||
|
Text(ratio.displayName).tag(ratio)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
labelWithIcon("Language", iconName: "character.bubble")
|
||||||
|
}
|
||||||
|
|
||||||
|
Picker(selection: $config.regioncode) {
|
||||||
|
ForEach(SystemRegionCode.allCases, id: \.self) { ratio in
|
||||||
|
Text(ratio.displayName).tag(ratio)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
labelWithIcon("Region", iconName: "globe")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// globe
|
||||||
|
} header: {
|
||||||
|
Text("Language and Region Settings")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.textCase(nil)
|
||||||
|
.headerProminence(.increased)
|
||||||
|
} footer: {
|
||||||
|
Text("Configure the System Language and the Region.")
|
||||||
|
}
|
||||||
|
|
||||||
// CPU Mode
|
// CPU Mode
|
||||||
Section {
|
Section {
|
||||||
if filteredMemoryModes.isEmpty {
|
if filteredMemoryModes.isEmpty {
|
||||||
@ -392,7 +392,7 @@ struct SettingsView: View {
|
|||||||
.onAppear() {
|
.onAppear() {
|
||||||
print("CPU Info: \(cpuInfo)")
|
print("CPU Info: \(cpuInfo)")
|
||||||
}
|
}
|
||||||
} else if getEntitlementValue("com.apple.private.hypervisor") {
|
} else if checkAppEntitlement("com.apple.private.hypervisor") {
|
||||||
Toggle(isOn: $config.hypervisor) {
|
Toggle(isOn: $config.hypervisor) {
|
||||||
labelWithIcon("Hypervisor", iconName: "bolt")
|
labelWithIcon("Hypervisor", iconName: "bolt")
|
||||||
}
|
}
|
||||||
@ -514,12 +514,25 @@ struct SettingsView: View {
|
|||||||
// Info
|
// Info
|
||||||
Section {
|
Section {
|
||||||
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
||||||
|
let model = getDeviceModel()
|
||||||
|
let deviceType = model.hasPrefix("iPad") ? "iPadOS" :
|
||||||
|
model.hasPrefix("iPhone") ? "iOS" :
|
||||||
|
"macOS"
|
||||||
|
|
||||||
|
let iconName = model.hasPrefix("iPad") ? "ipad.landscape" :
|
||||||
|
model.hasPrefix("iPhone") ? "iphone" :
|
||||||
|
"macwindow"
|
||||||
|
|
||||||
labelWithIcon("JIT Acquisition: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
|
labelWithIcon("JIT Acquisition: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
|
||||||
|
|
||||||
labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
|
labelWithIcon("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("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill")
|
||||||
|
|
||||||
|
labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo")
|
||||||
|
|
||||||
} header: {
|
} header: {
|
||||||
Text("Information")
|
Text("Information")
|
||||||
.font(.title3.weight(.semibold))
|
.font(.title3.weight(.semibold))
|
||||||
@ -531,12 +544,10 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
Section {
|
Section {
|
||||||
if #unavailable(iOS 17) {
|
Toggle(isOn: $windowCode) {
|
||||||
Toggle(isOn: $windowCode) {
|
labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle")
|
||||||
labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
}
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
|
||||||
DisclosureGroup {
|
DisclosureGroup {
|
||||||
|
|
||||||
@ -589,9 +600,9 @@ struct SettingsView: View {
|
|||||||
.headerProminence(.increased)
|
.headerProminence(.increased)
|
||||||
} footer: {
|
} footer: {
|
||||||
if #available(iOS 17, *) {
|
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 {
|
} 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" : "")")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,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() {
|
func saveSettings() {
|
||||||
#if targetEnvironment(simulator)
|
#if targetEnvironment(simulator)
|
||||||
|
|
||||||
|
168
src/MeloNX/MeloNX/App/Views/Updates/GameDLCManagerSheet.swift
Normal file
168
src/MeloNX/MeloNX/App/Views/Updates/GameDLCManagerSheet.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -15,39 +15,45 @@ struct UpdateManagerSheet: View {
|
|||||||
@Binding var game: Game?
|
@Binding var game: Game?
|
||||||
@State private var isSelectingGameUpdate = false
|
@State private var isSelectingGameUpdate = false
|
||||||
@State private var jsonURL: URL? = nil
|
@State private var jsonURL: URL? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack {
|
List(paths, id: \..self, selection: $selectedItem) { item in
|
||||||
List(paths, id: \..self) { item in
|
Button(action: {
|
||||||
Button(action: {
|
selectItem(item.lastPathComponent)
|
||||||
selectItem(item.lastPathComponent)
|
}) {
|
||||||
}) {
|
HStack {
|
||||||
HStack {
|
Text(item.lastPathComponent)
|
||||||
Text(item.lastPathComponent)
|
.foregroundStyle(Color(uiColor: .label))
|
||||||
if selectedItem == "\(game!.titleId)/\(item.lastPathComponent)" {
|
Spacer()
|
||||||
Spacer()
|
if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" {
|
||||||
Image(systemName: "checkmark")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
}
|
.foregroundStyle(Color.accentColor)
|
||||||
}
|
.font(.system(size: 24))
|
||||||
}
|
} else {
|
||||||
.contextMenu {
|
Image(systemName: "circle")
|
||||||
Button {
|
.foregroundStyle(Color(uiColor: .secondaryLabel))
|
||||||
removeUpdate(item)
|
.font(.system(size: 24))
|
||||||
} label: {
|
|
||||||
Text("Remove Update")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
removeUpdate(item)
|
||||||
|
} label: {
|
||||||
|
Text("Remove Update")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
|
print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
|
||||||
|
|
||||||
loadJSON(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")
|
.navigationTitle("\(game!.titleName) Updates")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
Button("+") {
|
Button("Add", systemImage: "plus") {
|
||||||
isSelectingGameUpdate = true
|
isSelectingGameUpdate = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,7 +86,8 @@ struct UpdateManagerSheet: View {
|
|||||||
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
|
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
|
||||||
try? fileManager.copyItem(at: url, to: destinationURL)
|
try? fileManager.copyItem(at: url, to: destinationURL)
|
||||||
|
|
||||||
Ryujinx.shared.setTitleUpdate(titleId: gameInfo.titleId, updatePath: "\(gameInfo.titleId)/" + url.lastPathComponent)
|
items.append("updates/" + gameInfo.titleId + "/" + url.lastPathComponent)
|
||||||
|
selectItem(url.lastPathComponent)
|
||||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
loadJSON(jsonURL!)
|
loadJSON(jsonURL!)
|
||||||
} catch {
|
} catch {
|
||||||
@ -93,7 +100,7 @@ struct UpdateManagerSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func removeUpdate(_ game: URL) {
|
func removeUpdate(_ game: URL) {
|
||||||
let gameString = "\(self.game!.titleId)/\(game.lastPathComponent)"
|
let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)"
|
||||||
paths.removeAll { $0 == game }
|
paths.removeAll { $0 == game }
|
||||||
items.removeAll { $0 == gameString }
|
items.removeAll { $0 == gameString }
|
||||||
|
|
||||||
@ -108,12 +115,13 @@ struct UpdateManagerSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveJSON(selectedItem: selectedItem ?? "")
|
saveJSON(selectedItem: selectedItem ?? "")
|
||||||
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveJSON(selectedItem: String) {
|
func saveJSON(selectedItem: String?) {
|
||||||
guard let jsonURL = jsonURL else { return }
|
guard let jsonURL = jsonURL else { return }
|
||||||
do {
|
do {
|
||||||
let jsonDict = ["paths": items, "selected": selectedItem] as [String: Any]
|
let jsonDict = ["paths": items, "selected": selectedItem ?? self.selectedItem ?? ""] as [String: Any]
|
||||||
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
||||||
try newData.write(to: jsonURL)
|
try newData.write(to: jsonURL)
|
||||||
} catch {
|
} catch {
|
||||||
@ -122,26 +130,28 @@ struct UpdateManagerSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadJSON(_ json: URL) {
|
func loadJSON(_ json: URL) {
|
||||||
|
|
||||||
self.jsonURL = json
|
self.jsonURL = json
|
||||||
print("Failed to read JSO")
|
|
||||||
|
|
||||||
guard let jsonURL = jsonURL else { return }
|
guard let jsonURL else { return }
|
||||||
print("Failed to read JSOK")
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: jsonURL)
|
let data = try Data(contentsOf: jsonURL)
|
||||||
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||||
let list = jsonDict["paths"] as? [String] {
|
let list = jsonDict["paths"] as? [String]
|
||||||
var urls: [URL] = []
|
{
|
||||||
|
|
||||||
for path in list {
|
let filteredList = list.filter { relativePath in
|
||||||
urls.append(URL.documentsDirectory.appendingPathComponent("updates").appendingPathComponent(path))
|
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
|
||||||
|
return FileManager.default.fileExists(atPath: path.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.items = list
|
let urls: [URL] = filteredList.map { relativePath in
|
||||||
self.paths = urls
|
URL.documentsDirectory.appendingPathComponent(relativePath)
|
||||||
self.selectedItem = jsonDict["selected"] as? String
|
}
|
||||||
|
|
||||||
|
items = filteredList
|
||||||
|
paths = urls
|
||||||
|
selectedItem = jsonDict["selected"] as? String
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to read JSON: \(error)")
|
print("Failed to read JSON: \(error)")
|
||||||
@ -155,17 +165,17 @@ struct UpdateManagerSheet: View {
|
|||||||
do {
|
do {
|
||||||
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
|
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
|
||||||
try newData.write(to: jsonURL)
|
try newData.write(to: jsonURL)
|
||||||
self.items = []
|
items = []
|
||||||
self.selectedItem = ""
|
selectedItem = ""
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to create default JSON: \(error)")
|
print("Failed to create default JSON: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectItem(_ item: String) {
|
func selectItem(_ item: String) {
|
||||||
let newSelection = "\(game!.titleId)/\(item)"
|
let newSelection = "updates/\(game!.titleId)/\(item)"
|
||||||
|
|
||||||
guard let jsonURL = jsonURL else { return }
|
guard let jsonURL else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: jsonURL)
|
let data = try Data(contentsOf: jsonURL)
|
||||||
@ -175,17 +185,17 @@ struct UpdateManagerSheet: View {
|
|||||||
jsonDict["selected"] = ""
|
jsonDict["selected"] = ""
|
||||||
selectedItem = ""
|
selectedItem = ""
|
||||||
} else {
|
} else {
|
||||||
jsonDict["selected"] = newSelection
|
jsonDict["selected"] = "\(newSelection)"
|
||||||
selectedItem = newSelection
|
selectedItem = newSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonDict["paths"] = items
|
jsonDict["paths"] = items
|
||||||
|
|
||||||
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
||||||
try newData.write(to: jsonURL)
|
try newData.write(to: jsonURL)
|
||||||
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to update JSON: \(error)")
|
print("Failed to update JSON: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
@ -26,10 +26,16 @@ namespace Ryujinx.Graphics.Nvdec.FFmpeg.Native
|
|||||||
{
|
{
|
||||||
return $"lib{libraryName}.so.{version}";
|
return $"lib{libraryName}.so.{version}";
|
||||||
}
|
}
|
||||||
else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) // TODO: ffmpeg on ios
|
else if (OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
return $"lib{libraryName}.{version}.dylib";
|
return $"lib{libraryName}.{version}.dylib";
|
||||||
}
|
}
|
||||||
|
else if (OperatingSystem.IsIOS())
|
||||||
|
{
|
||||||
|
string libName = $"lib{libraryName}.{version}.dylib";
|
||||||
|
Console.WriteLine($"[iOS] Required firmware library: {libName}");
|
||||||
|
return libName;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new NotImplementedException($"Unsupported OS for FFmpeg: {RuntimeInformation.RuntimeIdentifier}");
|
throw new NotImplementedException($"Unsupported OS for FFmpeg: {RuntimeInformation.RuntimeIdentifier}");
|
||||||
|
@ -9,10 +9,10 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
|
|||||||
[SupportedOSPlatform("ios")]
|
[SupportedOSPlatform("ios")]
|
||||||
public static partial class MVKInitialization
|
public static partial class MVKInitialization
|
||||||
{
|
{
|
||||||
[LibraryImport("MoltenVK.framework/MoltenVK")]
|
[LibraryImport("libMoltenVK.dylib")]
|
||||||
private static partial Result vkGetMoltenVKConfigurationMVK(IntPtr unusedInstance, out MVKConfiguration config, in IntPtr configSize);
|
private static partial Result vkGetMoltenVKConfigurationMVK(IntPtr unusedInstance, out MVKConfiguration config, in IntPtr configSize);
|
||||||
|
|
||||||
[LibraryImport("MoltenVK.framework/MoltenVK")]
|
[LibraryImport("libMoltenVK.dylib")]
|
||||||
private static partial Result vkSetMoltenVKConfigurationMVK(IntPtr unusedInstance, in MVKConfiguration config, in IntPtr configSize);
|
private static partial Result vkSetMoltenVKConfigurationMVK(IntPtr unusedInstance, in MVKConfiguration config, in IntPtr configSize);
|
||||||
|
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
|
@ -601,7 +601,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
|
|
||||||
if (supportsExtDynamicState)
|
if (supportsExtDynamicState)
|
||||||
{
|
{
|
||||||
dynamicStates[8] = DynamicState.VertexInputBindingStrideExt;
|
// dynamicStates[8] = DynamicState.VertexInputBindingStrideExt;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pipelineDynamicStateCreateInfo = new PipelineDynamicStateCreateInfo
|
var pipelineDynamicStateCreateInfo = new PipelineDynamicStateCreateInfo
|
||||||
|
@ -142,34 +142,95 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "set_title_update")]
|
[UnmanagedCallersOnly(EntryPoint = "get_dlc_nca_list")]
|
||||||
public static unsafe void SetTitleUpdate(IntPtr titleIdPtr, IntPtr updatePathPtr) {
|
public static unsafe DlcNcaList GetDlcNcaList(IntPtr titleIdPtr, IntPtr pathPtr)
|
||||||
|
{
|
||||||
var titleId = Marshal.PtrToStringAnsi(titleIdPtr);
|
var titleId = Marshal.PtrToStringAnsi(titleIdPtr);
|
||||||
var updatePath = Marshal.PtrToStringAnsi(updatePathPtr);
|
var containerPath = Marshal.PtrToStringAnsi(pathPtr);
|
||||||
string _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
|
|
||||||
|
|
||||||
TitleUpdateMetadata _titleUpdateWindowData;
|
if (!File.Exists(containerPath))
|
||||||
|
{
|
||||||
if (File.Exists(_updateJsonPath)) {
|
return new DlcNcaList { success = false };
|
||||||
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath, _titleSerializerContext.TitleUpdateMetadata);
|
|
||||||
|
|
||||||
_titleUpdateWindowData.Paths ??= new List<string>();
|
|
||||||
if (!_titleUpdateWindowData.Paths.Contains(updatePath)) {
|
|
||||||
_titleUpdateWindowData.Paths.Add(updatePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
_titleUpdateWindowData.Selected = updatePath;
|
|
||||||
} else {
|
|
||||||
_titleUpdateWindowData = new TitleUpdateMetadata {
|
|
||||||
Selected = updatePath,
|
|
||||||
Paths = new List<string> { updatePath },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, _titleSerializerContext.TitleUpdateMetadata);
|
using FileStream containerFile = File.OpenRead(containerPath);
|
||||||
|
|
||||||
|
PartitionFileSystem pfs = new();
|
||||||
|
pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||||
|
bool containsDlc = false;
|
||||||
|
|
||||||
|
_virtualFileSystem.ImportTickets(pfs);
|
||||||
|
|
||||||
|
// TreeIter? parentIter = null;
|
||||||
|
|
||||||
|
List<DlcNcaListItem> listItems = new();
|
||||||
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
|
{
|
||||||
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath);
|
||||||
|
|
||||||
|
if (nca == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||||
|
{
|
||||||
|
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != titleId)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"ContainerPath: {containerPath}");
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"TitleId: {nca.Header.TitleId}");
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"fileEntry.FullPath: {fileEntry.FullPath}");
|
||||||
|
|
||||||
|
// parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
|
||||||
|
// ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
|
||||||
|
|
||||||
|
DlcNcaListItem item = new();
|
||||||
|
CopyStringToFixedArray(fileEntry.FullPath, item.Path, 256);
|
||||||
|
item.TitleId = nca.Header.TitleId;
|
||||||
|
listItems.Add(item);
|
||||||
|
|
||||||
|
containsDlc = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!containsDlc)
|
||||||
|
{
|
||||||
|
return new DlcNcaList { success = false };
|
||||||
|
// GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = new DlcNcaList { success = true, size = (uint) listItems.Count };
|
||||||
|
|
||||||
|
DlcNcaListItem[] items = listItems.ToArray();
|
||||||
|
|
||||||
|
fixed (DlcNcaListItem* p = &items[0])
|
||||||
|
{
|
||||||
|
list.items = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Nca TryCreateNca(IStorage ncaStorage, string containerPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
||||||
public static unsafe int GetFPS()
|
public static unsafe int GetFPS()
|
||||||
@ -258,7 +319,7 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
var result = Parser.Default.ParseArguments<Options>(args)
|
var result = Parser.Default.ParseArguments<Options>(args)
|
||||||
.WithParsed(options =>
|
.WithParsed(options =>
|
||||||
{
|
{
|
||||||
Load(options); // Load is called with the parsed options
|
Load(options);
|
||||||
})
|
})
|
||||||
.WithNotParsed(errors => errors.Output());
|
.WithNotParsed(errors => errors.Output());
|
||||||
|
|
||||||
@ -751,7 +812,8 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
|
|
||||||
if (File.Exists(titleUpdateMetadataPath))
|
if (File.Exists(titleUpdateMetadataPath))
|
||||||
{
|
{
|
||||||
updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
string updatePathRelative = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
||||||
|
updatePath = Path.Combine(AppDataManager.BaseDirPath, updatePathRelative);
|
||||||
|
|
||||||
if (File.Exists(updatePath))
|
if (File.Exists(updatePath))
|
||||||
{
|
{
|
||||||
@ -1517,6 +1579,19 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
public byte[]? Icon;
|
public byte[]? Icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public unsafe struct DlcNcaListItem
|
||||||
|
{
|
||||||
|
public fixed byte Path[256];
|
||||||
|
public ulong TitleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe struct DlcNcaList
|
||||||
|
{
|
||||||
|
public bool success;
|
||||||
|
public uint size;
|
||||||
|
public unsafe DlcNcaListItem* items;
|
||||||
|
}
|
||||||
|
|
||||||
public unsafe struct GameInfoNative
|
public unsafe struct GameInfoNative
|
||||||
{
|
{
|
||||||
public ulong FileSize;
|
public ulong FileSize;
|
||||||
@ -1564,14 +1639,13 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
ImageData = null;
|
ImageData = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CopyStringToFixedArray(string source, byte* destination, int length)
|
|
||||||
{
|
|
||||||
var span = new Span<byte>(destination, length);
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user