Latest Changes and almost works
@ -19,8 +19,7 @@ namespace ARMeilleure.CodeGen.Arm64
|
|||||||
LinuxFeatureInfoHwCap = (LinuxFeatureFlagsHwCap)getauxval(AT_HWCAP);
|
LinuxFeatureInfoHwCap = (LinuxFeatureFlagsHwCap)getauxval(AT_HWCAP);
|
||||||
LinuxFeatureInfoHwCap2 = (LinuxFeatureFlagsHwCap2)getauxval(AT_HWCAP2);
|
LinuxFeatureInfoHwCap2 = (LinuxFeatureFlagsHwCap2)getauxval(AT_HWCAP2);
|
||||||
}
|
}
|
||||||
|
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
|
||||||
if (OperatingSystem.IsMacOS())
|
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _sysctlNames.Length; i++)
|
for (int i = 0; i < _sysctlNames.Length; i++)
|
||||||
{
|
{
|
||||||
@ -130,6 +129,7 @@ namespace ARMeilleure.CodeGen.Arm64
|
|||||||
private static unsafe partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, out int oldValue, ref ulong oldSize, nint newValue, ulong newValueSize);
|
private static unsafe partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, out int oldValue, ref ulong oldSize, nint newValue, ulong newValueSize);
|
||||||
|
|
||||||
[SupportedOSPlatform("macos")]
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
private static bool CheckSysctlName(string name)
|
private static bool CheckSysctlName(string name)
|
||||||
{
|
{
|
||||||
ulong size = sizeof(int);
|
ulong size = sizeof(int);
|
||||||
|
@ -87,13 +87,13 @@ namespace ARMeilleure.Signal
|
|||||||
|
|
||||||
private static Operand GenerateUnixFaultAddress(EmitterContext context, Operand sigInfoPtr)
|
private static Operand GenerateUnixFaultAddress(EmitterContext context, Operand sigInfoPtr)
|
||||||
{
|
{
|
||||||
ulong structAddressOffset = OperatingSystem.IsMacOS() ? 24ul : 16ul; // si_addr
|
ulong structAddressOffset = (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS()) ? 24ul : 16ul; // si_addr
|
||||||
return context.Load(OperandType.I64, context.Add(sigInfoPtr, Const(structAddressOffset)));
|
return context.Load(OperandType.I64, context.Add(sigInfoPtr, Const(structAddressOffset)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Operand GenerateUnixWriteFlag(EmitterContext context, Operand ucontextPtr)
|
private static Operand GenerateUnixWriteFlag(EmitterContext context, Operand ucontextPtr)
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
const ulong McontextOffset = 48; // uc_mcontext
|
const ulong McontextOffset = 48; // uc_mcontext
|
||||||
Operand ctxPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(McontextOffset)));
|
Operand ctxPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(McontextOffset)));
|
||||||
|
@ -30,21 +30,26 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
_blocks.Add(new MemoryBlock(0, capacity));
|
_blocks.Add(new MemoryBlock(0, capacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Allocate(int size)
|
public int Allocate(ref int size, int alignment)
|
||||||
{
|
{
|
||||||
|
int alignM1 = alignment - 1;
|
||||||
for (int i = 0; i < _blocks.Count; i++)
|
for (int i = 0; i < _blocks.Count; i++)
|
||||||
{
|
{
|
||||||
MemoryBlock block = _blocks[i];
|
MemoryBlock block = _blocks[i];
|
||||||
|
int misAlignment = ((block.Offset + alignM1) & (~alignM1)) - block.Offset;
|
||||||
|
int alignedSize = size + misAlignment;
|
||||||
|
|
||||||
if (block.Size > size)
|
if (block.Size > alignedSize)
|
||||||
{
|
{
|
||||||
_blocks[i] = new MemoryBlock(block.Offset + size, block.Size - size);
|
size = alignedSize;
|
||||||
return block.Offset;
|
_blocks[i] = new MemoryBlock(block.Offset + alignedSize, block.Size - alignedSize);
|
||||||
|
return block.Offset + misAlignment;
|
||||||
}
|
}
|
||||||
else if (block.Size == size)
|
else if (block.Size == alignedSize)
|
||||||
{
|
{
|
||||||
|
size = alignedSize;
|
||||||
_blocks.RemoveAt(i);
|
_blocks.RemoveAt(i);
|
||||||
return block.Offset;
|
return block.Offset + misAlignment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_jitRegion = new ReservedRegion(allocator, CacheSize);
|
_jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize));
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
|
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
@ -80,6 +80,7 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
|
|
||||||
if (OperatingSystem.IsIOS())
|
if (OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
|
ReprotectAsWritable(funcOffset, code.Length);
|
||||||
Marshal.Copy(code, 0, funcPtr, code.Length);
|
Marshal.Copy(code, 0, funcPtr, code.Length);
|
||||||
ReprotectAsExecutable(funcOffset, code.Length);
|
ReprotectAsExecutable(funcOffset, code.Length);
|
||||||
|
|
||||||
@ -119,6 +120,13 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
|
|
||||||
public static void Unmap(nint pointer)
|
public static void Unmap(nint pointer)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
if (OperatingSystem.IsIOS())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
Debug.Assert(_initialized);
|
Debug.Assert(_initialized);
|
||||||
@ -157,7 +165,18 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
{
|
{
|
||||||
codeSize = AlignCodeSize(codeSize);
|
codeSize = AlignCodeSize(codeSize);
|
||||||
|
|
||||||
int allocOffset = _cacheAllocator.Allocate(codeSize);
|
int alignment = CodeAlignment;
|
||||||
|
|
||||||
|
if (OperatingSystem.IsIOS())
|
||||||
|
{
|
||||||
|
alignment = 0x4000;
|
||||||
|
}
|
||||||
|
|
||||||
|
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
|
||||||
|
|
||||||
|
//DEBUG: Show JIT Memory Allocation
|
||||||
|
|
||||||
|
//Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}");
|
||||||
|
|
||||||
if (allocOffset < 0)
|
if (allocOffset < 0)
|
||||||
{
|
{
|
||||||
@ -171,6 +190,13 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
|
|
||||||
private static int AlignCodeSize(int codeSize)
|
private static int AlignCodeSize(int codeSize)
|
||||||
{
|
{
|
||||||
|
int alignment = CodeAlignment;
|
||||||
|
|
||||||
|
if (OperatingSystem.IsIOS())
|
||||||
|
{
|
||||||
|
alignment = 0x4000;
|
||||||
|
}
|
||||||
|
|
||||||
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
|
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1022,6 +1022,7 @@ namespace ARMeilleure.Translation.PTC
|
|||||||
osPlatform |= (OperatingSystem.IsLinux() ? 1u : 0u) << 1;
|
osPlatform |= (OperatingSystem.IsLinux() ? 1u : 0u) << 1;
|
||||||
osPlatform |= (OperatingSystem.IsMacOS() ? 1u : 0u) << 2;
|
osPlatform |= (OperatingSystem.IsMacOS() ? 1u : 0u) << 2;
|
||||||
osPlatform |= (OperatingSystem.IsWindows() ? 1u : 0u) << 3;
|
osPlatform |= (OperatingSystem.IsWindows() ? 1u : 0u) << 3;
|
||||||
|
osPlatform |= (OperatingSystem.IsIOS() ? 1u : 0u) << 4;
|
||||||
#pragma warning restore IDE0055
|
#pragma warning restore IDE0055
|
||||||
|
|
||||||
return osPlatform;
|
return osPlatform;
|
||||||
|
@ -62,7 +62,6 @@
|
|||||||
4E6715F12CFEEB6E00425F0C /* Exceptions for "MeloNX" folder in "Embed Libraries" phase from "MeloNX" target */ = {
|
4E6715F12CFEEB6E00425F0C /* Exceptions for "MeloNX" folder in "Embed Libraries" phase from "MeloNX" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;
|
isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;
|
||||||
attributesByRelativePath = {
|
attributesByRelativePath = {
|
||||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (CodeSignOnCopy, );
|
|
||||||
"Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (CodeSignOnCopy, );
|
"Dependencies/Dynamic Libraries/libMoltenVK.dylib" = (CodeSignOnCopy, );
|
||||||
"Dependencies/Dynamic Libraries/libavcodec.dylib" = (CodeSignOnCopy, );
|
"Dependencies/Dynamic Libraries/libavcodec.dylib" = (CodeSignOnCopy, );
|
||||||
"Dependencies/Dynamic Libraries/libavutil.dylib" = (CodeSignOnCopy, );
|
"Dependencies/Dynamic Libraries/libavutil.dylib" = (CodeSignOnCopy, );
|
||||||
@ -82,7 +81,6 @@
|
|||||||
"Dependencies/Dynamic Libraries/libavcodec.dylib",
|
"Dependencies/Dynamic Libraries/libavcodec.dylib",
|
||||||
"Dependencies/Dynamic Libraries/libavutil.dylib",
|
"Dependencies/Dynamic Libraries/libavutil.dylib",
|
||||||
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
|
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
|
||||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
|
|
||||||
Dependencies/XCFrameworks/libavcodec.xcframework,
|
Dependencies/XCFrameworks/libavcodec.xcframework,
|
||||||
Dependencies/XCFrameworks/libavfilter.xcframework,
|
Dependencies/XCFrameworks/libavfilter.xcframework,
|
||||||
Dependencies/XCFrameworks/libavformat.xcframework,
|
Dependencies/XCFrameworks/libavformat.xcframework,
|
||||||
@ -579,6 +577,16 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(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.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
@ -694,6 +702,16 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(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.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
|
27
src/MeloNX/MeloNX/Core/DetectJIT/utils.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#if __has_feature(modules)
|
||||||
|
@import UIKit;
|
||||||
|
@import Foundation;
|
||||||
|
#else
|
||||||
|
#import "UIKit/UIKit.h"
|
||||||
|
#import "Foundation/Foundation.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define DISPATCH_ASYNC_START dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
#define DISPATCH_ASYNC_CLOSE });
|
||||||
|
|
||||||
|
#define PT_TRACE_ME 0
|
||||||
|
extern int ptrace(int, pid_t, caddr_t, int);
|
||||||
|
|
||||||
|
#define CS_DEBUGGED 0x10000000
|
||||||
|
extern int csops(
|
||||||
|
pid_t pid,
|
||||||
|
unsigned int ops,
|
||||||
|
void *useraddr,
|
||||||
|
size_t usersize
|
||||||
|
);
|
||||||
|
|
||||||
|
extern BOOL getEntitlementValue(NSString *key);
|
||||||
|
extern BOOL isJITEnabled(void);
|
||||||
|
|
||||||
|
#define DLOG(format, ...) ShowAlert(@"DEBUG", [NSString stringWithFormat:@"\n %s [Line %d] \n %@", __PRETTY_FUNCTION__, __LINE__, [NSString stringWithFormat:format, ##__VA_ARGS__]])
|
||||||
|
void ShowAlert(NSString* title, NSString* message, _Bool* showok);
|
91
src/MeloNX/MeloNX/Core/DetectJIT/utils.m
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#import "utils.h"
|
||||||
|
|
||||||
|
typedef struct __SecTask * SecTaskRef;
|
||||||
|
extern CFTypeRef SecTaskCopyValueForEntitlement(
|
||||||
|
SecTaskRef task,
|
||||||
|
NSString* entitlement,
|
||||||
|
CFErrorRef _Nullable *error
|
||||||
|
)
|
||||||
|
__attribute__((weak_import));
|
||||||
|
|
||||||
|
extern SecTaskRef SecTaskCreateFromSelf(CFAllocatorRef allocator)
|
||||||
|
__attribute__((weak_import));
|
||||||
|
|
||||||
|
BOOL getEntitlementValue(NSString *key)
|
||||||
|
{
|
||||||
|
if (SecTaskCreateFromSelf == NULL || SecTaskCopyValueForEntitlement == NULL)
|
||||||
|
return NO;
|
||||||
|
SecTaskRef sec_task = SecTaskCreateFromSelf(NULL);
|
||||||
|
if(!sec_task) return NO;
|
||||||
|
CFTypeRef value = SecTaskCopyValueForEntitlement(sec_task, key, nil);
|
||||||
|
if (value != nil)
|
||||||
|
{
|
||||||
|
CFRelease(value);
|
||||||
|
}
|
||||||
|
CFRelease(sec_task);
|
||||||
|
return value != nil && [(__bridge id)value boolValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL isJITEnabled(void)
|
||||||
|
{
|
||||||
|
if (getEntitlementValue(@"dynamic-codesigning"))
|
||||||
|
{
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flags;
|
||||||
|
csops(getpid(), 0, &flags, sizeof(flags));
|
||||||
|
return (flags & CS_DEBUGGED) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowAlert(NSString* title, NSString* message, _Bool* showok)
|
||||||
|
{
|
||||||
|
DISPATCH_ASYNC_START
|
||||||
|
UIWindow* mainWindow = [[UIApplication sharedApplication] windows].lastObject;
|
||||||
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
|
||||||
|
message:message
|
||||||
|
preferredStyle:UIAlertControllerStyleAlert];
|
||||||
|
if (showok) {
|
||||||
|
[alert addAction:[UIAlertAction actionWithTitle:@"ok!"
|
||||||
|
style:UIAlertActionStyleDefault
|
||||||
|
handler:nil]];
|
||||||
|
}
|
||||||
|
[mainWindow.rootViewController presentViewController:alert
|
||||||
|
animated:true
|
||||||
|
completion:nil];
|
||||||
|
DISPATCH_ASYNC_CLOSE
|
||||||
|
}
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
__attribute__((constructor)) static void entry(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (isJITEnabled()) {
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults setBool:YES forKey:@"JIT"];
|
||||||
|
[defaults synchronize]; // Ensure the value is saved immediately
|
||||||
|
} else {
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults setBool:NO forKey:@"JIT"];
|
||||||
|
[defaults synchronize]; // Ensure the value is saved immediately
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getEntitlementValue(@"com.apple.developer.kernel.increased-memory-limit")) {
|
||||||
|
NSLog(@"Entitlement Does Exist");
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults setBool:YES forKey:@"increased-memory-limit"];
|
||||||
|
[defaults synchronize]; // Ensure the value is saved immediately
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getEntitlementValue(@"com.apple.developer.kernel.increased-debugging-memory-limit")) {
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults setBool:YES forKey:@"increased-debugging-memory-limit"];
|
||||||
|
[defaults synchronize]; // Ensure the value is saved immediately
|
||||||
|
}
|
||||||
|
if (getEntitlementValue(@"com.apple.developer.kernel.extended-virtual-addressing")) {
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults setBool:YES forKey:@"extended-virtual-addressing"];
|
||||||
|
[defaults synchronize]; // Ensure the value is saved immediately
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,13 +8,29 @@
|
|||||||
#ifndef RyujinxHeader
|
#ifndef RyujinxHeader
|
||||||
#define RyujinxHeader
|
#define RyujinxHeader
|
||||||
|
|
||||||
|
|
||||||
|
#import "SDL2/SDL.h"
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
struct GameInfo {
|
||||||
|
long FileSize;
|
||||||
|
char TitleName[512];
|
||||||
|
long TitleId;
|
||||||
|
char Developer[256];
|
||||||
|
int Version;
|
||||||
|
unsigned char* ImageData;
|
||||||
|
unsigned int ImageSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
// extern struct GameInfo get_game_info(int, char*);
|
||||||
// Declare the main_ryujinx_sdl function, matching the signature
|
// Declare the main_ryujinx_sdl function, matching the signature
|
||||||
int main_ryujinx_sdl(int argc, char **argv);
|
int main_ryujinx_sdl(int argc, char **argv);
|
||||||
|
|
||||||
|
// void initialize();
|
||||||
|
|
||||||
const char* get_game_controllers();
|
const char* get_game_controllers();
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
class MTLHud {
|
class MTLHud {
|
||||||
|
|
||||||
var canMetalHud: Bool {
|
var canMetalHud: Bool {
|
||||||
|
@ -2,98 +2,188 @@
|
|||||||
// VirtualController.swift
|
// VirtualController.swift
|
||||||
// MeloNX
|
// MeloNX
|
||||||
//
|
//
|
||||||
// Created by Stossy11 on 28/11/2024.
|
// Created by Stossy11 on 8/12/2024.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GameController
|
import CoreHaptics
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public var controllerCallback: (() -> Void)?
|
class VirtualController {
|
||||||
|
private var instanceID: SDL_JoystickID = -1
|
||||||
var VirtualController: GCVirtualController!
|
private var controller: OpaquePointer?
|
||||||
func showVirtualController() {
|
|
||||||
let config = GCVirtualController.Configuration()
|
public let controllername = "MeloNX Touch Controller"
|
||||||
if UserDefaults.standard.bool(forKey: "RyuDemoControls") {
|
|
||||||
config.elements = [
|
init() {
|
||||||
GCInputLeftThumbstick,
|
setupVirtualController()
|
||||||
GCInputButtonA,
|
|
||||||
GCInputButtonB,
|
|
||||||
GCInputButtonX,
|
|
||||||
GCInputButtonY,
|
|
||||||
// GCInputRightThumbstick,
|
|
||||||
GCInputRightTrigger,
|
|
||||||
GCInputLeftTrigger,
|
|
||||||
GCInputLeftShoulder,
|
|
||||||
GCInputRightShoulder
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
config.elements = [
|
|
||||||
GCInputLeftThumbstick,
|
|
||||||
GCInputButtonA,
|
|
||||||
GCInputButtonB,
|
|
||||||
GCInputButtonX,
|
|
||||||
GCInputButtonY,
|
|
||||||
GCInputRightThumbstick,
|
|
||||||
GCInputRightTrigger,
|
|
||||||
GCInputLeftTrigger,
|
|
||||||
GCInputLeftShoulder,
|
|
||||||
GCInputRightShoulder
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
VirtualController = GCVirtualController(configuration: config)
|
|
||||||
VirtualController.connect { err in
|
private func setupVirtualController() {
|
||||||
print("controller connect: \(String(describing: err))")
|
// Initialize SDL if not already initialized
|
||||||
patchMakeKeyAndVisible()
|
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
|
||||||
if let controllerCallback {
|
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
|
||||||
controllerCallback()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitforcontroller() {
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
|
|
||||||
|
|
||||||
if let window = UIApplication.shared.windows.first {
|
// Create virtual controller
|
||||||
// Function to recursively search for GCControllerView
|
var joystickDesc = SDL_VirtualJoystickDesc(
|
||||||
func findGCControllerView(in view: UIView) -> UIView? {
|
version: UInt16(SDL_VIRTUAL_JOYSTICK_DESC_VERSION),
|
||||||
// Check if current view is GCControllerView
|
type: Uint16(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue),
|
||||||
if String(describing: type(of: view)) == "GCControllerView" {
|
naxes: 6,
|
||||||
return view
|
nbuttons: 15,
|
||||||
|
nhats: 1,
|
||||||
|
vendor_id: 0,
|
||||||
|
product_id: 0,
|
||||||
|
padding: 0,
|
||||||
|
button_mask: 0,
|
||||||
|
axis_mask: 0,
|
||||||
|
name: controllername.withCString { $0 },
|
||||||
|
userdata: nil,
|
||||||
|
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)")
|
||||||
|
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
)
|
||||||
// Search through subviews
|
|
||||||
for subview in view.subviews {
|
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
|
||||||
if let found = findGCControllerView(in: subview) {
|
if instanceID < 0 {
|
||||||
return found
|
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
|
||||||
}
|
return
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if let gcControllerView = findGCControllerView(in: window) {
|
|
||||||
// Found the GCControllerView
|
|
||||||
print("Found GCControllerView:", gcControllerView)
|
|
||||||
|
|
||||||
if let theWindow = theWindow, (findGCControllerView(in: theWindow) == nil) {
|
|
||||||
theWindow.addSubview(gcControllerView)
|
|
||||||
|
|
||||||
theWindow.bringSubviewToFront(gcControllerView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = controller {
|
||||||
|
SDL_GameControllerClose(controller)
|
||||||
|
self.controller = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
cleanup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 15.0, *)
|
enum VirtualControllerButton: Int {
|
||||||
func reconnectVirtualController() {
|
case B
|
||||||
VirtualController.disconnect()
|
case A
|
||||||
DispatchQueue.main.async {
|
case Y
|
||||||
VirtualController.connect { err in
|
case X
|
||||||
print("reconnected: err \(String(describing: err))")
|
case back
|
||||||
}
|
case guide
|
||||||
}
|
case start
|
||||||
|
case leftStick
|
||||||
|
case rightStick
|
||||||
|
case leftShoulder
|
||||||
|
case rightShoulder
|
||||||
|
case dPadUp
|
||||||
|
case dPadDown
|
||||||
|
case dPadLeft
|
||||||
|
case dPadRight
|
||||||
|
case leftTrigger
|
||||||
|
case rightTrigger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ThumbstickType: Int {
|
||||||
|
case left
|
||||||
|
case right
|
||||||
|
}
|
||||||
|
65
src/MeloNX/MeloNX/Core/Swift/Controller/WaitforVC.swift
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
//
|
||||||
|
// VirtualController.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 28/11/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GameController
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
func waitforcontroller() {
|
||||||
|
if let window = theWindow {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Function to recursively search for GCControllerView
|
||||||
|
func findGCControllerView(in view: UIView) -> UIView? {
|
||||||
|
// Check if current view is GCControllerView
|
||||||
|
if String(describing: type(of: view)) == "ControllerView" {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search through subviews
|
||||||
|
for subview in view.subviews {
|
||||||
|
if let found = findGCControllerView(in: subview) {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let controllerView = ControllerView()
|
||||||
|
let controllerHostingController = UIHostingController(rootView: controllerView)
|
||||||
|
let containerView = TransparentHostingContainerView(frame: window.bounds)
|
||||||
|
containerView.backgroundColor = .clear
|
||||||
|
|
||||||
|
controllerHostingController.view.frame = containerView.bounds
|
||||||
|
controllerHostingController.view.backgroundColor = .clear
|
||||||
|
containerView.addSubview(controllerHostingController.view)
|
||||||
|
|
||||||
|
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||||
|
if findGCControllerView(in: window) == nil {
|
||||||
|
window.addSubview(containerView)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
window.bringSubviewToFront(containerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TransparentHostingContainerView: UIView {
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
// Check if the point is within the subviews of this container
|
||||||
|
let view = super.hitTest(point, with: event)
|
||||||
|
|
||||||
|
// Return nil if the touch is outside visible content (passes through to views below)
|
||||||
|
return view === self ? nil : view
|
||||||
|
}
|
||||||
|
}
|
@ -19,13 +19,26 @@ extension UIWindow {
|
|||||||
}
|
}
|
||||||
self.wdb_makeKeyAndVisible()
|
self.wdb_makeKeyAndVisible()
|
||||||
theWindow = self
|
theWindow = self
|
||||||
if #available(iOS 15.0, *) {
|
|
||||||
reconnectVirtualController()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if let window = theWindow {
|
if UserDefaults.standard.bool(forKey: "isVirtualController") {
|
||||||
waitforcontroller()
|
if let window = theWindow {
|
||||||
|
|
||||||
|
class LandscapeViewController: UIViewController {
|
||||||
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
|
return .landscape
|
||||||
|
}
|
||||||
|
|
||||||
|
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
||||||
|
return .landscapeLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let landscapeVC = LandscapeViewController()
|
||||||
|
landscapeVC.modalPresentationStyle = .fullScreen
|
||||||
|
theWindow?.rootViewController?.present(landscapeVC, animated: false, completion: nil)
|
||||||
|
waitforcontroller()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,3 +51,4 @@ func patchMakeKeyAndVisible() {
|
|||||||
method_exchangeImplementations(m1, m2)
|
method_exchangeImplementations(m1, m2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SDL2
|
|
||||||
import GameController
|
import GameController
|
||||||
|
|
||||||
struct Controller: Identifiable, Hashable {
|
struct Controller: Identifiable, Hashable {
|
||||||
@ -32,13 +31,15 @@ struct iOSNav<Content: View>: View {
|
|||||||
class Ryujinx {
|
class Ryujinx {
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
|
|
||||||
|
let virtualController = VirtualController()
|
||||||
|
|
||||||
@Published var controllerMap: [Controller] = []
|
@Published var controllerMap: [Controller] = []
|
||||||
|
|
||||||
static let shared = Ryujinx()
|
static let shared = Ryujinx()
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
public struct Configuration : Codable {
|
public struct Configuration : Codable, Equatable {
|
||||||
var gamepath: String
|
var gamepath: String
|
||||||
var inputids: [String]
|
var inputids: [String]
|
||||||
var resscale: Float
|
var resscale: Float
|
||||||
@ -49,7 +50,6 @@ class Ryujinx {
|
|||||||
var listinputids: Bool
|
var listinputids: Bool
|
||||||
var fullscreen: Bool
|
var fullscreen: Bool
|
||||||
var memoryManagerMode: String
|
var memoryManagerMode: String
|
||||||
var disableVSync: Bool
|
|
||||||
var disableShaderCache: Bool
|
var disableShaderCache: Bool
|
||||||
var disableDockedMode: Bool
|
var disableDockedMode: Bool
|
||||||
var enableTextureRecompression: Bool
|
var enableTextureRecompression: Bool
|
||||||
@ -63,7 +63,6 @@ class Ryujinx {
|
|||||||
listinputids: Bool = false,
|
listinputids: Bool = false,
|
||||||
fullscreen: Bool = true,
|
fullscreen: Bool = true,
|
||||||
memoryManagerMode: String = "HostMapped",
|
memoryManagerMode: String = "HostMapped",
|
||||||
disableVSync: Bool = false,
|
|
||||||
disableShaderCache: Bool = false,
|
disableShaderCache: Bool = false,
|
||||||
disableDockedMode: Bool = false,
|
disableDockedMode: Bool = false,
|
||||||
nintendoinput: Bool = true,
|
nintendoinput: Bool = true,
|
||||||
@ -78,7 +77,6 @@ class Ryujinx {
|
|||||||
self.tracelogs = tracelogs
|
self.tracelogs = tracelogs
|
||||||
self.listinputids = listinputids
|
self.listinputids = listinputids
|
||||||
self.fullscreen = fullscreen
|
self.fullscreen = fullscreen
|
||||||
self.disableVSync = disableVSync
|
|
||||||
self.disableShaderCache = disableShaderCache
|
self.disableShaderCache = disableShaderCache
|
||||||
self.disableDockedMode = disableDockedMode
|
self.disableDockedMode = disableDockedMode
|
||||||
self.enableTextureRecompression = enableTextureRecompression
|
self.enableTextureRecompression = enableTextureRecompression
|
||||||
@ -99,7 +97,7 @@ class Ryujinx {
|
|||||||
isRunning = true
|
isRunning = true
|
||||||
|
|
||||||
// Start The Emulation on the main thread
|
// Start The Emulation on the main thread
|
||||||
DispatchQueue.main.async {
|
RunLoop.current.perform {
|
||||||
do {
|
do {
|
||||||
let args = self.buildCommandLineArgs(from: config)
|
let args = self.buildCommandLineArgs(from: config)
|
||||||
|
|
||||||
@ -145,29 +143,25 @@ class Ryujinx {
|
|||||||
args.append("--graphics-backend")
|
args.append("--graphics-backend")
|
||||||
args.append("Vulkan")
|
args.append("Vulkan")
|
||||||
|
|
||||||
// Fixes the Stubs.DispatchLoop Crash
|
args.append(contentsOf: ["--memory-manager-mode", "SoftwarePageTable"])
|
||||||
args.append(contentsOf: ["--memory-manager-mode", config.memoryManagerMode])
|
|
||||||
if config.fullscreen {
|
args.append(contentsOf: ["--exclusive-fullscreen", String(config.fullscreen)])
|
||||||
args.append(contentsOf: ["--exclusive-fullscreen", String(config.fullscreen)])
|
args.append(contentsOf: ["--exclusive-fullscreen-width", "\(Int(UIScreen.main.bounds.width))"])
|
||||||
args.append(contentsOf: ["--exclusive-fullscreen-width", "1280"])
|
args.append(contentsOf: ["--exclusive-fullscreen-height", "\(Int(UIScreen.main.bounds.height))"])
|
||||||
args.append(contentsOf: ["--exclusive-fullscreen-height", "720"])
|
|
||||||
|
|
||||||
|
if config.nintendoinput {
|
||||||
|
// args.append("--correct-controller")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.resscale != 1 {
|
|
||||||
|
//args.append("--disable-vsync")
|
||||||
|
|
||||||
|
|
||||||
|
if config.resscale != 1.0 {
|
||||||
args.append(contentsOf: ["--resolution-scale", String(config.resscale)])
|
args.append(contentsOf: ["--resolution-scale", String(config.resscale)])
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.nintendoinput {
|
|
||||||
// args.append("--correct-ons-controller")
|
|
||||||
}
|
|
||||||
if config.enableInternet {
|
|
||||||
args.append("--enable-internet-connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding default args directly into additionalArgs
|
|
||||||
if config.disableVSync {
|
|
||||||
// args.append("--disable-vsync")
|
|
||||||
}
|
|
||||||
if config.disableShaderCache {
|
if config.disableShaderCache {
|
||||||
args.append("--disable-shader-cache")
|
args.append("--disable-shader-cache")
|
||||||
}
|
}
|
||||||
@ -204,9 +198,9 @@ class Ryujinx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getConnectedControllers() -> [Controller] {
|
func getConnectedControllers() -> [Controller] {
|
||||||
var nill: String?
|
|
||||||
|
|
||||||
guard let jsonPtr = nill else {//get_game_controllers() else {
|
guard let jsonPtr = get_game_controllers() else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>MeloID</key>
|
||||||
|
<string></string>
|
||||||
<key>UIFileSharingEnabled</key>
|
<key>UIFileSharingEnabled</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -6,12 +6,118 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct MeloNXApp: App {
|
struct MeloNXApp: App {
|
||||||
|
|
||||||
|
@AppStorage("showeddrmcheck") var showed = true
|
||||||
|
|
||||||
|
init() {
|
||||||
|
DispatchQueue.main.async { [self] in
|
||||||
|
// drmcheck()
|
||||||
|
if showed {
|
||||||
|
drmcheck() { bool in
|
||||||
|
if bool {
|
||||||
|
print("Yippee")
|
||||||
|
} else {
|
||||||
|
// exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showAlert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
if showed {
|
||||||
|
ContentView()
|
||||||
|
} else {
|
||||||
|
HStack {
|
||||||
|
Text("Loading...")
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showAlert() {
|
||||||
|
// Create the alert controller
|
||||||
|
if let mainWindow = UIApplication.shared.windows.last {
|
||||||
|
let alertController = UIAlertController(title: "Enter license", message: "Enter license key:", preferredStyle: .alert)
|
||||||
|
|
||||||
|
// Add a text field to the alert
|
||||||
|
alertController.addTextField { textField in
|
||||||
|
textField.placeholder = "Enter key here"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the "OK" action
|
||||||
|
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
||||||
|
// Get the text entered in the text field
|
||||||
|
if let textField = alertController.textFields?.first, let enteredText = textField.text {
|
||||||
|
print("Entered text: \(enteredText)")
|
||||||
|
UserDefaults.standard.set(enteredText, forKey: "MeloDRMID")
|
||||||
|
drmcheck() { bool in
|
||||||
|
if bool {
|
||||||
|
showed = true
|
||||||
|
} else {
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alertController.addAction(okAction)
|
||||||
|
|
||||||
|
// Add a "Cancel" action
|
||||||
|
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
|
||||||
|
alertController.addAction(cancelAction)
|
||||||
|
|
||||||
|
// Present the alert
|
||||||
|
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
|
||||||
|
} else {
|
||||||
|
exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func drmcheck(completion: @escaping (Bool) -> Void) {
|
||||||
|
if let deviceid = UIDevice.current.identifierForVendor?.uuidString, let base64device = deviceid.data(using: .utf8)?.base64EncodedString() {
|
||||||
|
if let value = UserDefaults.standard.string(forKey: "MeloDRMID") {
|
||||||
|
if let url = URL(string: "https://mx.stossy11.com/auth/\(value)/\(base64device)") {
|
||||||
|
print(url)
|
||||||
|
// Create a URLSession
|
||||||
|
let session = URLSession.shared
|
||||||
|
|
||||||
|
// Create a data task
|
||||||
|
let task = session.dataTask(with: url) { data, response, error in
|
||||||
|
// Handle errors
|
||||||
|
if let error = error {
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check response and data
|
||||||
|
if let response = response as? HTTPURLResponse, response.statusCode == 200 {
|
||||||
|
print("Successfully Recieved API Data")
|
||||||
|
completion(true)
|
||||||
|
} else if let response = response as? HTTPURLResponse, response.statusCode == 201 {
|
||||||
|
print("Successfully Created Auth UUID")
|
||||||
|
completion(true)
|
||||||
|
} else {
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the task
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
45
src/MeloNX/MeloNX/Models/Game.swift
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// GameInfo.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 9/12/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
public struct Game: Identifiable, Equatable {
|
||||||
|
public var id = UUID()
|
||||||
|
|
||||||
|
var containerFolder: URL
|
||||||
|
var fileType: UTType
|
||||||
|
|
||||||
|
var fileURL: URL
|
||||||
|
|
||||||
|
var titleName: String
|
||||||
|
var titleId: String
|
||||||
|
var developer: String
|
||||||
|
var version: String
|
||||||
|
var icon: UIImage?
|
||||||
|
|
||||||
|
func createImage(from gameInfo: GameInfo) -> UIImage? {
|
||||||
|
// Access the struct
|
||||||
|
let gameInfoValue = gameInfo
|
||||||
|
|
||||||
|
// Get the image data
|
||||||
|
let imageSize = Int(gameInfoValue.ImageSize)
|
||||||
|
guard imageSize > 0, imageSize <= 1024 * 1024 else {
|
||||||
|
print("Invalid image size.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the ImageData byte array to Swift's Data
|
||||||
|
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
|
||||||
|
|
||||||
|
// Create a UIImage (or NSImage on macOS)
|
||||||
|
|
||||||
|
print(imageData)
|
||||||
|
|
||||||
|
return UIImage(data: imageData)
|
||||||
|
}
|
||||||
|
}
|
@ -6,13 +6,16 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SDL2
|
// import SDL2
|
||||||
import GameController
|
import GameController
|
||||||
|
import Darwin
|
||||||
|
import UIKit
|
||||||
|
import MetalKit
|
||||||
|
// import SDL
|
||||||
|
|
||||||
struct MoltenVKSettings: Codable, Hashable {
|
struct MoltenVKSettings: Codable, Hashable {
|
||||||
let string: String
|
let string: String
|
||||||
var bool: Bool?
|
var value: String
|
||||||
var value: String?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@ -25,38 +28,36 @@ struct ContentView: View {
|
|||||||
@State private var config: Ryujinx.Configuration
|
@State private var config: Ryujinx.Configuration
|
||||||
@State private var settings: [MoltenVKSettings]
|
@State private var settings: [MoltenVKSettings]
|
||||||
@State private var isVirtualControllerActive: Bool = false
|
@State private var isVirtualControllerActive: Bool = false
|
||||||
|
@AppStorage("isVirtualController") var isVCA: Bool = true
|
||||||
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
||||||
|
@AppStorage("JIT") var isJITEnabled: Bool = false
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
init() {
|
init() {
|
||||||
let defaultConfig = Ryujinx.Configuration(gamepath: "")
|
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
||||||
_config = State(initialValue: defaultConfig)
|
_config = State(initialValue: defaultConfig)
|
||||||
|
|
||||||
let defaultSettings: [MoltenVKSettings] = [
|
let defaultSettings: [MoltenVKSettings] = [
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "1024"),
|
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "192"),
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", value: "1"),
|
MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2"),
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_RESUME_LOST_DEVICE", value: "1")
|
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
|
||||||
|
MoltenVKSettings(string: "MVK_CONFIG_RESUME_LOST_DEVICE", value: "1"),
|
||||||
|
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1")
|
||||||
]
|
]
|
||||||
|
|
||||||
_settings = State(initialValue: defaultSettings)
|
_settings = State(initialValue: defaultSettings)
|
||||||
|
|
||||||
|
print("JIT Enabled: \(isJITEnabled)")
|
||||||
|
|
||||||
initializeSDL()
|
initializeSDL()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
if let game {
|
||||||
if let game {
|
emulationView
|
||||||
emulationView
|
} else {
|
||||||
} else {
|
mainMenuView
|
||||||
mainMenuView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: isVirtualControllerActive) { newValue in
|
|
||||||
if newValue {
|
|
||||||
createVirtualController()
|
|
||||||
} else {
|
|
||||||
destroyVirtualController()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,138 +70,93 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var mainMenuView: some View {
|
private var mainMenuView: some View {
|
||||||
HStack {
|
MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
|
||||||
GameListView(startemu: $game)
|
.onAppear() {
|
||||||
.onAppear {
|
refreshControllersList()
|
||||||
createVirtualController()
|
|
||||||
refreshControllersList()
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsListView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var settingsListView: some View {
|
|
||||||
List {
|
|
||||||
Section("Settings") {
|
|
||||||
NavigationLink("Config") {
|
|
||||||
SettingsView(config: $config, MoltenVKSettings: $settings)
|
|
||||||
.onAppear() {
|
|
||||||
virtualController?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Controller") {
|
|
||||||
Button("Refresh", action: refreshControllersList)
|
|
||||||
Divider()
|
|
||||||
ForEach(controllersList, id: \.self) { controller in
|
|
||||||
controllerRow(for: controller)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func controllerRow(for controller: Controller) -> some View {
|
|
||||||
HStack {
|
|
||||||
Button(controller.name) {
|
|
||||||
toggleController(controller)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
if currentControllers.contains(where: { $0.id == controller.id }) {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Controller Management
|
|
||||||
private func createVirtualController() {
|
|
||||||
let configuration = GCVirtualController.Configuration()
|
|
||||||
configuration.elements = [
|
|
||||||
/*
|
|
||||||
GCInputLeftThumbstick,
|
|
||||||
GCInputRightThumbstick,
|
|
||||||
GCInputButtonA,
|
|
||||||
GCInputButtonB,
|
|
||||||
GCInputButtonX,
|
|
||||||
GCInputButtonY,
|
|
||||||
*/
|
|
||||||
]
|
|
||||||
|
|
||||||
virtualController = GCVirtualController(configuration: configuration)
|
|
||||||
virtualController?.connect()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func destroyVirtualController() {
|
|
||||||
virtualController?.disconnect()
|
|
||||||
virtualController = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
// MARK: - Helper Methods
|
||||||
|
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO;
|
||||||
private func initializeSDL() {
|
private func initializeSDL() {
|
||||||
DispatchQueue.main.async {
|
setMoltenVKSettings()
|
||||||
setMoltenVKSettings()
|
SDL_SetMainReady()
|
||||||
SDL_SetMainReady()
|
SDL_iPhoneSetEventPump(SDL_TRUE)
|
||||||
SDL_iPhoneSetEventPump(SDL_TRUE)
|
SDL_Init(SdlInitFlags)
|
||||||
SDL_Init(SDL_INIT_VIDEO)
|
// initialize()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupEmulation() {
|
private func setupEmulation() {
|
||||||
virtualController?.disconnect()
|
virtualController?.disconnect()
|
||||||
|
patchMakeKeyAndVisible()
|
||||||
|
|
||||||
if controllersList.first(where: { $0 == onscreencontroller}) != nil {
|
if (currentControllers.first(where: { $0 == onscreencontroller }) != nil) {
|
||||||
controllerCallback = {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
controllersList = Ryujinx.shared.getConnectedControllers()
|
|
||||||
|
|
||||||
print(currentControllers)
|
|
||||||
start(displayid: 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
isVCA = true
|
||||||
showVirtualController()
|
|
||||||
} else {
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
print(currentControllers)
|
|
||||||
start(displayid: 1)
|
start(displayid: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
isVCA = false
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
start(displayid: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshControllersList() {
|
private func refreshControllersList() {
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
|
|
||||||
controllersList = Ryujinx.shared.getConnectedControllers()
|
|
||||||
var controller = controllersList.first(where: { $0.name.hasPrefix("Apple")})
|
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
|
||||||
self.onscreencontroller = (controller ?? Controller(id: "", name: ""))
|
self.onscreencontroller = onscreen
|
||||||
if controllersList.count > 2 {
|
|
||||||
let controller = controllersList[2]
|
|
||||||
currentControllers.append(controller)
|
|
||||||
|
|
||||||
} else if let controller = controllersList.first(where: { $0.id == onscreencontroller.id }), !controllersList.isEmpty {
|
|
||||||
currentControllers.append(controller)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
controllersList.removeAll(where: { $0.id == "0"})
|
||||||
private func toggleController(_ controller: Controller) {
|
|
||||||
if currentControllers.contains(where: { $0.id == controller.id }) {
|
if controllersList.count > 2 {
|
||||||
currentControllers.removeAll(where: { $0.id == controller.id })
|
let controller = controllersList[2]
|
||||||
} else {
|
currentControllers.append(controller)
|
||||||
|
} else if let controller = controllersList.first(where: { $0.id == onscreencontroller.id }), !controllersList.isEmpty {
|
||||||
currentControllers.append(controller)
|
currentControllers.append(controller)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showAlert(title: String, message: String, showOk: Bool, completion: @escaping (Bool) -> Void) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let mainWindow = UIApplication.shared.windows.last {
|
||||||
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
|
||||||
|
if showOk {
|
||||||
|
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.addAction(okAction)
|
||||||
|
} else {
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.rootViewController?.present(alert, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func start(displayid: UInt32) {
|
private func start(displayid: UInt32) {
|
||||||
guard let game else { return }
|
guard let game else { return }
|
||||||
|
|
||||||
config.gamepath = game.path
|
config.gamepath = game.path
|
||||||
config.inputids = currentControllers.map(\.id)
|
config.inputids = Array(Set(currentControllers.map(\.id)))
|
||||||
|
|
||||||
allocateMemory()
|
if config.inputids.isEmpty {
|
||||||
|
config.inputids.append("0")
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try Ryujinx.shared.start(with: config)
|
try Ryujinx.shared.start(with: config)
|
||||||
@ -208,22 +164,9 @@ struct ContentView: View {
|
|||||||
print("Error: \(error.localizedDescription)")
|
print("Error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func allocateMemory() {
|
|
||||||
let physicalMemory = ProcessInfo.processInfo.physicalMemory
|
|
||||||
let totalMemoryInGB = Double(physicalMemory) / (1024 * 1024 * 1024)
|
|
||||||
|
|
||||||
let pointer = UnsafeMutableRawPointer.allocate(
|
|
||||||
byteCount: Int(totalMemoryInGB),
|
|
||||||
alignment: MemoryLayout<UInt8>.alignment
|
|
||||||
)
|
|
||||||
pointer.initializeMemory(as: UInt8.self, repeating: 0, count: Int(totalMemoryInGB))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setMoltenVKSettings() {
|
private func setMoltenVKSettings() {
|
||||||
if let configs = loadSettings() {
|
|
||||||
self.config = configs
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.forEach { setting in
|
settings.forEach { setting in
|
||||||
setenv(setting.string, setting.value, 1)
|
setenv(setting.string, setting.value, 1)
|
||||||
@ -245,3 +188,4 @@ func loadSettings() -> Ryujinx.Configuration? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
268
src/MeloNX/MeloNX/Views/ControllerView/ControllerView.swift
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
//
|
||||||
|
// ControllerView.swift
|
||||||
|
// Pomelo-V2
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 16/7/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import GameController
|
||||||
|
import SwiftUIJoystick
|
||||||
|
import CoreMotion
|
||||||
|
|
||||||
|
struct ControllerView: View {
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
VStack {
|
||||||
|
ShoulderButtonsViewLeft()
|
||||||
|
ZStack {
|
||||||
|
Joystick()
|
||||||
|
DPadView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
VStack {
|
||||||
|
ShoulderButtonsViewRight()
|
||||||
|
ZStack {
|
||||||
|
Joystick(iscool: true) // hope this works
|
||||||
|
ABXYView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
ButtonView(button: .start) // Adding the + button
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
ButtonView(button: .back) // Adding the - button
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, geometry.size.height / 3.2) // very broken
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// could be landscape
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
|
||||||
|
// gotta fuckin add + and - now
|
||||||
|
VStack {
|
||||||
|
ShoulderButtonsViewLeft()
|
||||||
|
ZStack {
|
||||||
|
Joystick()
|
||||||
|
DPadView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
// Spacer()
|
||||||
|
VStack {
|
||||||
|
// Spacer()
|
||||||
|
ButtonView(button: .back) // Adding the + button
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
VStack {
|
||||||
|
// Spacer()
|
||||||
|
ButtonView(button: .start) // Adding the - button
|
||||||
|
}
|
||||||
|
// Spacer()
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
ShoulderButtonsViewRight()
|
||||||
|
ZStack {
|
||||||
|
Joystick(iscool: true) // hope this work s
|
||||||
|
ABXYView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// .padding(.bottom, geometry.size.height / 11) // also extremally broken (
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShoulderButtonsViewLeft: View {
|
||||||
|
@State var width: CGFloat = 160
|
||||||
|
@State var height: CGFloat = 20
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
ButtonView(button: .leftTrigger)
|
||||||
|
.padding(.horizontal)
|
||||||
|
ButtonView(button: .leftShoulder)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.frame(width: width, height: height)
|
||||||
|
.onAppear() {
|
||||||
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
|
width *= 1.2
|
||||||
|
height *= 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShoulderButtonsViewRight: View {
|
||||||
|
@State var width: CGFloat = 160
|
||||||
|
@State var height: CGFloat = 20
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
ButtonView(button: .rightShoulder)
|
||||||
|
.padding(.horizontal)
|
||||||
|
ButtonView(button: .rightTrigger)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.frame(width: width, height: height)
|
||||||
|
.onAppear() {
|
||||||
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
|
width *= 1.2
|
||||||
|
height *= 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DPadView: View {
|
||||||
|
@State var size: CGFloat = 145
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
ButtonView(button: .dPadUp)
|
||||||
|
HStack {
|
||||||
|
ButtonView(button: .dPadLeft)
|
||||||
|
Spacer(minLength: 20)
|
||||||
|
ButtonView(button: .dPadRight)
|
||||||
|
}
|
||||||
|
ButtonView(button: .dPadDown)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.onAppear() {
|
||||||
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
|
size *= 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ABXYView: View {
|
||||||
|
@State var size: CGFloat = 145
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
ButtonView(button: .X)
|
||||||
|
HStack {
|
||||||
|
ButtonView(button: .Y)
|
||||||
|
Spacer(minLength: 20)
|
||||||
|
ButtonView(button: .A)
|
||||||
|
}
|
||||||
|
ButtonView(button: .B)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.onAppear() {
|
||||||
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
|
size *= 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ButtonView: View {
|
||||||
|
var button: VirtualControllerButton
|
||||||
|
@State var width: CGFloat = 45
|
||||||
|
@State var height: CGFloat = 45
|
||||||
|
@State var isPressed = false
|
||||||
|
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
@Environment(\.presentationMode) var presentationMode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Image(systemName: buttonText)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: width, height: height)
|
||||||
|
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
|
||||||
|
.opacity(isPressed ? 0.4 : 0.7)
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 0)
|
||||||
|
.onChanged { _ in
|
||||||
|
if !self.isPressed {
|
||||||
|
self.isPressed = true
|
||||||
|
Ryujinx.shared.virtualController.setButtonState(1, for: button)
|
||||||
|
Haptics.shared.play(.heavy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
self.isPressed = false
|
||||||
|
Ryujinx.shared.virtualController.setButtonState(0, for: button)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onAppear() {
|
||||||
|
if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder {
|
||||||
|
width = 65
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if button == .back || button == .start || button == .guide {
|
||||||
|
width = 35
|
||||||
|
height = 35
|
||||||
|
}
|
||||||
|
|
||||||
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
|
width *= 1.2
|
||||||
|
height *= 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private var buttonText: String {
|
||||||
|
switch button {
|
||||||
|
case .A:
|
||||||
|
return "a.circle.fill"
|
||||||
|
case .B:
|
||||||
|
return "b.circle.fill"
|
||||||
|
case .X:
|
||||||
|
return "x.circle.fill"
|
||||||
|
case .Y:
|
||||||
|
return "y.circle.fill"
|
||||||
|
case .dPadUp:
|
||||||
|
return "arrowtriangle.up.circle.fill"
|
||||||
|
case .dPadDown:
|
||||||
|
return "arrowtriangle.down.circle.fill"
|
||||||
|
case .dPadLeft:
|
||||||
|
return "arrowtriangle.left.circle.fill"
|
||||||
|
case .dPadRight:
|
||||||
|
return "arrowtriangle.right.circle.fill"
|
||||||
|
case .leftTrigger:
|
||||||
|
return"zl.rectangle.roundedtop.fill"
|
||||||
|
case .rightTrigger:
|
||||||
|
return "zr.rectangle.roundedtop.fill"
|
||||||
|
case .leftShoulder:
|
||||||
|
return "l.rectangle.roundedbottom.fill"
|
||||||
|
case .rightShoulder:
|
||||||
|
return "r.rectangle.roundedbottom.fill"
|
||||||
|
case .start:
|
||||||
|
return "plus.circle.fill" // System symbol for +
|
||||||
|
case .back:
|
||||||
|
return "minus.circle.fill" // System symbol for -
|
||||||
|
case .guide:
|
||||||
|
return "house.circle.fill"
|
||||||
|
// This should be all the cases
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
27
src/MeloNX/MeloNX/Views/ControllerView/Haptics/Haptics.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// Haptics.swift
|
||||||
|
// Pomelo
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 11/9/2024.
|
||||||
|
// Copyright © 2024 Stossy11. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class Haptics {
|
||||||
|
static let shared = Haptics()
|
||||||
|
|
||||||
|
private init() { }
|
||||||
|
|
||||||
|
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||||
|
print("haptics")
|
||||||
|
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
|
||||||
|
}
|
||||||
|
|
||||||
|
func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) {
|
||||||
|
UINotificationFeedbackGenerator().notificationOccurred(feedbackType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickBase@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickBase@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickBase@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 8.6 KiB |
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickHandle@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickHandle@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickHandle@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 4.4 KiB |
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickBaseCustom@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickBaseCustom@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickBaseCustom@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 6.4 KiB |
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickHandleCustom@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickHandleCustom@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "JoyStickHandleCustom@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 54 KiB |
@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// JoystickView.swift
|
||||||
|
// Pomelo
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 30/9/2024.
|
||||||
|
// Copyright © 2024 Stossy11. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftUIJoystick
|
||||||
|
|
||||||
|
public struct Joystick: View {
|
||||||
|
@State var iscool: Bool? = nil
|
||||||
|
|
||||||
|
@ObservedObject public var joystickMonitor = JoystickMonitor()
|
||||||
|
var dragDiameter: CGFloat {
|
||||||
|
var selfs = CGFloat(160)
|
||||||
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
|
return selfs * 1.2
|
||||||
|
}
|
||||||
|
return selfs
|
||||||
|
}
|
||||||
|
private let shape: JoystickShape = .circle
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
VStack{
|
||||||
|
JoystickBuilder(
|
||||||
|
monitor: self.joystickMonitor,
|
||||||
|
width: self.dragDiameter,
|
||||||
|
shape: .circle,
|
||||||
|
background: {
|
||||||
|
Text("")
|
||||||
|
.hidden()
|
||||||
|
},
|
||||||
|
foreground: {
|
||||||
|
Circle().fill(Color.gray)
|
||||||
|
.opacity(0.7)
|
||||||
|
},
|
||||||
|
locksInPlace: false)
|
||||||
|
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
|
||||||
|
let scaledX = Float(newValue.x)
|
||||||
|
let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/
|
||||||
|
print("Joystick Position: (\(scaledX), \(scaledY))")
|
||||||
|
|
||||||
|
if iscool != nil {
|
||||||
|
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
|
||||||
|
} else {
|
||||||
|
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: newValue.x, y: newValue.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,25 +5,147 @@
|
|||||||
// Created by Stossy11 on 3/11/2024.
|
// Created by Stossy11 on 3/11/2024.
|
||||||
//
|
//
|
||||||
|
|
||||||
// MARK: - This will most likely not be used in prod
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct GameListView: View {
|
|
||||||
|
struct GameLibraryView: View {
|
||||||
@Binding var startemu: URL?
|
@Binding var startemu: URL?
|
||||||
@State private var games: [URL] = []
|
@State private var games: [Game] = []
|
||||||
|
@State private var searchText = ""
|
||||||
|
@State private var isSearching = false
|
||||||
|
@AppStorage("recentGames") private var recentGamesData: Data = Data()
|
||||||
|
@State private var recentGames: [Game] = []
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
var filteredGames: [Game] {
|
||||||
|
if searchText.isEmpty {
|
||||||
|
return games
|
||||||
|
}
|
||||||
|
return games.filter {
|
||||||
|
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
|
||||||
|
$0.developer.localizedCaseInsensitiveContains(searchText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List(games, id: \.self) { game in
|
iOSNav {
|
||||||
Button {
|
ScrollView {
|
||||||
startemu = game
|
LazyVStack(alignment: .leading, spacing: 20) {
|
||||||
} label: {
|
if !isSearching {
|
||||||
Text(game.lastPathComponent)
|
Text("Games")
|
||||||
|
.font(.system(size: 34, weight: .bold))
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.top, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
if games.isEmpty {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Image(systemName: "gamecontroller.fill")
|
||||||
|
.font(.system(size: 64))
|
||||||
|
.foregroundColor(.secondary.opacity(0.7))
|
||||||
|
.padding(.top, 60)
|
||||||
|
Text("No Games Found")
|
||||||
|
.font(.title2.bold())
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text("Add ROM, Keys and Firmware to get started")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.top, 40)
|
||||||
|
} else {
|
||||||
|
if !isSearching && !recentGames.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Recent")
|
||||||
|
.font(.title2.bold())
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack(spacing: 16) {
|
||||||
|
ForEach(recentGames) { game in
|
||||||
|
RecentGameCard(game: game, startemu: $startemu)
|
||||||
|
.onTapGesture {
|
||||||
|
addToRecentGames(game)
|
||||||
|
startemu = game.fileURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("All Games")
|
||||||
|
.font(.title2.bold())
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
LazyVStack(spacing: 2) {
|
||||||
|
ForEach(filteredGames) { game in
|
||||||
|
GameListRow(game: game, startemu: $startemu)
|
||||||
|
.onTapGesture {
|
||||||
|
addToRecentGames(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyVStack(spacing: 2) {
|
||||||
|
ForEach(filteredGames) { game in
|
||||||
|
GameListRow(game: game, startemu: $startemu)
|
||||||
|
.onTapGesture {
|
||||||
|
addToRecentGames(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
loadGames()
|
||||||
|
loadRecentGames()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Games")
|
.background(Color(.systemGroupedBackground))
|
||||||
.onAppear(perform: loadGames)
|
.searchable(text: $searchText)
|
||||||
|
.onChange(of: searchText) { _ in
|
||||||
|
isSearching = !searchText.isEmpty
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addToRecentGames(_ game: Game) {
|
||||||
|
recentGames.removeAll { $0.id == game.id }
|
||||||
|
|
||||||
|
recentGames.insert(game, at: 0)
|
||||||
|
|
||||||
|
if recentGames.count > 5 {
|
||||||
|
recentGames = Array(recentGames.prefix(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
saveRecentGames()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveRecentGames() {
|
||||||
|
do {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(recentGames)
|
||||||
|
recentGamesData = data
|
||||||
|
} catch {
|
||||||
|
print("Error saving recent games: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadRecentGames() {
|
||||||
|
do {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
recentGames = try decoder.decode([Game].self, from: recentGamesData)
|
||||||
|
} catch {
|
||||||
|
print("Error loading recent games: \(error)")
|
||||||
|
recentGames = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func loadGames() {
|
private func loadGames() {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||||
@ -38,13 +160,187 @@ struct GameListView: View {
|
|||||||
print("Failed to create roms directory: \(error)")
|
print("Failed to create roms directory: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
games = []
|
||||||
// Load games only from "roms" folder
|
// Load games only from "roms" folder
|
||||||
do {
|
do {
|
||||||
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
|
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
|
||||||
games = files
|
|
||||||
|
files.forEach { fileURLCandidate in
|
||||||
|
do {
|
||||||
|
let handle = try FileHandle(forReadingFrom: fileURLCandidate)
|
||||||
|
let fileExtension = (fileURLCandidate.pathExtension as NSString).utf8String
|
||||||
|
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
||||||
|
|
||||||
|
|
||||||
|
var game = Game(containerFolder: romsDirectory, fileType: .item, fileURL: fileURLCandidate, titleName: fileURLCandidate.lastPathComponent, titleId: "", developer: "", version: "")
|
||||||
|
|
||||||
|
/*
|
||||||
|
game.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
|
||||||
|
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||||
|
String(cString: $0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
game.developer = withUnsafePointer(to: &gameInfo.Developer) {
|
||||||
|
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
||||||
|
String(cString: $0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
games.append(game)
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
print("Error loading games from roms folder: \(error)")
|
print("Error loading games from roms folder: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure your Game model conforms to Codable
|
||||||
|
extension Game: Codable {
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case titleName, titleId, developer, version, fileURL
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
titleName = try container.decode(String.self, forKey: .titleName)
|
||||||
|
titleId = try container.decode(String.self, forKey: .titleId)
|
||||||
|
developer = try container.decode(String.self, forKey: .developer)
|
||||||
|
version = try container.decode(String.self, forKey: .version)
|
||||||
|
fileURL = try container.decode(URL.self, forKey: .fileURL)
|
||||||
|
|
||||||
|
// Initialize other properties
|
||||||
|
self.containerFolder = fileURL.deletingLastPathComponent()
|
||||||
|
self.fileType = .item
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(titleName, forKey: .titleName)
|
||||||
|
try container.encode(titleId, forKey: .titleId)
|
||||||
|
try container.encode(developer, forKey: .developer)
|
||||||
|
try container.encode(version, forKey: .version)
|
||||||
|
try container.encode(fileURL, forKey: .fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RecentGameCard: View {
|
||||||
|
let game: Game
|
||||||
|
@Binding var startemu: URL?
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
startemu = game.fileURL
|
||||||
|
}) {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
if let icon = game.icon {
|
||||||
|
Image(uiImage: icon)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: 140, height: 140)
|
||||||
|
.cornerRadius(12)
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(colorScheme == .dark ?
|
||||||
|
Color(.systemGray5) : Color(.systemGray6))
|
||||||
|
.frame(width: 140, height: 140)
|
||||||
|
|
||||||
|
Image(systemName: "gamecontroller.fill")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(game.titleName)
|
||||||
|
.font(.subheadline.bold())
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text(game.developer)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GameListRow: View {
|
||||||
|
let game: Game
|
||||||
|
@Binding var startemu: URL?
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
startemu = game.fileURL
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
// Game Icon
|
||||||
|
if let icon = game.icon {
|
||||||
|
Image(uiImage: icon)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: 45, height: 45)
|
||||||
|
.cornerRadius(8)
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(colorScheme == .dark ?
|
||||||
|
Color(.systemGray5) : Color(.systemGray6))
|
||||||
|
.frame(width: 45, height: 45)
|
||||||
|
|
||||||
|
Image(systemName: "gamecontroller.fill")
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game Info
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(game.titleName)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text(game.developer)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "play.circle.fill")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.opacity(0.8)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
startemu = game.fileURL
|
||||||
|
} label: {
|
||||||
|
Label("Play Now", systemImage: "play.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
// Add info action
|
||||||
|
} label: {
|
||||||
|
Label("Game Info", systemImage: "info.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
//
|
|
||||||
// VulkanSDLView.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 3/11/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import MetalKit
|
|
||||||
import SDL2
|
|
||||||
|
|
||||||
class SDLView: UIView {
|
|
||||||
var sdlwin: OpaquePointer?
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
DispatchQueue.main.async { [self] in
|
|
||||||
makeSDLWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
DispatchQueue.main.async { [self] in
|
|
||||||
makeSDLWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWindowFlags() -> UInt32 {
|
|
||||||
return SDL_WINDOW_VULKAN.rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeSDLWindow() {
|
|
||||||
let width: Int32 = 1280 // Replace with the desired width
|
|
||||||
let height: Int32 = 720 // Replace with the desired height
|
|
||||||
|
|
||||||
let defaultFlags: UInt32 = SDL_WINDOW_SHOWN.rawValue
|
|
||||||
let fullscreenFlag: UInt32 = SDL_WINDOW_FULLSCREEN.rawValue // Or SDL_WINDOW_FULLSCREEN_DESKTOP if needed
|
|
||||||
|
|
||||||
// Create the SDL window
|
|
||||||
sdlwin = SDL_CreateWindow(
|
|
||||||
"Ryujinx",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
defaultFlags | getWindowFlags() // | fullscreenFlag | getWindowFlags()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if we successfully retrieved the SDL window
|
|
||||||
guard sdlwin != nil else {
|
|
||||||
print("Error creating SDL window: \(String(cString: SDL_GetError()))")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("SDL window created successfully.")
|
|
||||||
|
|
||||||
// Position SDL window over this UIView
|
|
||||||
self.syncSDLWindowPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func syncSDLWindowPosition() {
|
|
||||||
guard let sdlwin = sdlwin else { return }
|
|
||||||
|
|
||||||
|
|
||||||
// Get the frame of the UIView in screen coordinates
|
|
||||||
let viewFrameInWindow = self.convert(self.bounds, to: nil)
|
|
||||||
|
|
||||||
// Set the SDL window position and size to match the UIView frame
|
|
||||||
SDL_SetWindowPosition(sdlwin, Int32(viewFrameInWindow.origin.x), Int32(viewFrameInWindow.origin.y))
|
|
||||||
SDL_SetWindowSize(sdlwin, Int32(viewFrameInWindow.width), Int32(viewFrameInWindow.height))
|
|
||||||
|
|
||||||
// Bring SDL window to the front
|
|
||||||
SDL_RaiseWindow(sdlwin)
|
|
||||||
|
|
||||||
print("SDL window positioned over SDLView.")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
// Adjust SDL window whenever layout changes
|
|
||||||
syncSDLWindowPosition()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
//
|
|
||||||
// VulkanSDLViewRepresentable.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 3/11/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import SwiftUI
|
|
||||||
import SDL2
|
|
||||||
import GameController
|
|
||||||
|
|
||||||
struct SDLViewRepresentable: UIViewRepresentable {
|
|
||||||
let configure: (UInt32) -> Void
|
|
||||||
func makeUIView(context: Context) -> SDLView {
|
|
||||||
// Configure (start ryu) before initialsing SDLView so SDLView can get the SDL_Window from Ryu
|
|
||||||
let view = SDLView(frame: .zero)
|
|
||||||
configure(SDL_GetWindowID(view.sdlwin))
|
|
||||||
return view
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: SDLView, context: Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -11,6 +11,13 @@ struct SettingsView: View {
|
|||||||
@Binding var config: Ryujinx.Configuration
|
@Binding var config: Ryujinx.Configuration
|
||||||
@Binding var MoltenVKSettings: [MoltenVKSettings]
|
@Binding var MoltenVKSettings: [MoltenVKSettings]
|
||||||
|
|
||||||
|
@Binding var controllersList: [Controller]
|
||||||
|
@Binding var currentControllers: [Controller]
|
||||||
|
|
||||||
|
@Binding var onscreencontroller: Controller
|
||||||
|
|
||||||
|
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
||||||
|
|
||||||
var memoryManagerModes = [
|
var memoryManagerModes = [
|
||||||
("HostMapped", "Host (fast)"),
|
("HostMapped", "Host (fast)"),
|
||||||
("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
|
("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
|
||||||
@ -18,166 +25,294 @@ struct SettingsView: View {
|
|||||||
]
|
]
|
||||||
|
|
||||||
@AppStorage("RyuDemoControls") var ryuDemo: Bool = false
|
@AppStorage("RyuDemoControls") var ryuDemo: Bool = false
|
||||||
|
|
||||||
@AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false
|
@AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false
|
||||||
|
|
||||||
|
@State private var showResolutionInfo = false
|
||||||
|
@State private var searchText = ""
|
||||||
|
|
||||||
|
var filteredMemoryModes: [(String, String)] {
|
||||||
|
guard !searchText.isEmpty else { return memoryManagerModes }
|
||||||
|
return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
iOSNav {
|
||||||
VStack {
|
List {
|
||||||
Section(header: Title("Graphics and Performance")) {
|
// Graphics & Performance
|
||||||
Toggle("Ryujinx Fullscreen", isOn: $config.fullscreen)
|
Section {
|
||||||
Toggle("Disable V-Sync", isOn: $config.disableVSync)
|
Toggle(isOn: $config.fullscreen) {
|
||||||
Toggle("Disable Shader Cache", isOn: $config.disableShaderCache)
|
labelWithIcon("Fullscreen", iconName: "rectangle.expand.vertical")
|
||||||
Toggle("Enable Texture Recompression", isOn: $config.enableTextureRecompression)
|
}
|
||||||
Toggle("Disable Docked Mode", isOn: $config.disableDockedMode)
|
.tint(.blue)
|
||||||
Resolution(value: $config.resscale)
|
|
||||||
Toggle("Enable Metal HUD", isOn: $metalHUDEnabled)
|
Toggle(isOn: $config.disableShaderCache) {
|
||||||
.onChange(of: metalHUDEnabled) { newValue in
|
labelWithIcon("Disable Shader Cache", iconName: "memorychip")
|
||||||
if newValue {
|
}
|
||||||
MTLHud.shared.enable()
|
.tint(.blue)
|
||||||
} else {
|
|
||||||
MTLHud.shared.disable()
|
Toggle(isOn: $config.enableTextureRecompression) {
|
||||||
|
labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical")
|
||||||
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
|
||||||
|
Toggle(isOn: $config.disableDockedMode) {
|
||||||
|
labelWithIcon("Disable Docked Mode", iconName: "dock.rectangle")
|
||||||
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
showResolutionInfo.toggle()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "info.circle")
|
||||||
|
.symbolRenderingMode(.hierarchical)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Learn more about Resolution Scale")
|
||||||
|
.alert(isPresented: $showResolutionInfo) {
|
||||||
|
Alert(
|
||||||
|
title: Text("Resolution Scale"),
|
||||||
|
message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
|
||||||
|
dismissButton: .default(Text("OK"))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Slider(value: $config.resscale, in: 0.1...3.0, step: 0.1) {
|
||||||
|
Text("Resolution Scale")
|
||||||
|
} minimumValueLabel: {
|
||||||
|
Text("0.1x")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} maximumValueLabel: {
|
||||||
|
Text("3.0x")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
Text("\(config.resscale, specifier: "%.2f")x")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
Toggle(isOn: $metalHUDEnabled) {
|
||||||
|
labelWithIcon("Metal HUD", iconName: "speedometer")
|
||||||
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
.onChange(of: metalHUDEnabled) { newValue in
|
||||||
|
// Preserves original functionality
|
||||||
|
if newValue {
|
||||||
|
MTLHud.shared.enable()
|
||||||
|
} else {
|
||||||
|
MTLHud.shared.disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Graphics & Performance")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.textCase(nil)
|
||||||
|
.headerProminence(.increased)
|
||||||
|
} footer: {
|
||||||
|
Text("Fine-tune graphics and performance to suit your device and preferences.")
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Title("Input Settings")) {
|
// Input Selector
|
||||||
Toggle("List Input IDs", isOn: $config.listinputids)
|
Section {
|
||||||
Toggle("Nintendo Controller Layout", isOn: $config.nintendoinput)
|
ForEach(controllersList) { controller in
|
||||||
Toggle("Ryujinx Demo On-Screen Controller", isOn: $ryuDemo)
|
var customBinding: Binding<Bool> {
|
||||||
// Toggle("Host Mapped Memory", isOn: $config.hostMappedMemory)
|
Binding(
|
||||||
|
get: { currentControllers.contains(controller) },
|
||||||
|
set: { bool in
|
||||||
|
if !bool {
|
||||||
|
currentControllers.removeAll(where: { $0.id == controller.id })
|
||||||
|
} else {
|
||||||
|
currentControllers.append(controller)
|
||||||
|
}
|
||||||
|
// toggleController(controller)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: customBinding) {
|
||||||
|
labelWithIcon(controller.name, iconName: "")
|
||||||
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Input Selector")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.textCase(nil)
|
||||||
|
.headerProminence(.increased)
|
||||||
|
} footer: {
|
||||||
|
Text("Select input devices and on-screen controls to play with.")
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Title("Logging Settings")) {
|
// Input Settings
|
||||||
Toggle("Enable Debug Logs", isOn: $config.debuglogs)
|
Section {
|
||||||
Toggle("Enable Trace Logs", isOn: $config.tracelogs)
|
|
||||||
|
Toggle(isOn: $config.listinputids) {
|
||||||
|
labelWithIcon("List Input IDs", iconName: "list.bullet")
|
||||||
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
|
||||||
|
Toggle(isOn: $ryuDemo) {
|
||||||
|
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
|
||||||
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
.disabled(true)
|
||||||
|
} header: {
|
||||||
|
Text("Input Settings")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.textCase(nil)
|
||||||
|
.headerProminence(.increased)
|
||||||
|
} footer: {
|
||||||
|
Text("Configure input devices and on-screen controls for easier navigation and play.")
|
||||||
}
|
}
|
||||||
Section(header: Title("CPU Mode")) {
|
|
||||||
HStack {
|
// Logging
|
||||||
Spacer()
|
Section {
|
||||||
Picker("Memory Manager Mode", selection: $config.memoryManagerMode) {
|
Toggle(isOn: $config.debuglogs) {
|
||||||
ForEach(memoryManagerModes, id: \.0) { key, displayName in
|
labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble")
|
||||||
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
|
||||||
|
Toggle(isOn: $config.tracelogs) {
|
||||||
|
labelWithIcon("Trace Logs", iconName: "waveform.path")
|
||||||
|
}
|
||||||
|
.tint(.blue)
|
||||||
|
} header: {
|
||||||
|
Text("Logging")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.textCase(nil)
|
||||||
|
.headerProminence(.increased)
|
||||||
|
} footer: {
|
||||||
|
Text("Enable logs for troubleshooting or keep them off for a cleaner experience.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU Mode
|
||||||
|
Section {
|
||||||
|
if filteredMemoryModes.isEmpty {
|
||||||
|
Text("No matches for \"\(searchText)\"")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
Picker(selection: $config.memoryManagerMode) {
|
||||||
|
ForEach(filteredMemoryModes, id: \.0) { key, displayName in
|
||||||
Text(displayName).tag(key)
|
Text(displayName).tag(key)
|
||||||
}
|
}
|
||||||
|
} label: {
|
||||||
|
labelWithIcon("Memory Manager Mode", iconName: "gearshape")
|
||||||
}
|
}
|
||||||
.pickerStyle(MenuPickerStyle()) // Dropdown style
|
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
|
Text("CPU Mode")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.textCase(nil)
|
||||||
|
.headerProminence(.increased)
|
||||||
|
} footer: {
|
||||||
|
Text("Select how memory is managed. 'Host (fast)' is best for most users.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advanced
|
||||||
|
Section {
|
||||||
Section(header: Title("Additional Settings")) {
|
DisclosureGroup {
|
||||||
//TextField("Game Path", text: $config.gamepath)
|
HStack {
|
||||||
|
labelWithIcon("Page Size", iconName: "textformat.size")
|
||||||
Text("PageSize \(String(Int(getpagesize())))")
|
Spacer()
|
||||||
|
Text("\(String(Int(getpagesize())))")
|
||||||
TextField("Additional Arguments", text: Binding(
|
.foregroundColor(.secondary)
|
||||||
get: {
|
|
||||||
config.additionalArgs.joined(separator: ", ")
|
|
||||||
},
|
|
||||||
set: { newValue in
|
|
||||||
config.additionalArgs = newValue.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) }
|
|
||||||
}
|
}
|
||||||
))
|
|
||||||
|
TextField("Additional Arguments", text: Binding(
|
||||||
|
get: {
|
||||||
|
config.additionalArgs.joined(separator: ", ")
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
config.additionalArgs = newValue
|
||||||
|
.split(separator: ",")
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.textInputAutocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
} label: {
|
||||||
|
Text("Advanced Options")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Advanced")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.textCase(nil)
|
||||||
|
.headerProminence(.increased)
|
||||||
|
} footer: {
|
||||||
|
Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
|
||||||
}
|
.navigationTitle("Settings")
|
||||||
.onAppear {
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
if let configs = loadSettings() {
|
.listStyle(.insetGrouped)
|
||||||
self.config = configs
|
.onAppear {
|
||||||
print(configs)
|
if let configs = loadSettings() {
|
||||||
|
self.config = configs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: config) { _ in
|
||||||
|
saveSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Settings")
|
.navigationViewStyle(.stack)
|
||||||
.navigationBarItems(trailing: Button("Save") {
|
}
|
||||||
saveSettings()
|
|
||||||
})
|
private func toggleController(_ controller: Controller) {
|
||||||
|
if currentControllers.contains(where: { $0.id == controller.id }) {
|
||||||
|
currentControllers.removeAll(where: { $0.id == controller.id })
|
||||||
|
} else {
|
||||||
|
currentControllers.append(controller)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveSettings() {
|
func saveSettings() {
|
||||||
do {
|
do {
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
encoder.outputFormatting = .prettyPrinted // Optional: Makes the JSON easier to read
|
encoder.outputFormatting = .prettyPrinted
|
||||||
let data = try encoder.encode(config)
|
let data = try encoder.encode(config)
|
||||||
let jsonString = String(data: data, encoding: .utf8)
|
let jsonString = String(data: data, encoding: .utf8)
|
||||||
|
|
||||||
// Save to UserDefaults
|
|
||||||
UserDefaults.standard.set(jsonString, forKey: "config")
|
UserDefaults.standard.set(jsonString, forKey: "config")
|
||||||
|
|
||||||
print("Settings saved successfully!")
|
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to save settings: \(error)")
|
print("Failed to save settings: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Original loadSettings function assumed to exist
|
||||||
|
func loadSettings() -> Ryujinx.Configuration? {
|
||||||
struct Resolution: View {
|
guard let jsonString = UserDefaults.standard.string(forKey: "config"),
|
||||||
@Binding var value: Float
|
let data = jsonString.data(using: .utf8) else {
|
||||||
|
return nil
|
||||||
var body: some View {
|
}
|
||||||
HStack {
|
do {
|
||||||
Text("Resolution Scale (Custom):")
|
let decoder = JSONDecoder()
|
||||||
Spacer()
|
let configs = try decoder.decode(Ryujinx.Configuration.self, from: data)
|
||||||
|
return configs
|
||||||
Button(action: {
|
} catch {
|
||||||
if value > 0.1 { // Prevent values going below 0.1
|
print("Failed to load settings: \(error)")
|
||||||
value -= 0.10
|
return nil
|
||||||
value = round(value * 1000) / 1000 // Round to two decimal places
|
|
||||||
}
|
|
||||||
print(value)
|
|
||||||
}) {
|
|
||||||
Text("-")
|
|
||||||
.frame(width: 30, height: 30)
|
|
||||||
.background(Color.gray.opacity(0.2))
|
|
||||||
.cornerRadius(5)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField("", value: $value, formatter: NumberFormatter.floatFormatter)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.frame(width: 60)
|
|
||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
|
||||||
.keyboardType(.decimalPad)
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
value += 0.10
|
|
||||||
value = round(value * 1000) / 1000 // Round to two decimal places
|
|
||||||
print(value)
|
|
||||||
}) {
|
|
||||||
Text("+")
|
|
||||||
.frame(width: 30, height: 30)
|
|
||||||
.background(Color.gray.opacity(0.2))
|
|
||||||
.cornerRadius(5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension NumberFormatter {
|
|
||||||
static var floatFormatter: NumberFormatter {
|
|
||||||
let formatter = NumberFormatter()
|
|
||||||
formatter.numberStyle = .decimal
|
|
||||||
formatter.maximumFractionDigits = 2
|
|
||||||
formatter.minimumFractionDigits = 2
|
|
||||||
formatter.allowsFloats = true
|
|
||||||
return formatter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct Title: View {
|
|
||||||
let string: String
|
|
||||||
|
|
||||||
init(_ string: String) {
|
@ViewBuilder
|
||||||
self.string = string
|
private func labelWithIcon(_ text: String, iconName: String) -> some View {
|
||||||
}
|
HStack(spacing: 8) {
|
||||||
|
if !iconName.isEmpty {
|
||||||
var body: some View {
|
Image(systemName: iconName)
|
||||||
VStack {
|
.symbolRenderingMode(.hierarchical)
|
||||||
Text(string)
|
.foregroundStyle(.blue)
|
||||||
.font(.title2)
|
}
|
||||||
Divider()
|
Text(text)
|
||||||
}
|
}
|
||||||
|
.font(.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
src/MeloNX/MeloNX/Views/TabView/TabView.swift
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
//
|
||||||
|
// TabView.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 10/12/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
|
||||||
|
struct MainTabView: View {
|
||||||
|
@Binding var startemu: URL?
|
||||||
|
@Binding var config: Ryujinx.Configuration
|
||||||
|
@Binding var MVKconfig: [MoltenVKSettings]
|
||||||
|
@Binding var controllersList: [Controller]
|
||||||
|
@Binding var currentControllers: [Controller]
|
||||||
|
|
||||||
|
@Binding var onscreencontroller: Controller
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TabView {
|
||||||
|
GameLibraryView(startemu: $startemu)
|
||||||
|
.tabItem {
|
||||||
|
Label("Games", systemImage: "gamecontroller.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsView(config: $config, MoltenVKSettings: $MVKconfig, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
|
||||||
|
.tabItem {
|
||||||
|
Label("Settings", systemImage: "gear")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -65,7 +65,12 @@ namespace Ryujinx.Common.Configuration
|
|||||||
appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
string userProfilePath = Path.Combine(appDataPath, DefaultBaseDir);
|
string userProfilePath;
|
||||||
|
if (OperatingSystem.IsIOS()) {
|
||||||
|
userProfilePath = appDataPath;
|
||||||
|
} else {
|
||||||
|
userProfilePath = Path.Combine(appDataPath, DefaultBaseDir);
|
||||||
|
}
|
||||||
string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir);
|
string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir);
|
||||||
|
|
||||||
// On macOS, check for a portable directory next to the app bundle as well.
|
// On macOS, check for a portable directory next to the app bundle as well.
|
||||||
|
@ -27,6 +27,7 @@ namespace Ryujinx.Common.SystemInterop
|
|||||||
|
|
||||||
[SupportedOSPlatform("linux")]
|
[SupportedOSPlatform("linux")]
|
||||||
[SupportedOSPlatform("macos")]
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
private void RegisterPosix()
|
private void RegisterPosix()
|
||||||
{
|
{
|
||||||
const int StdErrFileno = 2;
|
const int StdErrFileno = 2;
|
||||||
@ -44,6 +45,7 @@ namespace Ryujinx.Common.SystemInterop
|
|||||||
|
|
||||||
[SupportedOSPlatform("linux")]
|
[SupportedOSPlatform("linux")]
|
||||||
[SupportedOSPlatform("macos")]
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
private async Task EventWorkerAsync(CancellationToken cancellationToken)
|
private async Task EventWorkerAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using TextReader reader = new StreamReader(_pipeReader, leaveOpen: true);
|
using TextReader reader = new StreamReader(_pipeReader, leaveOpen: true);
|
||||||
@ -92,6 +94,7 @@ namespace Ryujinx.Common.SystemInterop
|
|||||||
|
|
||||||
[SupportedOSPlatform("linux")]
|
[SupportedOSPlatform("linux")]
|
||||||
[SupportedOSPlatform("macos")]
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
private static Stream CreateFileDescriptorStream(int fd)
|
private static Stream CreateFileDescriptorStream(int fd)
|
||||||
{
|
{
|
||||||
return new FileStream(
|
return new FileStream(
|
||||||
@ -100,5 +103,6 @@ namespace Ryujinx.Common.SystemInterop
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,20 +134,12 @@ namespace Ryujinx.Common
|
|||||||
|
|
||||||
private static (Assembly, string) ResolveManifestPath(string filename)
|
private static (Assembly, string) ResolveManifestPath(string filename)
|
||||||
{
|
{
|
||||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
|
||||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
|
||||||
var segments = filename.Split('/', 2, StringSplitOptions.RemoveEmptyEntries);
|
var segments = filename.Split('/', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
if (segments.Length >= 2)
|
if (segments.Length >= 2)
|
||||||
{
|
{
|
||||||
foreach (var assembly in System.Runtime.Loader.AssemblyLoadContext.Default.Assemblies)
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
{
|
return (assembly, segments[1]);
|
||||||
if (assembly.GetName().Name == segments[0])
|
|
||||||
{
|
|
||||||
return (assembly, segments[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (_resourceAssembly, filename);
|
return (_resourceAssembly, filename);
|
||||||
|
@ -88,7 +88,7 @@ namespace Ryujinx.Cpu.Signal
|
|||||||
|
|
||||||
ref SignalHandlerConfig config = ref GetConfigRef();
|
ref SignalHandlerConfig config = ref GetConfigRef();
|
||||||
|
|
||||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
_signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateUnixSignalHandler(_handlerConfig, rangeStructSize));
|
_signalHandlerPtr = MapCode(NativeSignalHandlerGenerator.GenerateUnixSignalHandler(_handlerConfig, rangeStructSize));
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ namespace Ryujinx.Cpu.Signal
|
|||||||
throw new InvalidOperationException($"Could not register SIGSEGV sigaction. Error: {result}");
|
throw new InvalidOperationException($"Could not register SIGSEGV sigaction. Error: {result}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
result = sigaction(SIGBUS, ref sig, out _);
|
result = sigaction(SIGBUS, ref sig, out _);
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ namespace Ryujinx.Cpu.Signal
|
|||||||
|
|
||||||
public static bool RestoreExceptionHandler(SigAction oldAction)
|
public static bool RestoreExceptionHandler(SigAction oldAction)
|
||||||
{
|
{
|
||||||
return sigaction(SIGSEGV, ref oldAction, out SigAction _) == 0 && (!OperatingSystem.IsMacOS() || sigaction(SIGBUS, ref oldAction, out SigAction _) == 0);
|
return sigaction(SIGSEGV, ref oldAction, out SigAction _) == 0 && (!OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || sigaction(SIGBUS, ref oldAction, out SigAction _) == 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using System.Runtime.Versioning;
|
|||||||
namespace Ryujinx.Graphics.Vulkan.MoltenVK
|
namespace Ryujinx.Graphics.Vulkan.MoltenVK
|
||||||
{
|
{
|
||||||
[SupportedOSPlatform("macos")]
|
[SupportedOSPlatform("macos")]
|
||||||
|
[SupportedOSPlatform("ios")]
|
||||||
public static partial class MVKInitialization
|
public static partial class MVKInitialization
|
||||||
{
|
{
|
||||||
private const string VulkanLib = "libvulkan.dylib";
|
private const string VulkanLib = "libvulkan.dylib";
|
||||||
|
@ -14,6 +14,14 @@ using System.Runtime.InteropServices;
|
|||||||
using Format = Ryujinx.Graphics.GAL.Format;
|
using Format = Ryujinx.Graphics.GAL.Format;
|
||||||
using PrimitiveTopology = Ryujinx.Graphics.GAL.PrimitiveTopology;
|
using PrimitiveTopology = Ryujinx.Graphics.GAL.PrimitiveTopology;
|
||||||
using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo;
|
using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Resources;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
|
||||||
namespace Ryujinx.Graphics.Vulkan
|
namespace Ryujinx.Graphics.Vulkan
|
||||||
{
|
{
|
||||||
@ -498,6 +506,33 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
Queue = queue;
|
Queue = queue;
|
||||||
QueueLock = new object();
|
QueueLock = new object();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Set invariant culture
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
||||||
|
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
|
||||||
|
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
|
||||||
|
{
|
||||||
|
var assemblyName = new AssemblyName(args.Name);
|
||||||
|
assemblyName.CultureInfo = CultureInfo.InvariantCulture;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Assembly.Load(assemblyName);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to set culture: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
LoadFeatures(maxQueueCount, queueFamilyIndex);
|
LoadFeatures(maxQueueCount, queueFamilyIndex);
|
||||||
|
|
||||||
QueueFamilyIndex = queueFamilyIndex;
|
QueueFamilyIndex = queueFamilyIndex;
|
||||||
|
@ -98,7 +98,6 @@ namespace Ryujinx.HLE.HOS.Applets.Error
|
|||||||
SystemLanguage.CanadianFrench => "fr-CA",
|
SystemLanguage.CanadianFrench => "fr-CA",
|
||||||
SystemLanguage.LatinAmericanSpanish => "es-419",
|
SystemLanguage.LatinAmericanSpanish => "es-419",
|
||||||
SystemLanguage.SimplifiedChinese => "zh-Hans",
|
SystemLanguage.SimplifiedChinese => "zh-Hans",
|
||||||
SystemLanguage.TraditionalChinese => "zh-Hant",
|
|
||||||
SystemLanguage.BrazilianPortuguese => "pt-BR",
|
SystemLanguage.BrazilianPortuguese => "pt-BR",
|
||||||
_ => "en-US",
|
_ => "en-US",
|
||||||
#pragma warning restore IDE0055
|
#pragma warning restore IDE0055
|
||||||
|
@ -21,7 +21,6 @@ namespace Ryujinx.HLE.HOS.SystemState
|
|||||||
"fr-CA",
|
"fr-CA",
|
||||||
"es-419",
|
"es-419",
|
||||||
"zh-Hans",
|
"zh-Hans",
|
||||||
"zh-Hant",
|
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,10 +102,6 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
Version = "1";
|
Version = "1";
|
||||||
// Make process DPI aware for proper window sizing on high-res screens.
|
// Make process DPI aware for proper window sizing on high-res screens.
|
||||||
ForceDpiAware.Windows();
|
ForceDpiAware.Windows();
|
||||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
|
||||||
|
|
||||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
|
||||||
|
|
||||||
Silk.NET.Core.Loader.SearchPathContainer.Platform = Silk.NET.Core.Loader.UnderlyingPlatform.MacOS;
|
Silk.NET.Core.Loader.SearchPathContainer.Platform = Silk.NET.Core.Loader.UnderlyingPlatform.MacOS;
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
|
|
||||||
public NpadManager NpadManager { get; }
|
public NpadManager NpadManager { get; }
|
||||||
public TouchScreenManager TouchScreenManager { get; }
|
public TouchScreenManager TouchScreenManager { get; }
|
||||||
public Switch Device { get; private set; }
|
public Switch Device;
|
||||||
public IRenderer Renderer { get; private set; }
|
public IRenderer Renderer { get; private set; }
|
||||||
|
|
||||||
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
|
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
|
||||||
|
@ -26,7 +26,6 @@ namespace Ryujinx.Horizon.Sdk.Settings
|
|||||||
"fr-CA",
|
"fr-CA",
|
||||||
"es-419",
|
"es-419",
|
||||||
"zh-Hans",
|
"zh-Hans",
|
||||||
"zh-Hant",
|
|
||||||
"pt-BR"
|
"pt-BR"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ namespace Ryujinx.UI.Common.Helper
|
|||||||
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
||||||
public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
|
public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
|
||||||
{
|
{
|
||||||
culture ??= CultureInfo.CurrentCulture;
|
culture ??= CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
return utcDateTime?.ToLocalTime().ToString(culture);
|
return utcDateTime?.ToLocalTime().ToString(culture);
|
||||||
}
|
}
|
||||||
@ -159,7 +159,7 @@ namespace Ryujinx.UI.Common.Helper
|
|||||||
/// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns>
|
/// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns>
|
||||||
public static DateTime ParseDateTime(string dateTimeString)
|
public static DateTime ParseDateTime(string dateTimeString)
|
||||||
{
|
{
|
||||||
if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime))
|
if (!DateTime.TryParse(dateTimeString, CultureInfo.InvariantCulture, out DateTime parsedDateTime))
|
||||||
{
|
{
|
||||||
// Games that were never played are supposed to appear before the oldest played games in the list,
|
// Games that were never played are supposed to appear before the oldest played games in the list,
|
||||||
// so returning DateTime.UnixEpoch here makes sense.
|
// so returning DateTime.UnixEpoch here makes sense.
|
||||||
|
@ -1154,7 +1154,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
CompareInfo compareInfo = CultureInfo.CurrentCulture.CompareInfo;
|
CompareInfo compareInfo = CultureInfo.InvariantCulture.CompareInfo;
|
||||||
|
|
||||||
return compareInfo.IndexOf(app.Name, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0;
|
return compareInfo.IndexOf(app.Name, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0;
|
||||||
}
|
}
|
||||||
|