crazy
@ -15,11 +15,11 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
static partial class JitCache
|
static partial class JitCache
|
||||||
{
|
{
|
||||||
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
|
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
|
||||||
private static readonly int _pageMask = _pageSize - 1;
|
private static readonly int _pageMask = _pageSize - 2;
|
||||||
|
|
||||||
private const int CodeAlignment = 4; // Bytes.
|
private const int CodeAlignment = 4; // Bytes.
|
||||||
private const int CacheSize = 2047 * 1024 * 1024;
|
private const int CacheSize = 2047 * 1024 * 1024;
|
||||||
private const int CacheSizeIOS = 128 * 1024 * 1024;
|
private const int CacheSizeIOS = 64 * 1024 * 1024;
|
||||||
|
|
||||||
private static ReservedRegion _jitRegion;
|
private static ReservedRegion _jitRegion;
|
||||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||||
@ -91,15 +91,7 @@ namespace ARMeilleure.Translation.Cache
|
|||||||
|
|
||||||
if (OperatingSystem.IsIOS())
|
if (OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
// Marshal.Copy(code, 0, funcPtr, code.Length);
|
Marshal.Copy(code, 0, funcPtr, code.Length);
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
fixed (byte* codePtr = code)
|
|
||||||
{
|
|
||||||
JitSupportDarwinAot.Copy(funcPtr, (IntPtr)codePtr, (ulong)code.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deferProtect)
|
if (deferProtect)
|
||||||
{
|
{
|
||||||
_deferredRxProtect.Enqueue((funcOffset, code.Length));
|
_deferredRxProtect.Enqueue((funcOffset, code.Length));
|
||||||
|
@ -60,6 +60,8 @@ namespace ARMeilleure.Translation
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
return _dispatchLoop.Value;
|
return _dispatchLoop.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,9 +90,6 @@
|
|||||||
5650564D2D2A75B300C8BB1E /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = {
|
5650564D2D2A75B300C8BB1E /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = {
|
||||||
isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;
|
isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;
|
||||||
attributesByRelativePath = {
|
attributesByRelativePath = {
|
||||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
|
|
||||||
CodeSignOnCopy,
|
|
||||||
);
|
|
||||||
"Dependencies/Dynamic Libraries/SoftwareKeyboard.framework" = (
|
"Dependencies/Dynamic Libraries/SoftwareKeyboard.framework" = (
|
||||||
CodeSignOnCopy,
|
CodeSignOnCopy,
|
||||||
RemoveHeadersOnCopy,
|
RemoveHeadersOnCopy,
|
||||||
@ -152,7 +149,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/Dynamic Libraries/SoftwareKeyboard.framework",
|
"Dependencies/Dynamic Libraries/SoftwareKeyboard.framework",
|
||||||
Dependencies/XCFrameworks/libavcodec.xcframework,
|
Dependencies/XCFrameworks/libavcodec.xcframework,
|
||||||
Dependencies/XCFrameworks/libavfilter.xcframework,
|
Dependencies/XCFrameworks/libavfilter.xcframework,
|
||||||
@ -625,16 +621,6 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = fast;
|
GCC_OPTIMIZATION_LEVEL = fast;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -811,12 +797,32 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.0.8;
|
MARKETING_VERSION = 0.0.8;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/Core/Headers/Ryujinx-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
@ -841,16 +847,6 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = fast;
|
GCC_OPTIMIZATION_LEVEL = fast;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -1027,12 +1023,32 @@
|
|||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Core/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.0.8;
|
MARKETING_VERSION = 0.0.8;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/Core/Headers/Ryujinx-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
//
|
|
||||||
// Untitled.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 28/11/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import GameController
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var theWindow: UIWindow? = nil
|
|
||||||
extension UIWindow {
|
|
||||||
@objc func wdb_makeKeyAndVisible() {
|
|
||||||
if #available(iOS 13.0, *) {
|
|
||||||
self.windowScene = (UIApplication.shared.connectedScenes.first! as! UIWindowScene)
|
|
||||||
}
|
|
||||||
self.wdb_makeKeyAndVisible()
|
|
||||||
theWindow = self
|
|
||||||
|
|
||||||
|
|
||||||
if UserDefaults.standard.bool(forKey: "isVirtualController") {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func patchMakeKeyAndVisible() {
|
|
||||||
let uiwindowClass = UIWindow.self
|
|
||||||
if let m1 = class_getInstanceMethod(uiwindowClass, #selector(UIWindow.makeKeyAndVisible)),
|
|
||||||
let m2 = class_getInstanceMethod(uiwindowClass, #selector(UIWindow.wdb_makeKeyAndVisible)) {
|
|
||||||
method_exchangeImplementations(m1, m2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
|||||||
//
|
|
||||||
// FPSMonitor.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 21/12/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class FPSMonitor: ObservableObject {
|
|
||||||
@Published private(set) var currentFPS: UInt64 = 0
|
|
||||||
private var timer: Timer?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
|
|
||||||
self?.updateFPS()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
timer?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateFPS() {
|
|
||||||
let currentfps = UInt64(get_current_fps())
|
|
||||||
|
|
||||||
self.currentFPS = currentfps
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func formatFPS() -> String {
|
|
||||||
let fps = Double(currentFPS)
|
|
||||||
let fpsString = String(format: "FPS: %.2f", fps)
|
|
||||||
|
|
||||||
return fpsString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
|||||||
//
|
|
||||||
// MemoryUsageMonitor.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 21/12/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class MemoryUsageMonitor: ObservableObject {
|
|
||||||
@Published private(set) var memoryUsage: UInt64 = 0
|
|
||||||
private var timer: Timer?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
||||||
self?.updateMemoryUsage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
timer?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateMemoryUsage() {
|
|
||||||
var taskInfo = task_vm_info_data_t()
|
|
||||||
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info>.size) / 4
|
|
||||||
let result: kern_return_t = withUnsafeMutablePointer(to: &taskInfo) {
|
|
||||||
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
|
||||||
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if result == KERN_SUCCESS {
|
|
||||||
memoryUsage = taskInfo.phys_footprint
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
print("Error with task_info(): " +
|
|
||||||
(String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatMemorySize(_ bytes: UInt64) -> String {
|
|
||||||
let formatter = ByteCountFormatter()
|
|
||||||
formatter.allowedUnits = [.useMB, .useGB]
|
|
||||||
formatter.countStyle = .memory
|
|
||||||
return formatter.string(fromByteCount: Int64(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
|||||||
//
|
|
||||||
// Untitled.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 21/12/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct PerformanceOverlayView: View {
|
|
||||||
@StateObject private var memorymonitor = MemoryUsageMonitor()
|
|
||||||
|
|
||||||
@StateObject private var fpsmonitor = FPSMonitor()
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Text("\(fpsmonitor.formatFPS())")
|
|
||||||
Text(memorymonitor.formatMemorySize(memorymonitor.memoryUsage))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
|||||||
//
|
|
||||||
// MTLHUD.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 26/11/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
|
|
||||||
class MTLHud {
|
|
||||||
|
|
||||||
var canMetalHud: Bool {
|
|
||||||
return openMetalDylib()
|
|
||||||
}
|
|
||||||
|
|
||||||
var isEnabled: Bool {
|
|
||||||
if let getenv = getenv("MTL_HUD_ENABLED") {
|
|
||||||
return String(cString: getenv).contains("1")
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
static let shared = MTLHud()
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
openMetalDylib()
|
|
||||||
if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") {
|
|
||||||
enable()
|
|
||||||
} else {
|
|
||||||
disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func openMetalDylib() -> Bool {
|
|
||||||
let path = "/usr/lib/libMTLHud.dylib"
|
|
||||||
|
|
||||||
// Load the dynamic library
|
|
||||||
if dlopen(path, RTLD_NOW) != nil {
|
|
||||||
// Library loaded successfully
|
|
||||||
print("Library loaded from \(path)")
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
// Handle error
|
|
||||||
if let error = String(validatingUTF8: dlerror()) {
|
|
||||||
print("Error loading library: \(error)")
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func enable() {
|
|
||||||
setenv("MTL_HUD_ENABLED", "1", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func disable() {
|
|
||||||
setenv("MTL_HUD_ENABLED", "0", 1)
|
|
||||||
}
|
|
||||||
}
|
|
@ -264,7 +264,7 @@ struct ContentView: View {
|
|||||||
if game.titleName.lowercased() != "super mario odyssey" {
|
if game.titleName.lowercased() != "super mario odyssey" {
|
||||||
setting = (MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"))
|
setting = (MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"))
|
||||||
} else {
|
} else {
|
||||||
setting = (MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "2"))
|
setting = (MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"))
|
||||||
}
|
}
|
||||||
setenv(setting.string, setting.value, 1)
|
setenv(setting.string, setting.value, 1)
|
||||||
|
|
||||||
|
BIN
src/MeloNX/MeloNX/Dependencies/Ryujinx.Headless.SDL2.dylib
Normal file
@ -4,8 +4,6 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -1,309 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 3/11/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
// import SDL2
|
|
||||||
import GameController
|
|
||||||
import Darwin
|
|
||||||
import UIKit
|
|
||||||
import MetalKit
|
|
||||||
// import SDL
|
|
||||||
import SoftwareKeyboard
|
|
||||||
|
|
||||||
struct MoltenVKSettings: Codable, Hashable {
|
|
||||||
let string: String
|
|
||||||
var value: String
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
// MARK: - Properties
|
|
||||||
@State private var theWindow: UIWindow?
|
|
||||||
@State private var game: Game?
|
|
||||||
@State private var controllersList: [Controller] = []
|
|
||||||
@State private var currentControllers: [Controller] = []
|
|
||||||
@State private var config: Ryujinx.Configuration
|
|
||||||
@State var settings: [MoltenVKSettings]
|
|
||||||
@AppStorage("useTrollStore") var useTrollStore: Bool = false
|
|
||||||
@State private var isVirtualControllerActive: Bool = false
|
|
||||||
@AppStorage("isVirtualController") var isVCA: Bool = true
|
|
||||||
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
|
||||||
@AppStorage("JIT") var isJITEnabled: Bool = false
|
|
||||||
@State var isMK8: Bool = false
|
|
||||||
@AppStorage("quit") var quit: Bool = false
|
|
||||||
|
|
||||||
@State var quits: Bool = false
|
|
||||||
@State private var clumpOffset: CGFloat = -100
|
|
||||||
private let clumpWidth: CGFloat = 100
|
|
||||||
private let animationDuration: Double = 1.0
|
|
||||||
@State private var isAnimating = false
|
|
||||||
@State var isLoading = true
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
|
||||||
init() {
|
|
||||||
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
|
||||||
_config = State(initialValue: defaultConfig)
|
|
||||||
|
|
||||||
let defaultSettings: [MoltenVKSettings] = [
|
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
|
|
||||||
MoltenVKSettings(string: "MVK_CONFIG_RESUME_LOST_DEVICE", value: "1")
|
|
||||||
]
|
|
||||||
|
|
||||||
_settings = State(initialValue: defaultSettings)
|
|
||||||
|
|
||||||
print("JIT Enabled: \(isJITEnabled)")
|
|
||||||
|
|
||||||
initializeSDL()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Body
|
|
||||||
var body: some View {
|
|
||||||
if let game, quits == false {
|
|
||||||
if isLoading {
|
|
||||||
emulationView
|
|
||||||
.onAppear() {
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
|
||||||
timer.invalidate()
|
|
||||||
quits = quit
|
|
||||||
|
|
||||||
if quits {
|
|
||||||
quit = false
|
|
||||||
timer.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
VStack {
|
|
||||||
|
|
||||||
}
|
|
||||||
.onAppear() {
|
|
||||||
isAnimating = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mainMenuView
|
|
||||||
.onAppear() {
|
|
||||||
quits = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - View Components
|
|
||||||
private var emulationView: some View {
|
|
||||||
GeometryReader { screenGeometry in
|
|
||||||
ZStack {
|
|
||||||
HStack(spacing: screenGeometry.size.width * 0.04) {
|
|
||||||
if let icon = game?.icon {
|
|
||||||
Image(uiImage: icon)
|
|
||||||
.resizable()
|
|
||||||
.frame(
|
|
||||||
width: min(screenGeometry.size.width * 0.25, 250),
|
|
||||||
height: min(screenGeometry.size.width * 0.25, 250)
|
|
||||||
)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
||||||
.shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: screenGeometry.size.height * 0.015) {
|
|
||||||
Text("Loading \(game?.titleName ?? "Game")")
|
|
||||||
.font(.system(size: min(screenGeometry.size.width * 0.04, 32)))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
|
|
||||||
GeometryReader { geometry in
|
|
||||||
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
|
|
||||||
|
|
||||||
ZStack(alignment: .leading) {
|
|
||||||
// Background track
|
|
||||||
Rectangle()
|
|
||||||
.cornerRadius(10)
|
|
||||||
.frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
|
||||||
.foregroundColor(.gray.opacity(0.3))
|
|
||||||
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
|
|
||||||
|
|
||||||
// Animated loading bar
|
|
||||||
Rectangle()
|
|
||||||
.cornerRadius(10)
|
|
||||||
.frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.shadow(color: .blue.opacity(0.5), radius: 4, x: 0, y: 2)
|
|
||||||
.offset(x: isAnimating ? containerWidth : -clumpWidth)
|
|
||||||
.animation(
|
|
||||||
Animation.linear(duration: 1.0)
|
|
||||||
.repeatForever(autoreverses: false),
|
|
||||||
value: isAnimating
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
||||||
.onAppear {
|
|
||||||
isAnimating = true
|
|
||||||
|
|
||||||
setupEmulation()
|
|
||||||
|
|
||||||
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
|
|
||||||
if get_current_fps() != 0 {
|
|
||||||
isLoading = false
|
|
||||||
isAnimating = false
|
|
||||||
timer.invalidate()
|
|
||||||
}
|
|
||||||
print(get_current_fps())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: min(screenGeometry.size.height * 0.015, 12))
|
|
||||||
.frame(width: min(screenGeometry.size.width * 0.35, 350))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, screenGeometry.size.width * 0.06)
|
|
||||||
.padding(.vertical, screenGeometry.size.height * 0.05)
|
|
||||||
.position(
|
|
||||||
x: screenGeometry.size.width / 2,
|
|
||||||
y: screenGeometry.size.height * 0.5
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mainMenuView: some View {
|
|
||||||
MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
|
|
||||||
.onAppear() {
|
|
||||||
refreshControllersList()
|
|
||||||
|
|
||||||
|
|
||||||
let isJIT = UserDefaults.standard.bool(forKey: "JIT-ENABLED")
|
|
||||||
|
|
||||||
if !isJIT, useTrollStore {
|
|
||||||
askForJIT()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
|
||||||
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO;
|
|
||||||
private func initializeSDL() {
|
|
||||||
setMoltenVKSettings()
|
|
||||||
SDL_SetMainReady()
|
|
||||||
SDL_iPhoneSetEventPump(SDL_TRUE)
|
|
||||||
SDL_Init(SdlInitFlags)
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupEmulation() {
|
|
||||||
patchMakeKeyAndVisible()
|
|
||||||
|
|
||||||
if (currentControllers.first(where: { $0 == onscreencontroller }) != nil) {
|
|
||||||
|
|
||||||
isVCA = true
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
start(displayid: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
|
||||||
isVCA = false
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
start(displayid: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshControllersList() {
|
|
||||||
controllersList = Ryujinx.shared.getConnectedControllers()
|
|
||||||
|
|
||||||
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
|
|
||||||
self.onscreencontroller = onscreen
|
|
||||||
}
|
|
||||||
|
|
||||||
controllersList.removeAll(where: { $0.id == "0"})
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
guard let game else { return }
|
|
||||||
|
|
||||||
config.gamepath = game.fileURL.path
|
|
||||||
config.inputids = Array(Set(currentControllers.map(\.id)))
|
|
||||||
var setting: MoltenVKSettings
|
|
||||||
|
|
||||||
if game.titleName.lowercased() != "super mario odyssey" {
|
|
||||||
setting = (MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"))
|
|
||||||
} else {
|
|
||||||
setting = (MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "2"))
|
|
||||||
}
|
|
||||||
setenv(setting.string, setting.value, 1)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if config.inputids.isEmpty {
|
|
||||||
config.inputids.append("0")
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try Ryujinx.shared.start(with: config)
|
|
||||||
} catch {
|
|
||||||
print("Error: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private func setMoltenVKSettings() {
|
|
||||||
|
|
||||||
settings.forEach { setting in
|
|
||||||
setenv(setting.string, setting.value, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helper Functions
|
|
||||||
func loadSettings() -> Ryujinx.Configuration? {
|
|
||||||
guard let jsonString = UserDefaults.standard.string(forKey: "config"),
|
|
||||||
let data = jsonString.data(using: .utf8) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
return try JSONDecoder().decode(Ryujinx.Configuration.self, from: data)
|
|
||||||
} catch {
|
|
||||||
print("Failed to load settings: \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,304 +0,0 @@
|
|||||||
//
|
|
||||||
// ControllerView.swift
|
|
||||||
// Pomelo-V2
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 16/7/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import GameController
|
|
||||||
import SwiftUIJoystick
|
|
||||||
import CoreMotion
|
|
||||||
|
|
||||||
struct ControllerView: View {
|
|
||||||
|
|
||||||
@AppStorage("performacehud") var performacehud: Bool = false
|
|
||||||
@AppStorage("quit") var quit: Bool = false
|
|
||||||
var body: some View {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
|
|
||||||
VStack {
|
|
||||||
if performacehud {
|
|
||||||
HStack {
|
|
||||||
|
|
||||||
PerformanceOverlayView()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// Button("Stop emulation") {
|
|
||||||
// DispatchQueue.main.async {
|
|
||||||
// stop_emulation()
|
|
||||||
// quit = true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if performacehud {
|
|
||||||
HStack {
|
|
||||||
PerformanceOverlayView()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// Button("Stop emulation") {
|
|
||||||
// DispatchQueue.main.async {
|
|
||||||
// stop_emulation()
|
|
||||||
// quit = true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 8.6 KiB |
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 4.4 KiB |
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 6.4 KiB |
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 54 KiB |
@ -1,53 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,493 +0,0 @@
|
|||||||
//
|
|
||||||
// GameListView.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 3/11/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import UniformTypeIdentifiers
|
|
||||||
|
|
||||||
|
|
||||||
struct GameLibraryView: View {
|
|
||||||
@Binding var startemu: Game?
|
|
||||||
@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
|
|
||||||
@State var firmwareInstaller = false
|
|
||||||
@State var firmwareversion = "0"
|
|
||||||
@State var isImporting: Bool = false
|
|
||||||
@State var startgame = false
|
|
||||||
|
|
||||||
|
|
||||||
var filteredGames: [Game] {
|
|
||||||
if searchText.isEmpty {
|
|
||||||
return games
|
|
||||||
}
|
|
||||||
return games.filter {
|
|
||||||
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
|
|
||||||
$0.developer.localizedCaseInsensitiveContains(searchText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
iOSNav {
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(alignment: .leading, spacing: 20) {
|
|
||||||
if !isSearching {
|
|
||||||
Text("Games")
|
|
||||||
.font(.system(size: 34, weight: .bold))
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.top, 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.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()
|
|
||||||
|
|
||||||
|
|
||||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
|
||||||
firmwareversion = (firmware == "" ? "0" : firmware)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .topBarLeading) {
|
|
||||||
Menu {
|
|
||||||
|
|
||||||
Text("Firmware Version: \(firmwareversion)")
|
|
||||||
.tint(.white)
|
|
||||||
|
|
||||||
if firmwareversion == "0" {
|
|
||||||
Button {
|
|
||||||
firmwareInstaller.toggle()
|
|
||||||
} label: {
|
|
||||||
Text("Install Firmware")
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Button {
|
|
||||||
Ryujinx.shared.removeFirmware()
|
|
||||||
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
|
||||||
firmwareversion = (firmware == "" ? "0" : firmware)
|
|
||||||
} label: {
|
|
||||||
Text("Remove Firmware")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Button {
|
|
||||||
let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
|
|
||||||
|
|
||||||
self.startemu = game
|
|
||||||
} label: {
|
|
||||||
Text("Mii Maker")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
|
|
||||||
isImporting.toggle()
|
|
||||||
} label: {
|
|
||||||
Text("Open game from system")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
||||||
let sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
|
|
||||||
let furl = URL(string: sharedurl)!
|
|
||||||
if UIApplication.shared.canOpenURL(furl) {
|
|
||||||
UIApplication.shared.open(furl, options: [:])
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("Show MeloNX Folder")
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "ellipsis.circle")
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
.searchable(text: $searchText)
|
|
||||||
.onChange(of: searchText) { _ in
|
|
||||||
isSearching = !searchText.isEmpty
|
|
||||||
}
|
|
||||||
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
|
|
||||||
switch result {
|
|
||||||
|
|
||||||
case .success(let url):
|
|
||||||
|
|
||||||
do {
|
|
||||||
|
|
||||||
let fun = url.startAccessingSecurityScopedResource()
|
|
||||||
let path = url.path
|
|
||||||
|
|
||||||
Ryujinx.shared.installFirmware(firmwarePath: path)
|
|
||||||
|
|
||||||
firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion())
|
|
||||||
if fun {
|
|
||||||
url.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let error):
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .data]) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let url):
|
|
||||||
guard url.startAccessingSecurityScopedResource() else {
|
|
||||||
print("Failed to access security-scoped resource")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer { url.stopAccessingSecurityScopedResource() }
|
|
||||||
|
|
||||||
do {
|
|
||||||
let handle = try FileHandle(forReadingFrom: url)
|
|
||||||
let fileExtension = (url.pathExtension as NSString).utf8String
|
|
||||||
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
|
||||||
|
|
||||||
var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
|
||||||
|
|
||||||
var game = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
|
|
||||||
|
|
||||||
game.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
|
|
||||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
|
||||||
String(cString: $0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
game.developer = withUnsafePointer(to: &gameInfo.Developer) {
|
|
||||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
|
||||||
String(cString: $0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
game.titleId = String(gameInfo.TitleId)
|
|
||||||
|
|
||||||
print(String(gameInfo.TitleId))
|
|
||||||
|
|
||||||
|
|
||||||
game.version = String(gameInfo.Version)
|
|
||||||
|
|
||||||
game.icon = game.createImage(from: gameInfo)
|
|
||||||
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
startemu = game
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
case .failure(let err):
|
|
||||||
print("File import failed: \(err.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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() {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
|
||||||
|
|
||||||
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
|
|
||||||
|
|
||||||
// Check if "roms" folder exists; if not, create it
|
|
||||||
if !fileManager.fileExists(atPath: romsDirectory.path) {
|
|
||||||
do {
|
|
||||||
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
} catch {
|
|
||||||
print("Failed to create roms directory: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
games = []
|
|
||||||
// Load games only from "roms" folder
|
|
||||||
do {
|
|
||||||
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
|
|
||||||
|
|
||||||
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 gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
|
||||||
|
|
||||||
var game = Game(containerFolder: romsDirectory, fileType: .item, fileURL: fileURLCandidate, titleName: "", titleId: "", developer: "", version: "")
|
|
||||||
|
|
||||||
game.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
|
|
||||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
|
||||||
String(cString: $0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
game.developer = withUnsafePointer(to: &gameInfo.Developer) {
|
|
||||||
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
|
|
||||||
String(cString: $0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
game.titleId = String(gameInfo.TitleId)
|
|
||||||
|
|
||||||
|
|
||||||
game.version = String(gameInfo.Version)
|
|
||||||
|
|
||||||
game.icon = game.createImage(from: gameInfo)
|
|
||||||
|
|
||||||
|
|
||||||
games.append(game)
|
|
||||||
} catch {
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
print("Error loading games from roms folder: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: Game?
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: {
|
|
||||||
startemu = game
|
|
||||||
}) {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
if let icon = game.icon {
|
|
||||||
Image(uiImage: icon)
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
.frame(width: 140, height: 140)
|
|
||||||
.cornerRadius(12)
|
|
||||||
} else {
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(colorScheme == .dark ?
|
|
||||||
Color(.systemGray5) : Color(.systemGray6))
|
|
||||||
.frame(width: 140, height: 140)
|
|
||||||
|
|
||||||
Image(systemName: "gamecontroller.fill")
|
|
||||||
.font(.system(size: 40))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(game.titleName)
|
|
||||||
.font(.subheadline.bold())
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
Text(game.developer)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GameListRow: View {
|
|
||||||
let game: Game
|
|
||||||
@Binding var startemu: Game?
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: {
|
|
||||||
startemu = game
|
|
||||||
}) {
|
|
||||||
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
|
|
||||||
} label: {
|
|
||||||
Label("Play Now", systemImage: "play.fill")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
// Add info action
|
|
||||||
} label: {
|
|
||||||
Label("Game Info", systemImage: "info.circle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,430 +0,0 @@
|
|||||||
//
|
|
||||||
// SettingsView.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 25/11/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftSVG
|
|
||||||
|
|
||||||
struct SettingsView: View {
|
|
||||||
@Binding var config: Ryujinx.Configuration
|
|
||||||
@Binding var MoltenVKSettings: [MoltenVKSettings]
|
|
||||||
|
|
||||||
@Binding var controllersList: [Controller]
|
|
||||||
@Binding var currentControllers: [Controller]
|
|
||||||
|
|
||||||
@Binding var onscreencontroller: Controller
|
|
||||||
@AppStorage("useTrollStore") var useTrollStore: Bool = false
|
|
||||||
|
|
||||||
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
|
||||||
|
|
||||||
var memoryManagerModes = [
|
|
||||||
("HostMapped", "Host (fast)"),
|
|
||||||
("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
|
|
||||||
("SoftwarePageTable", "Software (slow)"),
|
|
||||||
]
|
|
||||||
|
|
||||||
@AppStorage("RyuDemoControls") var ryuDemo: Bool = false
|
|
||||||
@AppStorage("MTL_HUD_ENABLED") var metalHUDEnabled: Bool = false
|
|
||||||
|
|
||||||
@AppStorage("performacehud") var performacehud: 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 {
|
|
||||||
iOSNav {
|
|
||||||
List {
|
|
||||||
|
|
||||||
|
|
||||||
// Graphics & Performance
|
|
||||||
Section {
|
|
||||||
Toggle(isOn: $config.fullscreen) {
|
|
||||||
labelWithIcon("Fullscreen", iconName: "rectangle.expand.vertical")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
|
|
||||||
Toggle(isOn: $config.disableShaderCache) {
|
|
||||||
labelWithIcon("Shader Cache", iconName: "memorychip")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
|
|
||||||
Toggle(isOn: $config.enableTextureRecompression) {
|
|
||||||
labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
|
|
||||||
Toggle(isOn: $config.disableDockedMode) {
|
|
||||||
labelWithIcon("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: $performacehud) {
|
|
||||||
labelWithIcon("Performance Overlay", iconName: "speedometer")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
} 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.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input Selector
|
|
||||||
Section {
|
|
||||||
if !controllersList.filter({ !currentControllers.contains($0) }).isEmpty {
|
|
||||||
DisclosureGroup("Unselected Controllers") {
|
|
||||||
ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in
|
|
||||||
var customBinding: Binding<Bool> {
|
|
||||||
Binding(
|
|
||||||
get: { currentControllers.contains(controller) },
|
|
||||||
set: { bool in
|
|
||||||
if !bool {
|
|
||||||
currentControllers.removeAll(where: { $0.id == controller.id })
|
|
||||||
} else {
|
|
||||||
currentControllers.append(controller)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Toggle(isOn: customBinding) {
|
|
||||||
Text(controller.name)
|
|
||||||
.font(.body)
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ForEach(controllersList) { controller in
|
|
||||||
|
|
||||||
var customBinding: Binding<Bool> {
|
|
||||||
Binding(
|
|
||||||
get: { currentControllers.contains(controller) },
|
|
||||||
set: { bool in
|
|
||||||
if !bool {
|
|
||||||
currentControllers.removeAll(where: { $0.id == controller.id })
|
|
||||||
} else {
|
|
||||||
currentControllers.append(controller)
|
|
||||||
}
|
|
||||||
// toggleController(controller)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if customBinding.wrappedValue {
|
|
||||||
DisclosureGroup {
|
|
||||||
Toggle(isOn: customBinding) {
|
|
||||||
Text(controller.name)
|
|
||||||
.font(.body)
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
} label: {
|
|
||||||
let controller = String((controllersList.firstIndex(where: { $0.id == controller.id }) ?? 0) + 1)
|
|
||||||
|
|
||||||
|
|
||||||
Text("Player \(controller)")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text("Input Selector")
|
|
||||||
.font(.title3.weight(.semibold))
|
|
||||||
.textCase(nil)
|
|
||||||
.headerProminence(.increased)
|
|
||||||
} footer: {
|
|
||||||
Text("Select input devices and on-screen controls to play with. ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input Settings
|
|
||||||
Section {
|
|
||||||
|
|
||||||
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.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
labelWithIcon("Memory Manager Mode", iconName: "gearshape")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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.")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Other Settings
|
|
||||||
Section {
|
|
||||||
|
|
||||||
Toggle(isOn: $useTrollStore) {
|
|
||||||
labelWithIcon("TrollStore", iconName: "troll.svg")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
|
|
||||||
Toggle(isOn: $config.debuglogs) {
|
|
||||||
labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
|
|
||||||
Toggle(isOn: $config.tracelogs) {
|
|
||||||
labelWithIcon("Trace Logs", iconName: "waveform.path")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
} header: {
|
|
||||||
Text("Miscellaneous Options")
|
|
||||||
.font(.title3.weight(.semibold))
|
|
||||||
.textCase(nil)
|
|
||||||
.headerProminence(.increased)
|
|
||||||
} footer: {
|
|
||||||
Text("Enable logs for troubleshooting and Enable automatic TrollStore JIT.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advanced
|
|
||||||
Section {
|
|
||||||
DisclosureGroup {
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
labelWithIcon("Page Size", iconName: "textformat.size")
|
|
||||||
Spacer()
|
|
||||||
Text("\(String(Int(getpagesize())))")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
Button {
|
|
||||||
Ryujinx.shared.removeFirmware()
|
|
||||||
|
|
||||||
} label: {
|
|
||||||
Text("Remove Firmware")
|
|
||||||
.font(.body)
|
|
||||||
}
|
|
||||||
} 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)")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
|
|
||||||
.navigationTitle("Settings")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.listStyle(.insetGrouped)
|
|
||||||
.onAppear {
|
|
||||||
if let configs = loadSettings() {
|
|
||||||
self.config = configs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: config) { _ in
|
|
||||||
saveSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationViewStyle(.stack)
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
#if targetEnvironment(simulator)
|
|
||||||
|
|
||||||
print("Saving Settings")
|
|
||||||
#else
|
|
||||||
do {
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
encoder.outputFormatting = .prettyPrinted
|
|
||||||
let data = try encoder.encode(config)
|
|
||||||
let jsonString = String(data: data, encoding: .utf8)
|
|
||||||
UserDefaults.standard.set(jsonString, forKey: "config")
|
|
||||||
} catch {
|
|
||||||
print("Failed to save settings: \(error)")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// Original loadSettings function assumed to exist
|
|
||||||
func loadSettings() -> Ryujinx.Configuration? {
|
|
||||||
|
|
||||||
#if targetEnvironment(simulator)
|
|
||||||
print("Running on Simulator")
|
|
||||||
|
|
||||||
return Ryujinx.Configuration(gamepath: "")
|
|
||||||
#else
|
|
||||||
guard let jsonString = UserDefaults.standard.string(forKey: "config"),
|
|
||||||
let data = jsonString.data(using: .utf8) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
let configs = try decoder.decode(Ryujinx.Configuration.self, from: data)
|
|
||||||
return configs
|
|
||||||
} catch {
|
|
||||||
print("Failed to load settings: \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
if iconName.hasSuffix(".svg"){
|
|
||||||
if let flipimage, flipimage {
|
|
||||||
SVGView(svgName: iconName, color: .blue)
|
|
||||||
.symbolRenderingMode(.hierarchical)
|
|
||||||
.frame(width: 20, height: 20)
|
|
||||||
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
|
||||||
} else {
|
|
||||||
SVGView(svgName: iconName, color: .blue)
|
|
||||||
.symbolRenderingMode(.hierarchical)
|
|
||||||
.frame(width: 20, height: 20)
|
|
||||||
}
|
|
||||||
} else if !iconName.isEmpty {
|
|
||||||
Image(systemName: iconName)
|
|
||||||
.symbolRenderingMode(.hierarchical)
|
|
||||||
.foregroundStyle(.blue)
|
|
||||||
}
|
|
||||||
Text(text)
|
|
||||||
}
|
|
||||||
.font(.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct SVGView: UIViewRepresentable {
|
|
||||||
var svgName: String
|
|
||||||
var color: Color = Color.black
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIView {
|
|
||||||
var svgName = svgName
|
|
||||||
var hammock = UIView()
|
|
||||||
|
|
||||||
if svgName.hasSuffix(".svg") {
|
|
||||||
svgName.removeLast(4)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let svgLayer = UIView(SVGNamed: svgName) { svgLayer in
|
|
||||||
svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color
|
|
||||||
svgLayer.resizeToFit(hammock.frame)
|
|
||||||
hammock.layer.addSublayer(svgLayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hammock
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIView, context: Context) {
|
|
||||||
// Update the SVG view's fill color when the color changes
|
|
||||||
if let svgLayer = uiView.layer.sublayers?.first as? CAShapeLayer {
|
|
||||||
svgLayer.fillColor = UIColor(color).cgColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
//
|
|
||||||
// TabView.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 10/12/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import UniformTypeIdentifiers
|
|
||||||
|
|
||||||
|
|
||||||
struct MainTabView: View {
|
|
||||||
@Binding var startemu: Game?
|
|
||||||
@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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -66,6 +66,8 @@ namespace Ryujinx.Cpu.LightningJit
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
return _dispatchLoop.Value;
|
return _dispatchLoop.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|