forked from MeloNX/MeloNX
Compare commits
4 Commits
cd24276a7d
...
2d1a357d1b
Author | SHA1 | Date | |
---|---|---|---|
2d1a357d1b | |||
abbb3bb0c3 | |||
8e60f6dc50 | |||
|
3b99631dfb |
@ -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)
|
||||||
|
Binary file not shown.
@ -12,12 +12,12 @@
|
|||||||
<key>Ryujinx.xcscheme_^#shared#^_</key>
|
<key>Ryujinx.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>4</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
|
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>3</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,23 +484,21 @@ 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 {
|
self.metalLayer = layer
|
||||||
self.metalLayer = layer
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
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: String { titleId }
|
||||||
|
|
||||||
var containerFolder: URL
|
var containerFolder: URL
|
||||||
var fileType: UTType
|
var fileType: UTType
|
||||||
@ -21,7 +21,6 @@ public struct Game: Identifiable, Equatable, Hashable {
|
|||||||
var version: String
|
var version: String
|
||||||
var icon: UIImage?
|
var icon: UIImage?
|
||||||
|
|
||||||
|
|
||||||
static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game {
|
static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game {
|
||||||
var gameInfo = gameInfo
|
var gameInfo = gameInfo
|
||||||
var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
|
var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
|
||||||
|
@ -39,14 +39,23 @@ 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.titleId == game.titleId })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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.titleId == recentGame.titleId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
iOSNav {
|
||||||
List {
|
List {
|
||||||
@ -66,46 +75,32 @@ struct GameLibraryView: View {
|
|||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.top, 40)
|
.padding(.top, 40)
|
||||||
} else {
|
} else {
|
||||||
if !isSearching && !recentGames.isEmpty {
|
if !isSearching && !realRecentGames.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
Section {
|
||||||
Text("Recent")
|
ForEach(realRecentGames) { game in
|
||||||
.font(.title2.bold())
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
||||||
.padding(.horizontal)
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
|
Button(role: .destructive) {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
removeFromRecentGames(game)
|
||||||
LazyHStack(spacing: 16) {
|
} label: {
|
||||||
ForEach(recentGames) { game in
|
Label("Delete", systemImage: "trash")
|
||||||
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, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
|
||||||
.onTapGesture {
|
|
||||||
addToRecentGames(game)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ForEach(filteredGames) { game in
|
ForEach(filteredGames) { game in
|
||||||
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
|
||||||
.onTapGesture {
|
|
||||||
addToRecentGames(game)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,8 +201,13 @@ struct GameLibraryView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: startemu) { game in
|
||||||
|
guard let game else { return }
|
||||||
|
addToRecentGames(game)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.searchable(text: $searchText)
|
.searchable(text: $searchText)
|
||||||
|
.animation(.easeInOut, value: searchText)
|
||||||
.onChange(of: searchText) { _ in
|
.onChange(of: searchText) { _ in
|
||||||
isSearching = !searchText.isEmpty
|
isSearching = !searchText.isEmpty
|
||||||
}
|
}
|
||||||
@ -291,8 +291,8 @@ 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 {
|
||||||
@ -301,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()
|
||||||
@ -364,53 +369,6 @@ extension Game: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Recent Game Card
|
|
||||||
struct RecentGameCard: View {
|
|
||||||
let game: Game
|
|
||||||
@Binding var startemu: Game?
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: {
|
|
||||||
startemu = game
|
|
||||||
}) {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
if let icon = game.icon {
|
|
||||||
Image(uiImage: icon)
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
.frame(width: 140, height: 140)
|
|
||||||
.cornerRadius(12)
|
|
||||||
} else {
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(colorScheme == .dark ?
|
|
||||||
Color(.systemGray5) : Color(.systemGray6))
|
|
||||||
.frame(width: 140, height: 140)
|
|
||||||
|
|
||||||
Image(systemName: "gamecontroller.fill")
|
|
||||||
.font(.system(size: 40))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(game.titleName)
|
|
||||||
.font(.subheadline.bold())
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
Text(game.developer)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Game List Item
|
// MARK: - Game List Item
|
||||||
struct GameListRow: View {
|
struct GameListRow: View {
|
||||||
let game: Game
|
let game: Game
|
||||||
@ -467,49 +425,48 @@ struct GameListRow: View {
|
|||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
.opacity(0.8)
|
.opacity(0.8)
|
||||||
}
|
}
|
||||||
.contextMenu {
|
}
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Button {
|
||||||
Button {
|
gameInfo = game
|
||||||
gameInfo = game
|
isViewingGameInfo.toggle()
|
||||||
isSelectingGameUpdate.toggle()
|
} label: {
|
||||||
} label: {
|
Label("Game Info", systemImage: "info.circle")
|
||||||
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) {
|
Section {
|
||||||
gametoDelete = game
|
Button {
|
||||||
showGameDeleteConfirmation.toggle()
|
gameInfo = game
|
||||||
} label: {
|
isSelectingGameUpdate.toggle()
|
||||||
Label("Delete", systemImage: "trash")
|
} 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 {
|
||||||
|
@ -272,42 +272,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 +333,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 {
|
||||||
|
@ -45,7 +45,7 @@ struct DLCManagerSheet: View {
|
|||||||
Self.saveDlcs(game, dlc: dlcs)
|
Self.saveDlcs(game, dlc: dlcs)
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(dlc.containerPath)
|
Text((dlc.containerPath as NSString).lastPathComponent)
|
||||||
.foregroundStyle(Color(uiColor: .label))
|
.foregroundStyle(Color(uiColor: .label))
|
||||||
Spacer()
|
Spacer()
|
||||||
if dlc.downloadableContentNcaList.first?.enabled == true {
|
if dlc.downloadableContentNcaList.first?.enabled == true {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user