diff --git a/distribution/ios/get_dotnet.sh b/distribution/ios/get_dotnet.sh index a600eb398..f69969682 100755 --- a/distribution/ios/get_dotnet.sh +++ b/distribution/ios/get_dotnet.sh @@ -41,4 +41,6 @@ ESCAPED_PATH=$(echo "$DOTNET_PATH" | sed 's/\//\\\//g') # Update the xcconfig file sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE" +$DOTNET_PATH clean + echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH" diff --git a/src/MeloNX/MeloNX.xcconfig b/src/MeloNX/MeloNX.xcconfig index 33fdc27ba..9b97df59a 100644 --- a/src/MeloNX/MeloNX.xcconfig +++ b/src/MeloNX/MeloNX.xcconfig @@ -8,6 +8,6 @@ // Configuration settings file format documentation can be found at: // https://help.apple.com/xcode/#/dev745c5c974 -VERSION = 1.6.0 +VERSION = 1.7.0 DOTNET = /usr/local/share/dotnet/dotnet diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index e4c0aaef4..e4099b69e 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -116,7 +116,7 @@ "Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = ( CodeSignOnCopy, ); - "Dependencies/Dynamic Libraries/RyujinxKeyboard.framework" = ( + "Dependencies/Dynamic Libraries/RyujinxHelper.framework" = ( CodeSignOnCopy, RemoveHeadersOnCopy, ); @@ -177,7 +177,7 @@ "Dependencies/Dynamic Libraries/libavutil.dylib", "Dependencies/Dynamic Libraries/libMoltenVK.dylib", "Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib", - "Dependencies/Dynamic Libraries/RyujinxKeyboard.framework", + "Dependencies/Dynamic Libraries/RyujinxHelper.framework", Dependencies/XCFrameworks/libavcodec.xcframework, Dependencies/XCFrameworks/libavfilter.xcframework, Dependencies/XCFrameworks/libavformat.xcframework, @@ -484,6 +484,7 @@ /* Begin PBXTargetDependency section */ 4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = { isa = PBXTargetDependency; + platformFilter = ios; target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */; targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */; }; @@ -712,6 +713,14 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; @@ -842,10 +851,24 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = "$(VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h"; SWIFT_VERSION = 5.0; @@ -924,6 +947,14 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; @@ -1054,10 +1085,24 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = "$(VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h"; SWIFT_VERSION = 5.0; diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate index d32f7b21f..264afbacc 100644 Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme index ca7ef5201..131fd924a 100644 --- a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme +++ b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme @@ -64,7 +64,10 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" enableGPUValidationMode = "1" - allowLocationSimulation = "YES"> + allowLocationSimulation = "YES" + viewDebuggingEnabled = "No" + consoleMode = "0" + structuredConsoleMode = "2"> Void) { + let startTime = Date() + let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background)) + + timer.schedule(deadline: .now(), repeating: interval) + + timer.setEventHandler { + pingSite { connected in + if connected { + timer.cancel() + DispatchQueue.main.async { + completion(true) + } + } else if Date().timeIntervalSince(startTime) > timeout { + timer.cancel() + DispatchQueue.main.async { + completion(false) + } + } + } + } + + timer.resume() +} + +func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping (Bool) -> Void) { + guard let url = URL(string: host) else { + completion(false) + return + } + + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = 2.0 + config.timeoutIntervalForResource = 2.0 + + let session = URLSession(configuration: config) + + var request = URLRequest(url: url) + request.httpMethod = "GET" + + let task = session.dataTask(with: request) { _, response, error in + if let error = error { + print("Ping failed: \(error.localizedDescription)") + completion(false) + } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { + completion(true) + } else { + let httpResponse = response as? HTTPURLResponse + completion(false) + } + } + + task.resume() +} + + +func presentAlert(title: String, message: String, completion: (() -> Void)? = nil) { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let lastWindow = windowScene.windows.last { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in + completion?() + }) + + DispatchQueue.main.async { + lastWindow.rootViewController?.present(alert, animated: true) + } + } +} + struct LaunchApp: Codable { - let ok: Bool - let error: String? - let launching: Bool - let position: Int? - let mounting: Bool + let success: Bool + let message: String } func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) { @@ -48,28 +129,23 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) { var message = "" - if let error = result.error { - message = "Error: \(error)" - } else if result.mounting { - message = "App is mounting..." - } else if result.launching { - message = "App is launching..." + if !result.success { + message += "\n\(result.message)" + + + let alert = UIAlertController(title: "JIT Error", message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + + DispatchQueue.main.async { + viewController.present(alert, animated: true) + } } else { - message = "App launch status unknown." - } - - if let position = result.position { - message += "\nPosition: \(position)" - } - - let alert = UIAlertController(title: "Launch Status", message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default)) - - DispatchQueue.main.async { - viewController.present(alert, animated: true) + print("Hopefully JIT is enabled now...") + Ryujinx.shared.ryuIsJITEnabled() } } catch { + print(String(data: jsonData, encoding: .utf8)) let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift index de212adb0..743b843e4 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift @@ -189,6 +189,24 @@ enum VirtualControllerButton: Int { case dPadRight case leftTrigger case rightTrigger + + var isTrigger: Bool { + switch self { + case .leftTrigger, .rightTrigger, .leftShoulder, .rightShoulder: + return true + default: + return false + } + } + + var isSmall: Bool { + switch self { + case .back, .start, .guide: + return true + default: + return false + } + } } enum ThumbstickType: Int { diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift index 8742486f6..cd6819eb9 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift @@ -31,7 +31,7 @@ struct iOSNav: View { } -class Ryujinx { +class Ryujinx : ObservableObject { private var isRunning = false let virtualController = VirtualController() @@ -45,6 +45,10 @@ class Ryujinx { @Published var defMLContentSize: CGFloat? + var thread: Thread! + + @Published var jitenabled = false + var shouldMetal: Bool { metalLayer == nil } @@ -145,7 +149,7 @@ class Ryujinx { self.config = config - RunLoop.current.perform { [self] in + thread = Thread { [self] in isRunning = true @@ -178,6 +182,10 @@ class Ryujinx { Self.log("Emulation failed to start: \(error)") } } + + thread.qualityOfService = .background + thread.name = "MeloNX" + thread.start() } @@ -192,6 +200,7 @@ class Ryujinx { self.metalLayer = nil stop_emulation() + thread.cancel() } var running: Bool { @@ -500,6 +509,11 @@ class Ryujinx { static func log(_ message: String) { print("[Ryujinx] \(message)") } + + func ryuIsJITEnabled() { + jitenabled = isJITEnabled() + print("JIT \(jitenabled)") + } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift b/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift index db6fdef89..34fcb32c1 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift @@ -53,7 +53,7 @@ struct ContentView: View { private let animationDuration: Double = 1.0 @State private var isAnimating = false @State var isLoading = true - @State var jitNotEnabled = false + @StateObject var ryujinx = Ryujinx.shared // MARK: - SDL var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO @@ -79,14 +79,16 @@ struct ContentView: View { _settings = State(initialValue: defaultSettings) + print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue) + initializeSDL() } // MARK: - Body var body: some View { - if game != nil && (!jitNotEnabled || ignoreJIT) { + if game != nil && (ryujinx.jitenabled || ignoreJIT) { gameView - } else if game != nil && jitNotEnabled { + } else if game != nil && !ryujinx.jitenabled { jitErrorView } else { mainMenuView @@ -117,9 +119,16 @@ struct ContentView: View { private var jitErrorView: some View { Text("") - .sheet(isPresented: $jitNotEnabled) { + .sheet(isPresented:Binding( + get: { !ryujinx.jitenabled }, + set: { newValue in + ryujinx.jitenabled = newValue + + ryujinx.ryuIsJITEnabled() + }) + ) { JITPopover() { - jitNotEnabled = false + ryujinx.jitenabled = false } .interactiveDismissDisabled() } @@ -308,9 +317,9 @@ struct ContentView: View { } private func refreshControllersList() { - controllersList = Ryujinx.shared.getConnectedControllers() + controllersList = ryujinx.getConnectedControllers() - if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) { + if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) { self.onscreencontroller = onscreen } @@ -343,7 +352,7 @@ struct ContentView: View { } do { - try Ryujinx.shared.start(with: config) + try ryujinx.start(with: config) } catch { print("Error: \(error.localizedDescription)") } @@ -351,7 +360,8 @@ struct ContentView: View { private func configureEnvironmentVariables() { if mVKPreFillBuffer { - setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1) + mVKPreFillBuffer = false + // setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1) } if syncqsubmits { @@ -366,8 +376,8 @@ struct ContentView: View { } private func checkJitStatus() { - jitNotEnabled = !isJITEnabled() - if jitNotEnabled { + ryujinx.ryuIsJITEnabled() + if !ryujinx.jitenabled { if useTrollStore { askForJIT() } else if jitStreamerEB { @@ -382,9 +392,9 @@ struct ContentView: View { if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), components.host == "game" { if let text = components.queryItems?.first(where: { $0.name == "id" })?.value { - game = Ryujinx.shared.games.first(where: { $0.titleId == text }) + game = ryujinx.games.first(where: { $0.titleId == text }) } else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value { - game = Ryujinx.shared.games.first(where: { $0.titleName == text }) + game = ryujinx.games.first(where: { $0.titleName == text }) } } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift index da922f31d..077a3aac1 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift @@ -11,101 +11,155 @@ import SwiftUIJoystick import CoreMotion struct ControllerView: View { + // MARK: - Properties + @AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0 + @AppStorage("stick-button") private var stickButton = false + @State private var isPortrait = true + @Environment(\.verticalSizeClass) var verticalSizeClass + + + // MARK: - Body 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() - } - } - Spacer() - VStack { - ShoulderButtonsViewRight() - ZStack { - Joystick(iscool: true) // hope this works - ABXYView() - } - } + Group { + let isPad = UIDevice.current.userInterfaceIdiom == .pad + + if isPortrait && !isPad { + portraitLayout + } else { + landscapeLayout + } + } + .padding() + .onChange(of: verticalSizeClass) { _ in + updateOrientation() + } + .onAppear(perform: updateOrientation) + } + + // MARK: - Layouts + private var portraitLayout: some View { + VStack { + Spacer() + VStack(spacing: 20) { + HStack(spacing: 30) { + VStack(spacing: 15) { + ShoulderButtonsViewLeft() + ZStack { + Joystick() + DPadView() } - - HStack { - ButtonView(button: .start) // Adding the + button - .padding(.horizontal, 40) - ButtonView(button: .back) // Adding the - button - .padding(.horizontal, 40) + } + + VStack(spacing: 15) { + ShoulderButtonsViewRight() + ZStack { + Joystick(iscool: true) + ABXYView() } } } - } 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() - } - } - } - + HStack(spacing: 60) { + HStack { + ButtonView(button: .leftStick) + .padding() + ButtonView(button: .start) + } + + HStack { + ButtonView(button: .back) + ButtonView(button: .rightStick) + .padding() } - // .padding(.bottom, geometry.size.height / 11) // also extremally broken ( } } } - .padding() + } + + private var landscapeLayout: some View { + VStack { + Spacer() + + HStack { + VStack(spacing: 15) { + ShoulderButtonsViewLeft() + ZStack { + Joystick() + DPadView() + } + } + + Spacer() + + centerButtons + + Spacer() + + VStack(spacing: 15) { + ShoulderButtonsViewRight() + ZStack { + Joystick(iscool: true) + ABXYView() + } + } + } + } + } + + private var centerButtons: some View { + Group { + if stickButton { + VStack { + HStack(spacing: 50) { + ButtonView(button: .leftStick) + .padding() + Spacer() + ButtonView(button: .rightStick) + .padding() + } + .padding(.top, 30) + + HStack(spacing: 50) { + ButtonView(button: .back) + Spacer() + ButtonView(button: .start) + } + } + .padding(.bottom, 20) + } else { + HStack(spacing: 50) { + ButtonView(button: .back) + Spacer() + ButtonView(button: .start) + } + .padding(.bottom, 20) + } + } + } + + // MARK: - Methods + + private func updateOrientation() { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + isPortrait = window.bounds.size.height > window.bounds.size.width + } } } + struct ShoulderButtonsViewLeft: View { - @State var width: CGFloat = 160 - @State var height: CGFloat = 20 + @State private var width: CGFloat = 160 + @State private var height: CGFloat = 20 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 var body: some View { - HStack { + HStack(spacing: 20) { ButtonView(button: .leftTrigger) - .padding(.horizontal) ButtonView(button: .leftShoulder) - .padding(.horizontal) } .frame(width: width, height: height) - .onAppear() { + .onAppear { if UIDevice.current.systemName.contains("iPadOS") { width *= 1.2 height *= 1.2 @@ -118,19 +172,17 @@ struct ShoulderButtonsViewLeft: View { } struct ShoulderButtonsViewRight: View { - @State var width: CGFloat = 160 - @State var height: CGFloat = 20 + @State private var width: CGFloat = 160 + @State private var height: CGFloat = 20 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 var body: some View { - HStack { + HStack(spacing: 20) { ButtonView(button: .rightShoulder) - .padding(.horizontal) ButtonView(button: .rightTrigger) - .padding(.horizontal) } .frame(width: width, height: height) - .onAppear() { + .onAppear { if UIDevice.current.systemName.contains("iPadOS") { width *= 1.2 height *= 1.2 @@ -143,21 +195,21 @@ struct ShoulderButtonsViewRight: View { } struct DPadView: View { - @State var size: CGFloat = 145 + @State private var size: CGFloat = 145 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 + var body: some View { - VStack { + VStack(spacing: 5) { ButtonView(button: .dPadUp) - HStack { + HStack(spacing: 20) { ButtonView(button: .dPadLeft) Spacer(minLength: 20) ButtonView(button: .dPadRight) } ButtonView(button: .dPadDown) - .padding(.horizontal) } .frame(width: size, height: size) - .onAppear() { + .onAppear { if UIDevice.current.systemName.contains("iPadOS") { size *= 1.2 } @@ -168,22 +220,21 @@ struct DPadView: View { } struct ABXYView: View { - @State var size: CGFloat = 145 + @State private var size: CGFloat = 145 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 var body: some View { - VStack { + VStack(spacing: 5) { ButtonView(button: .X) - HStack { + HStack(spacing: 20) { ButtonView(button: .Y) Spacer(minLength: 20) ButtonView(button: .A) } ButtonView(button: .B) - .padding(.horizontal) } .frame(width: size, height: size) - .onAppear() { + .onAppear { if UIDevice.current.systemName.contains("iPadOS") { size *= 1.2 } @@ -195,58 +246,90 @@ struct ABXYView: View { struct ButtonView: View { var button: VirtualControllerButton - @State var width: CGFloat = 45 - @State var height: CGFloat = 45 - @State var isPressed = false + @State private var width: CGFloat = 45 + @State private var height: CGFloat = 45 + @State private var isPressed = false @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false - @Environment(\.colorScheme) var colorScheme @Environment(\.presentationMode) var presentationMode @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 - - + @State private var debounceTimer: Timer? var body: some View { Image(systemName: buttonText) .resizable() + .scaledToFit() .frame(width: width, height: height) - .foregroundColor(colorScheme == .dark ? Color.gray : Color.gray) - .opacity(isPressed ? 0.4 : 0.7) + .foregroundColor(true ? Color.white.opacity(0.9) : Color.black.opacity(0.9)) + .background( + Group { + if !button.isTrigger { + Circle() + .fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3)) + .frame(width: width * 1.25, height: height * 1.25) + } else { + Image(systemName: buttonText) + .resizable() + .scaledToFit() + .frame(width: width * 1.25, height: height * 1.25) + .foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3)) + } + } + ) + .opacity(isPressed ? 0.6 : 1.0) .gesture( DragGesture(minimumDistance: 0) .onChanged { _ in - if !self.isPressed { - self.isPressed = true - Ryujinx.shared.virtualController.setButtonState(1, for: button) - Haptics.shared.play(.heavy) - } + handleButtonPress() } .onEnded { _ in - self.isPressed = false - Ryujinx.shared.virtualController.setButtonState(0, for: button) + handleButtonRelease() } - ) - .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 - } - - width *= CGFloat(controllerScale) - height *= CGFloat(controllerScale) + ) + .onAppear { + configureSizeForButton() } } - + private func handleButtonPress() { + if !isPressed { + isPressed = true + + debounceTimer?.invalidate() + + Ryujinx.shared.virtualController.setButtonState(1, for: button) + + Haptics.shared.play(.medium) + } + } + + private func handleButtonRelease() { + if isPressed { + isPressed = false + + debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { _ in + Ryujinx.shared.virtualController.setButtonState(0, for: button) + } + } + } + + private func configureSizeForButton() { + if button.isTrigger { + width = 70 + height = 40 + } else if button.isSmall { + width = 35 + height = 35 + } + + // Adjust for iPad + if UIDevice.current.systemName.contains("iPadOS") { + width *= 1.2 + height *= 1.2 + } + + width *= CGFloat(controllerScale) + height *= CGFloat(controllerScale) + } private var buttonText: String { switch button { @@ -258,6 +341,10 @@ struct ButtonView: View { return "x.circle.fill" case .Y: return "y.circle.fill" + case .leftStick: + return "l.joystick.press.down.fill" + case .rightStick: + return "r.joystick.press.down.fill" case .dPadUp: return "arrowtriangle.up.circle.fill" case .dPadDown: @@ -267,7 +354,7 @@ struct ButtonView: View { case .dPadRight: return "arrowtriangle.right.circle.fill" case .leftTrigger: - return"zl.rectangle.roundedtop.fill" + return "zl.rectangle.roundedtop.fill" case .rightTrigger: return "zr.rectangle.roundedtop.fill" case .leftShoulder: @@ -275,16 +362,11 @@ struct ButtonView: View { case .rightShoulder: return "r.rectangle.roundedbottom.fill" case .start: - return "plus.circle.fill" // System symbol for + + return "plus.circle.fill" case .back: - return "minus.circle.fill" // System symbol for - + return "minus.circle.fill" case .guide: return "house.circle.fill" - // This should be all the cases - default: - return "" } } } - - diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift index 7747719c2..3bc838b97 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift @@ -11,7 +11,7 @@ import SwiftUIJoystick public struct Joystick: View { @State var iscool: Bool? = nil - + @Environment(\.colorScheme) var colorScheme @ObservedObject public var joystickMonitor = JoystickMonitor() @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 var dragDiameter: CGFloat { @@ -36,8 +36,13 @@ public struct Joystick: View { .hidden() }, foreground: { - Circle().fill(Color.gray) - .opacity(0.7) + Circle() + .fill(colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.7)) + .background( + Circle() + .fill(colorScheme == .dark ? Color.gray.opacity(0.3) : Color.gray.opacity(0.2)) + .frame(width: (dragDiameter / 4) * 1.2, height: (dragDiameter / 4) * 1.2) + ) }, locksInPlace: false) .onChange(of: self.joystickMonitor.xyPoint) { newValue in diff --git a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift index be8cbb270..6670b20f9 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift @@ -29,6 +29,7 @@ struct GameLibraryView: View { @State var isViewingGameInfo: Bool = false @State var isSelectingGameUpdate: Bool = false @State var isSelectingGameDLC: Bool = false + @StateObject var ryujinx = Ryujinx.shared @State var gameInfo: Game? var games: Binding<[Game]> { Binding( @@ -203,6 +204,13 @@ struct GameLibraryView: View { .foregroundColor(.blue) } } + + ToolbarItem(placement: .topBarLeading) { + if ryujinx.jitenabled { + Image(systemName: "checkmark") + .foregroundStyle(.green) + } + } } .onChange(of: startemu) { game in guard let game else { return } diff --git a/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift b/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift index 8c612b624..b5e219d9d 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift @@ -37,6 +37,8 @@ struct JITPopover: View { if isJIT { dismiss() onJITEnabled() + + Ryujinx.shared.ryuIsJITEnabled() } } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift b/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift index 8e9b631ad..d7951c1ee 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift @@ -96,13 +96,14 @@ struct LogFileView: View { private func startLogFileWatching() { showingLogs = true - self.readLatestLogFile() + Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in if showingLogs { self.readLatestLogFile() } if isfps { + sleep(1) if get_current_fps() != 0 { stopLogFileWatching() timer.invalidate() diff --git a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift index 1127122c9..c6a789233 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift @@ -48,11 +48,15 @@ struct SettingsView: View { @AppStorage("showlogsgame") var showlogsgame: Bool = false + @AppStorage("stick-button") var stickButton = false + @AppStorage("waitForVPN") var waitForVPN = false + @State private var showResolutionInfo = false @State private var showAnisotropicInfo = false @State private var showControllerInfo = false @State private var searchText = "" @AppStorage("portal") var gamepo = false + @StateObject var ryujinx = Ryujinx.shared var filteredMemoryModes: [(String, String)] { guard !searchText.isEmpty else { return memoryManagerModes } @@ -286,6 +290,11 @@ struct SettingsView: View { }.tint(.blue) + Toggle(isOn: $stickButton) { + labelWithIcon("Show Stick Buttons", iconName: "l.joystick.press.down") + }.tint(.blue) + + Toggle(isOn: $ryuDemo) { labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw") } @@ -451,10 +460,22 @@ struct SettingsView: View { } .tint(.blue) .contextMenu { + Button { + waitForVPN.toggle() + } label: { + Label { + Text("Wait for VPN") + } icon: { + if waitForVPN { + Image(systemName: "checkmark") + } + } + + } Button { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let mainWindow = windowScene.windows.last { - let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best iOS developers of all time jkcoxson <3", preferredStyle: .alert) + let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best, most kind, helpful and nice developers of all time jkcoxson <3", preferredStyle: .alert) let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!) @@ -539,7 +560,11 @@ struct SettingsView: View { model.hasPrefix("iPhone") ? "iphone" : "macwindow" - labelWithIcon("JIT Acquisition: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill") + labelWithIcon("JIT Acquisition: \(ryujinx.jitenabled ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill") + .onAppear() { + print("JIY ;(((((") + ryujinx.ryuIsJITEnabled() + } labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip") @@ -566,10 +591,6 @@ struct SettingsView: View { Section { DisclosureGroup { - Toggle(isOn: $mVKPreFillBuffer) { - labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape") - }.tint(.blue) - Toggle(isOn: $config.dfsIntegrityChecks) { labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield") }.tint(.blue) @@ -637,6 +658,8 @@ struct SettingsView: View { .navigationBarTitleDisplayMode(.inline) .listStyle(.insetGrouped) .onAppear { + mVKPreFillBuffer = false + if let configs = loadSettings() { self.config = configs } else { diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift index 1d55a5dcf..653e7c9a6 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift @@ -8,6 +8,7 @@ import SwiftUI import UniformTypeIdentifiers +// MARK: - Models struct DownloadableContentNca: Codable, Hashable { var fullPath: String var titleId: UInt @@ -20,9 +21,18 @@ struct DownloadableContentNca: Codable, Hashable { } } -struct DownloadableContentContainer: Codable, Hashable { +struct DownloadableContentContainer: Codable, Hashable, Identifiable { + var id: String { containerPath } var containerPath: String var downloadableContentNcaList: [DownloadableContentNca] + + var filename: String { + (containerPath as NSString).lastPathComponent + } + + var isEnabled: Bool { + downloadableContentNcaList.first?.enabled == true + } enum CodingKeys: String, CodingKey { case containerPath = "path" @@ -30,136 +40,265 @@ struct DownloadableContentContainer: Codable, Hashable { } } +// MARK: - View struct DLCManagerSheet: View { + // MARK: - Properties @Binding var game: Game! @State private var isSelectingGameDLC = false @State private var dlcs: [DownloadableContentContainer] = [] - + @Environment(\.dismiss) private var dismiss + + // MARK: - Body var body: some View { - NavigationView { - let withIndex = dlcs.enumerated().map { $0 } - List(withIndex, id: \.element.containerPath) { index, dlc in - Button(action: { - let toggle = dlcs[index].downloadableContentNcaList.first?.enabled ?? true - dlcs[index].downloadableContentNcaList.mutableForEach { $0.enabled = !toggle } - Self.saveDlcs(game, dlc: dlcs) - }) { - HStack { - Text((dlc.containerPath as NSString).lastPathComponent) - .foregroundStyle(Color(uiColor: .label)) - Spacer() - if dlc.downloadableContentNcaList.first?.enabled == true { - Image(systemName: "checkmark.circle.fill") - .foregroundStyle(Color.accentColor) - .font(.system(size: 24)) - } else { - Image(systemName: "circle") - .foregroundStyle(Color(uiColor: .secondaryLabel)) - .font(.system(size: 24)) - } - } - } - .contextMenu { - Button { - let path = URL.documentsDirectory.appendingPathComponent(dlc.containerPath) - try? FileManager.default.removeItem(atPath: path.path) - dlcs.remove(at: index) - Self.saveDlcs(game, dlc: dlcs) - } label: { - Text("Remove DLC") + iOSNav { + List { + if dlcs.isEmpty { + emptyStateView + } else { + ForEach(dlcs) { dlc in + dlcRow(dlc) } + .onDelete(perform: removeDLCs) } } .navigationTitle("\(game.titleName) DLCs") .navigationBarTitleDisplayMode(.inline) .toolbar { - Button("Add", systemImage: "plus") { - isSelectingGameDLC = true + ToolbarItem(placement: .navigationBarLeading) { + Button("Done") { + dismiss() + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button { + isSelectingGameDLC = true + } label: { + Label("Add DLC", systemImage: "plus") + } } } + .onAppear { + loadData() + } } - .onAppear { - dlcs = Self.loadDlc(game) - } - .fileImporter(isPresented: $isSelectingGameDLC, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in - switch result { - case .success(let urls): - for url in urls { - guard url.startAccessingSecurityScopedResource() else { - print("Failed to access security-scoped resource") - return - } - defer { url.stopAccessingSecurityScopedResource() } - - do { - let fileManager = FileManager.default - let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - let dlcDirectory = documentsDirectory.appendingPathComponent("dlc") - let romDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId) - - if !fileManager.fileExists(atPath: dlcDirectory.path) { - try fileManager.createDirectory(at: dlcDirectory, withIntermediateDirectories: true, attributes: nil) - } - - if !fileManager.fileExists(atPath: romDlcDirectory.path) { - try fileManager.createDirectory(at: romDlcDirectory, withIntermediateDirectories: true, attributes: nil) - } - - let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: url.path) - guard !dlcContent.isEmpty else { return } - - let destinationURL = romDlcDirectory.appendingPathComponent(url.lastPathComponent) - try? fileManager.copyItem(at: url, to: destinationURL) - - let container = DownloadableContentContainer( - containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL), - downloadableContentNcaList: dlcContent - ) - dlcs.append(container) - - Self.saveDlcs(game, dlc: dlcs) - } catch { - print("Error copying game file: \(error)") - } + .fileImporter( + isPresented: $isSelectingGameDLC, + allowedContentTypes: [.item], + allowsMultipleSelection: true, + onCompletion: handleFileImport + ) + } + + // MARK: - Views + private var emptyStateView: some View { + Group { + if #available(iOS 17, *) { + ContentUnavailableView( + "No DLCs Found", + systemImage: "puzzlepiece.extension", + description: Text("Tap the + button to add game DLCs.") + ) + } else { + VStack(spacing: 20) { + Spacer() + + Image(systemName: "puzzlepiece.extension") + .font(.system(size: 64)) + .foregroundColor(.secondary) + + Text("No DLCs Found") + .font(.title2) + .fontWeight(.semibold) + + Text("Tap the + button to add game DLCs.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) + + Spacer() } - case .failure(let err): - print("File import failed: \(err.localizedDescription)") + .frame(maxWidth: .infinity) + .listRowInsets(EdgeInsets()) } } } + + + private func dlcRow(_ dlc: DownloadableContentContainer) -> some View { + Button { + toggleDLC(dlc) + } label: { + HStack { + Text(dlc.filename) + .foregroundStyle(.primary) + Spacer() + Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle") + .foregroundStyle(dlc.isEnabled ? .primary : .secondary) + .imageScale(.large) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) { + removeDLC(at: IndexSet(integer: index)) + } + } label: { + Label("Delete", systemImage: "trash") + } + } + } + + // MARK: - Functions + private func loadData() { + dlcs = Self.loadDlc(game) + } + + private func toggleDLC(_ dlc: DownloadableContentContainer) { + guard let index = dlcs.firstIndex(where: { $0.id == dlc.id }) else { return } + + let toggle = !dlcs[index].isEnabled + dlcs[index].downloadableContentNcaList = dlcs[index].downloadableContentNcaList.map { nca in + var mutableNca = nca + mutableNca.enabled = toggle + return mutableNca + } + + Self.saveDlcs(game, dlc: dlcs) + } + + private func removeDLCs(at offsets: IndexSet) { + offsets.forEach { removeDLC(at: IndexSet(integer: $0)) } + } + + private func removeDLC(at indexSet: IndexSet) { + guard let index = indexSet.first else { return } + + let dlcToRemove = dlcs[index] + let path = URL.documentsDirectory.appendingPathComponent(dlcToRemove.containerPath) + + do { + try FileManager.default.removeItem(at: path) + dlcs.remove(at: index) + Self.saveDlcs(game, dlc: dlcs) + } catch { + print("Failed to remove DLC: \(error)") + } + } + + private func handleFileImport(result: Result<[URL], Error>) { + switch result { + case .success(let urls): + for url in urls { + importDLC(from: url) + } + case .failure(let error): + print("File import failed: \(error.localizedDescription)") + } + } + + private func importDLC(from url: URL) { + guard url.startAccessingSecurityScopedResource() else { + print("Failed to access security-scoped resource") + return + } + defer { url.stopAccessingSecurityScopedResource() } + + do { + let fileManager = FileManager.default + let dlcDirectory = URL.documentsDirectory.appendingPathComponent("dlc") + let gameDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId) + + try fileManager.createDirectory(at: gameDlcDirectory, withIntermediateDirectories: true) + + // Copy the DLC file + let destinationURL = gameDlcDirectory.appendingPathComponent(url.lastPathComponent) + try? fileManager.removeItem(at: destinationURL) + try fileManager.copyItem(at: url, to: destinationURL) + + // Fetch DLC metadata from Ryujinx + let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: destinationURL.path) + guard !dlcContent.isEmpty else { + print("No valid DLC content found") + return + } + + let newDlcContainer = DownloadableContentContainer( + containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL), + downloadableContentNcaList: dlcContent + ) + + dlcs.append(newDlcContainer) + Self.saveDlcs(game, dlc: dlcs) + + } catch { + print("Error importing DLC: \(error)") + } + } + } +// MARK: - Helper Methods private extension DLCManagerSheet { static func loadDlc(_ game: Game) -> [DownloadableContentContainer] { let jsonURL = dlcJsonPath(for: game) - try? FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true) - guard let data = try? Data(contentsOf: jsonURL), - var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data) - else { return [] } - - result = result.filter { container in - let path = URL.documentsDirectory.appendingPathComponent(container.containerPath) - return FileManager.default.fileExists(atPath: path.path) + + do { + try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true) + + guard FileManager.default.fileExists(atPath: jsonURL.path), + let data = try? Data(contentsOf: jsonURL), + var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data) + else { return [] } + + result = result.filter { container in + let path = URL.documentsDirectory.appendingPathComponent(container.containerPath) + return FileManager.default.fileExists(atPath: path.path) + } + + return result + } catch { + print("Error loading DLCs: \(error)") + return [] } - - return result } - + static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) { - guard let data = try? JSONEncoder().encode(dlc) else { return } - try? data.write(to: dlcJsonPath(for: game)) + do { + let data = try JSONEncoder().encode(dlc) + try data.write(to: dlcJsonPath(for: game)) + } catch { + print("Error saving DLCs: \(error)") + } } - + static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String { "dlc/\(game.titleId)/\(dlcPath.lastPathComponent)" } - + static func dlcJsonPath(for game: Game) -> URL { - URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game.titleId).appendingPathComponent("dlc.json") + URL.documentsDirectory + .appendingPathComponent("games") + .appendingPathComponent(game.titleId) + .appendingPathComponent("dlc.json") } } +// MARK: - Array Extension +extension Array where Element: AnyObject { + mutating func mutableForEach(_ body: (inout Element) -> Void) { + for index in indices { + var element = self[index] + body(&element) + self[index] = element + } + } +} +// MARK: - URL Extension extension URL { @available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above") static var documentsDirectory: URL { diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift index 145544a08..f4a236264 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift @@ -9,149 +9,169 @@ import SwiftUI import UniformTypeIdentifiers struct UpdateManagerSheet: View { - @State private var items: [String] = [] - @State private var paths: [URL] = [] - @State private var selectedItem: String? = nil + // MARK: - Properties + @State private var updates: [UpdateItem] = [] @Binding var game: Game? @State private var isSelectingGameUpdate = false @State private var jsonURL: URL? = nil + @Environment(\.dismiss) private var dismiss + + // MARK: - Models + class UpdateItem: Identifiable, ObservableObject { + let id = UUID() + let url: URL + let filename: String + let path: String + @Published var isSelected: Bool = false + + init(url: URL, filename: String, path: String, isSelected: Bool = false) { + self.url = url + self.filename = filename + self.path = path + self.isSelected = isSelected + } + } + + // MARK: - Body var body: some View { - NavigationView { - List(paths, id: \..self, selection: $selectedItem) { item in - Button(action: { - selectItem(item.lastPathComponent) - }) { - HStack { - Text(item.lastPathComponent) - .foregroundStyle(Color(uiColor: .label)) - Spacer() - if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" { - Image(systemName: "checkmark.circle.fill") - .foregroundStyle(Color.accentColor) - .font(.system(size: 24)) - } else { - Image(systemName: "circle") - .foregroundStyle(Color(uiColor: .secondaryLabel)) - .font(.system(size: 24)) - } + iOSNav { + List { + if updates.isEmpty { + emptyStateView + } else { + ForEach(updates) { update in + updateRow(update) + } + .onDelete(perform: removeUpdates) + } + } + .navigationTitle("\(game?.titleName ?? "Game") Updates") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Done") { + dismiss() } } - .contextMenu { + + ToolbarItem(placement: .navigationBarTrailing) { Button { - removeUpdate(item) + isSelectingGameUpdate = true } label: { - Text("Remove Update") + Label("Add Update", systemImage: "plus") } } } .onAppear { - print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json")) - - loadJSON(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json")) - } - .navigationTitle("\(game!.titleName) Updates") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - Button("Add", systemImage: "plus") { - isSelectingGameUpdate = true - } + loadData() } } - .fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item]) { result in - switch result { - case .success(let url): - guard url.startAccessingSecurityScopedResource() else { - print("Failed to access security-scoped resource") - return - } - defer { url.stopAccessingSecurityScopedResource() } - - let gameInfo = game! - - do { - let fileManager = FileManager.default - let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - let updatedDirectory = documentsDirectory.appendingPathComponent("updates") - let romUpdatedDirectory = updatedDirectory.appendingPathComponent(gameInfo.titleId) + .fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item], onCompletion: handleFileImport) + } + + // MARK: - Views + private var emptyStateView: some View { + Group { + if #available(iOS 17, *) { + ContentUnavailableView( + "No Updates Found", + systemImage: "arrow.down.circle", + description: Text("Tap the + button to add game updates.") + ) + } else { + VStack(spacing: 20) { + Spacer() - if !fileManager.fileExists(atPath: updatedDirectory.path) { - try fileManager.createDirectory(at: updatedDirectory, withIntermediateDirectories: true, attributes: nil) - } - - if !fileManager.fileExists(atPath: romUpdatedDirectory.path) { - try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil) - } - - let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent) - try? fileManager.copyItem(at: url, to: destinationURL) - - items.append("updates/" + gameInfo.titleId + "/" + url.lastPathComponent) - selectItem(url.lastPathComponent) - Ryujinx.shared.games = Ryujinx.shared.loadGames() - loadJSON(jsonURL!) - } catch { - print("Error copying game file: \(error)") + Image(systemName: "arrow.down.circle") + .font(.system(size: 64)) + .foregroundColor(.secondary) + + Text("No Updates Found") + .font(.title2) + .fontWeight(.semibold) + + Text("Tap the + button to add game updates.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) + + Spacer() } - case .failure(let err): - print("File import failed: \(err.localizedDescription)") + .frame(maxWidth: .infinity) + .listRowInsets(EdgeInsets()) } } } - func removeUpdate(_ game: URL) { - let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)" - paths.removeAll { $0 == game } - items.removeAll { $0 == gameString } - - if selectedItem == gameString { - selectedItem = nil + private func updateRow(_ update: UpdateItem) -> some View { + Button { + toggleSelection(update) + } label: { + HStack { + Text(update.filename) + .foregroundStyle(.primary) + Spacer() + Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle") + .foregroundStyle(update.isSelected ? .primary : .secondary) + .imageScale(.large) + } + .contentShape(Rectangle()) } - - do { - try FileManager.default.removeItem(at: game) - } catch { - print(error) + .buttonStyle(.plain) + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + if let index = updates.firstIndex(where: { $0.path == update.path }) { + removeUpdate(at: IndexSet(integer: index)) + } + } label: { + Label("Delete", systemImage: "trash") + } } - - saveJSON(selectedItem: selectedItem ?? "") - Ryujinx.shared.games = Ryujinx.shared.loadGames() } - func saveJSON(selectedItem: String?) { + // MARK: - Functions + private func loadData() { + guard let game = game else { return } + + let documentsDirectory = URL.documentsDirectory + jsonURL = documentsDirectory + .appendingPathComponent("games") + .appendingPathComponent(game.titleId) + .appendingPathComponent("updates.json") + + loadJSON() + } + + private func loadJSON() { guard let jsonURL = jsonURL else { return } - do { - let jsonDict = ["paths": items, "selected": selectedItem ?? self.selectedItem ?? ""] as [String: Any] - let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted) - try newData.write(to: jsonURL) - } catch { - print("Failed to update JSON: \(error)") - } - } - - func loadJSON(_ json: URL) { - self.jsonURL = json - - guard let jsonURL else { return } do { + if !FileManager.default.fileExists(atPath: jsonURL.path) { + createDefaultJSON() + return + } + let data = try Data(contentsOf: jsonURL) if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let list = jsonDict["paths"] as? [String] - { - - let filteredList = list.filter { relativePath in + let paths = jsonDict["paths"] as? [String], + let selected = jsonDict["selected"] as? String { + + let filteredPaths = paths.filter { relativePath in let path = URL.documentsDirectory.appendingPathComponent(relativePath) return FileManager.default.fileExists(atPath: path.path) } - - let urls: [URL] = filteredList.map { relativePath in - URL.documentsDirectory.appendingPathComponent(relativePath) + + updates = filteredPaths.map { relativePath in + let url = URL.documentsDirectory.appendingPathComponent(relativePath) + return UpdateItem( + url: url, + filename: url.lastPathComponent, + path: relativePath, + isSelected: selected == relativePath + ) } - - items = filteredList - paths = urls - selectedItem = jsonDict["selected"] as? String } } catch { print("Failed to read JSON: \(error)") @@ -159,42 +179,119 @@ struct UpdateManagerSheet: View { } } - func createDefaultJSON() { + private func createDefaultJSON() { guard let jsonURL = jsonURL else { return } - let defaultData: [String: Any] = ["selected": "", "paths": []] + do { + try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true) + + let defaultData: [String: Any] = ["selected": "", "paths": []] let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted) try newData.write(to: jsonURL) - items = [] - selectedItem = "" + updates = [] } catch { print("Failed to create default JSON: \(error)") } } - func selectItem(_ item: String) { - let newSelection = "updates/\(game!.titleId)/\(item)" - - guard let jsonURL else { return } - - do { - let data = try Data(contentsOf: jsonURL) - try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true) - var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:] - - if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection { - jsonDict["selected"] = "" - selectedItem = "" - } else { - jsonDict["selected"] = "\(newSelection)" - selectedItem = newSelection + private func handleFileImport(result: Result) { + switch result { + case .success(let selectedURL): + guard let game = game, + selectedURL.startAccessingSecurityScopedResource() else { + print("Failed to access security-scoped resource") + return } - jsonDict["paths"] = items - + defer { selectedURL.stopAccessingSecurityScopedResource() } + + do { + let fileManager = FileManager.default + let updatesDirectory = URL.documentsDirectory.appendingPathComponent("updates") + let gameUpdatesDirectory = updatesDirectory.appendingPathComponent(game.titleId) + + // Create directories if needed + try fileManager.createDirectory(at: gameUpdatesDirectory, withIntermediateDirectories: true) + + // Copy the file + let destinationURL = gameUpdatesDirectory.appendingPathComponent(selectedURL.lastPathComponent) + try? fileManager.removeItem(at: destinationURL) // Remove if exists + try fileManager.copyItem(at: selectedURL, to: destinationURL) + + // Add to updates + let relativePath = "updates/\(game.titleId)/\(selectedURL.lastPathComponent)" + let newUpdate = UpdateItem( + url: destinationURL, + filename: selectedURL.lastPathComponent, + path: relativePath + ) + + updates.append(newUpdate) + toggleSelection(newUpdate) + + // Reload games + Ryujinx.shared.games = Ryujinx.shared.loadGames() + } catch { + print("Error copying update file: \(error)") + } + + case .failure(let error): + print("File import failed: \(error.localizedDescription)") + } + } + + private func toggleSelection(_ update: UpdateItem) { + print("toggle selection \(update.path)") + + updates = updates.map { item in + var mutableItem = item + mutableItem.isSelected = item.path == update.path && !update.isSelected + print(mutableItem.isSelected) + print(update.isSelected) + return mutableItem + } + + print(updates) + + saveJSON() + } + + private func removeUpdates(at offsets: IndexSet) { + offsets.forEach { removeUpdate(at: IndexSet(integer: $0)) } + } + + private func removeUpdate(at indexSet: IndexSet) { + guard let index = indexSet.first else { return } + + let updateToRemove = updates[index] + + do { + // Remove the file + try FileManager.default.removeItem(at: updateToRemove.url) + + // Remove from updates array + updates.remove(at: index) + + // Save changes + saveJSON() + + // Reload games + Ryujinx.shared.games = Ryujinx.shared.loadGames() + } catch { + print("Failed to remove update: \(error)") + } + } + + private func saveJSON() { + guard let jsonURL = jsonURL else { return } + + do { + let paths = updates.map { $0.path } + let selected = updates.first(where: { $0.isSelected })?.path ?? "" + + let jsonDict = ["paths": paths, "selected": selected] as [String: Any] let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted) try newData.write(to: jsonURL) - Ryujinx.shared.games = Ryujinx.shared.loadGames() } catch { print("Failed to update JSON: \(error)") } diff --git a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift index 60900b039..9a55bc66c 100644 --- a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift +++ b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift @@ -21,7 +21,7 @@ struct SetupView: View { var body: some View { iOSNav { ZStack { - if UIDevice.current.userInterfaceIdiom == .pad { + if UIDevice.current.systemName.contains("iPadOS") { iPadSetupView( finished: $finished, isImportingKeys: $isImportingKeys, @@ -65,8 +65,9 @@ struct SetupView: View { initialize() finished = false keysImported = Ryujinx.shared.checkIfKeysImported() - print((Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0)) - firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0") + + let firmware = Ryujinx.shared.fetchFirmwareVersion() + firmImported = (firmware == "" ? "0" : firmware) != "0" } } @@ -369,9 +370,8 @@ struct SetupView: View { Ryujinx.shared.installFirmware(firmwarePath: fileURL.path) - print(Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0) - - firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0") + let firmware = Ryujinx.shared.fetchFirmwareVersion() + firmImported = (firmware == "" ? "0" : firmware) != "0" alertMessage = "Firmware installed successfully" showAlert = true diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Headers/RyujinxKeyboard.h b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Headers/RyujinxHelper.h similarity index 100% rename from src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Headers/RyujinxKeyboard.h rename to src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Headers/RyujinxHelper.h diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Info.plist b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist similarity index 52% rename from src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Info.plist rename to src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist index ead4b1203..0a5037678 100644 Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Info.plist and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Info.plist differ diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Modules/module.modulemap b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Modules/module.modulemap new file mode 100644 index 000000000..ce20a4e6c --- /dev/null +++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module RyujinxHelper { + umbrella header "RyujinxHelper.h" + export * + + module * { export * } +} diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/RyujinxKeyboard b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper similarity index 79% rename from src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/RyujinxKeyboard rename to src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper index a1ba63ade..cfb7b2348 100755 Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/RyujinxKeyboard and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/RyujinxHelper differ diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/_CodeSignature/CodeResources b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources similarity index 91% rename from src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/_CodeSignature/CodeResources rename to src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources index 02359dbd6..8ff39bf10 100644 --- a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/_CodeSignature/CodeResources +++ b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxHelper.framework/_CodeSignature/CodeResources @@ -4,22 +4,22 @@ files - Headers/RyujinxKeyboard.h + Headers/RyujinxHelper.h 5P7GN4g050n199pV6/+SpfMBgJc= Info.plist - hYdI/ktAKwjBSfaJpt6Yc8UKLCY= + UOH9NuuEcz5NQiQlrM2LNFaG2pI= Modules/module.modulemap - 0kFAMoTn+4Q1J/dM6uMLe3EhbL0= + JDij7psMD6pZZpigUfkSQldib+I= files2 - Headers/RyujinxKeyboard.h + Headers/RyujinxHelper.h hash2 @@ -30,7 +30,7 @@ hash2 - K+ZyxKhTI4bMVZuHBIspvd2PFqvCOlVUFYmwF96O5NQ= + 5t/lQcpkzC5bwJqFQqIf6h1ldlhHouYzDawRVrnUeyM= diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Modules/module.modulemap b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Modules/module.modulemap deleted file mode 100644 index a85f1c291..000000000 --- a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/RyujinxKeyboard.framework/Modules/module.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module RyujinxKeyboard { - umbrella header "RyujinxKeyboard.h" - export * - - module * { export * } -} diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs index d067c489d..aa16d883e 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs @@ -769,7 +769,7 @@ namespace Ryujinx.Graphics.Vulkan private void SetData(ReadOnlySpan data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle? region = null) { - const int MaxChunkSize = 1024 * 1024 * 16; // 16MB chunks + const int MaxChunkSize = 1024 * 1024; int bufferDataLength = GetBufferDataLength(data.Length); @@ -786,21 +786,39 @@ namespace Ryujinx.Graphics.Vulkan for (int i = 0; i < layers; i++) { - int currentLayer = layer + i; - int currentLayerSize = Math.Min(layerSize, data.Length - offset); - var layerData = data.Slice(offset, currentLayerSize); - - ProcessChunk(layerData, currentLayer, level, 1, levels, true); - offset += layerSize; - if (offset >= data.Length) break; + + int currentLayer = layer + i; + int currentLayerSize = Math.Min(layerSize, data.Length - offset); + + if (currentLayerSize <= 0) + break; + + try + { + var layerData = data.Slice(offset, currentLayerSize); + ProcessChunk(layerData, currentLayer, level, 1, levels, true); + offset += layerSize; + } + catch (ArgumentOutOfRangeException) + { + break; + } } } else if (region.HasValue) { var rect = region.Value; + + if (rect.Width <= 0 || rect.Height <= 0) + return; + int dataPerPixel = data.Length / (rect.Width * rect.Height); + + if (dataPerPixel <= 0) + return; + int rowStride = rect.Width * dataPerPixel; int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride); @@ -811,42 +829,63 @@ namespace Ryujinx.Graphics.Vulkan while (currentY < rect.Y + originalHeight) { int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY); + + if (chunkHeight <= 0) + break; + var chunkRegion = new Rectangle(rect.X, currentY, rect.Width, chunkHeight); int chunkSize = chunkHeight * rowStride; - int safeChunkSize = Math.Min(chunkSize, data.Length - offset); - var chunkData = data.Slice(offset, safeChunkSize); - - ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion); - - currentY += chunkHeight; - offset += chunkSize; if (offset >= data.Length) break; + + int safeChunkSize = Math.Min(chunkSize, data.Length - offset); + + if (safeChunkSize <= 0) + break; + + try + { + var chunkData = data.Slice(offset, safeChunkSize); + ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion); + + currentY += chunkHeight; + offset += chunkSize; + } + catch (ArgumentOutOfRangeException) + { + break; + } } } else { ProcessChunk(data, layer, level, layers, levels, singleSlice, region); } + } + + private void ProcessChunk(ReadOnlySpan chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle? chunkRegion = null) + { + int chunkBufferLength = GetBufferDataLength(chunkData.Length); - void ProcessChunk(ReadOnlySpan chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle? chunkRegion = null) + if (chunkBufferLength <= 0) + return; + + using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength); + + using (var imageAuto = GetImage()) { - int chunkBufferLength = GetBufferDataLength(chunkData.Length); + bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer); + var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer(); - using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength); - - using (var imageAuto = GetImage()) + if (loadInline) + { + _gd.PipelineInternal.EndRenderPass(); + } + + try { - bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer); - var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer(); - - if (loadInline) - { - _gd.PipelineInternal.EndRenderPass(); - } - CopyDataToBuffer(bufferHolder.GetDataStorage(0, chunkBufferLength), chunkData); var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; @@ -854,6 +893,11 @@ namespace Ryujinx.Graphics.Vulkan if (chunkRegion.HasValue) { + var region = chunkRegion.Value; + + if (region.Width <= 0 || region.Height <= 0) + return; + CopyFromOrToBuffer( cbs.CommandBuffer, buffer, @@ -862,10 +906,10 @@ namespace Ryujinx.Graphics.Vulkan false, chunkLayer, chunkLevel, - chunkRegion.Value.X, - chunkRegion.Value.Y, - chunkRegion.Value.Width, - chunkRegion.Value.Height); + region.X, + region.Y, + region.Width, + region.Height); } else { @@ -881,7 +925,11 @@ namespace Ryujinx.Graphics.Vulkan chunkLevels, chunkSingleSlice); } - } + } + catch (Exception e) + { + + } } } diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs index 822084387..c02c8850f 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs @@ -239,6 +239,7 @@ namespace Ryujinx.HLE.HOS.Applets StringLengthMax = _keyboardForegroundConfig.StringLengthMax, InitialText = initialText, }; + _device.UIHandler.DisplayInputDialog(args, inputText => { Console.WriteLine($"User entered: {inputText}"); diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs index e3f505f37..c546a8791 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs @@ -561,7 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid context.ResponseData.Write((int)_gyroscopeZeroDriftMode); - Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode }); + // Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode }); return ResultCode.Success; } diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 1531c2ee8..f99547296 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -251,15 +251,16 @@ namespace Ryujinx.Headless.SDL2 [UnmanagedCallersOnly(EntryPoint = "get_current_fps")] public static unsafe int GetFPS() { - if (_window != null) { - Switch Device = _window.Device; + if (_window == null || _window.Device == null) + { + return 0; + } - int intValue = (int)Device.Statistics.GetGameFrameRate(); + Switch Device = _window.Device; - return intValue; - } - return 0; - + int intValue = (int)Device.Statistics.GetGameFrameRate(); + + return intValue; } [UnmanagedCallersOnly(EntryPoint = "initialize")] diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index 48e17b4f1..5f8eda72a 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -486,7 +486,12 @@ namespace Ryujinx.Headless.SDL2 public bool DisplayMessageDialog(string title, string message) { - SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle); + if (OperatingSystem.IsIOS()) + { + AlertHelper.ShowAlert(title, message, false); + } else { + SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle); + } return true; } diff --git a/src/Ryujinx.Headless.SDL2/Keyboard-iOS.cs b/src/Ryujinx.Headless.SDL2/iOSAlertHelper.cs similarity index 64% rename from src/Ryujinx.Headless.SDL2/Keyboard-iOS.cs rename to src/Ryujinx.Headless.SDL2/iOSAlertHelper.cs index 2d75906d3..96997e543 100644 --- a/src/Ryujinx.Headless.SDL2/Keyboard-iOS.cs +++ b/src/Ryujinx.Headless.SDL2/iOSAlertHelper.cs @@ -7,13 +7,16 @@ namespace Ryujinx.Headless.SDL2 { public static class AlertHelper { - [DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)] + [DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)] public static extern void showKeyboardAlert(string title, string message, string placeholder); - [DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)] + [DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)] + public static extern void showAlert(string title, string message, bool showCancel); + + [DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr getKeyboardInput(); - [DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)] + [DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)] private static extern void clearKeyboardInput(); public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action onTextEntered) @@ -38,5 +41,10 @@ namespace Ryujinx.Headless.SDL2 } }); } + + + public static void ShowAlert(string title, string message, bool cancel) { + showAlert(title, message, cancel); + } } }