diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index e4099b69e..721a265a4 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -24,7 +24,6 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; }; 4E12B23C2D797CFA00FB2271 /* MeloNX.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; }; 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; }; 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 4EA5AE812D16807500AD0B9F /* SwiftSVG */; }; @@ -203,7 +202,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */, CA8F9C322D3F5AB200D7E586 /* GameController.framework in Frameworks */, 4EA5AE822D16807500AD0B9F /* SwiftSVG in Frameworks */, 4E8A80772D5FDD2D0041B48F /* GameController.framework in Frameworks */, @@ -301,7 +299,6 @@ ); name = MeloNX; packageProductDependencies = ( - 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */, 4EA5AE812D16807500AD0B9F /* SwiftSVG */, ); productName = MeloNX; @@ -393,7 +390,6 @@ mainGroup = 4E80A9842CD6F54500029585; minimizedProjectReferenceProxies = 1; packageReferences = ( - 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */, 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */, ); preferredProjectObjectVersion = 56; @@ -721,6 +717,12 @@ "$(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; @@ -863,6 +865,18 @@ "$(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; @@ -955,6 +969,12 @@ "$(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; @@ -1097,6 +1117,18 @@ "$(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; @@ -1298,14 +1330,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.5.0; - }; - }; 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mchoe/SwiftSVG"; @@ -1317,11 +1341,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = { - isa = XCSwiftPackageProductDependency; - package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */; - productName = SwiftUIJoystick; - }; 4EA5AE812D16807500AD0B9F /* SwiftSVG */ = { isa = XCSwiftPackageProductDependency; package = 4EA5AE802D16807500AD0B9F /* XCRemoteSwiftPackageReference "SwiftSVG" */; diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index af8dd513e..5f080c7a7 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d611b071fbe94fdc9900a07a218340eab4ce2c3c7168bf6542f2830c0400a72b", + "originHash" : "fedf09a893a63378a2e53f631cd833ae83a0c9ee7338eb8d153b04fd34aaf805", "pins" : [ { "identity" : "swiftsvg", @@ -9,15 +9,6 @@ "branch" : "master", "revision" : "88b9ee086b29019e35f6f49c8e30e5552eb8fa9d" } - }, - { - "identity" : "swiftuijoystick", - "kind" : "remoteSourceControl", - "location" : "https://github.com/michael94ellis/SwiftUIJoystick", - "state" : { - "revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65", - "version" : "1.5.0" - } } ], "version" : 3 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 264afbacc..1b2bf73ff 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 131fd924a..ae7c58c0b 100644 --- a/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme +++ b/src/MeloNX/MeloNX.xcodeproj/xcshareddata/xcschemes/MeloNX.xcscheme @@ -1,7 +1,7 @@ + version = "2.0"> Bool { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + return (window.bounds.size.height > window.bounds.size.width) + } + return false + } + func ryuIsJITEnabled() { jitenabled = isJITEnabled() - print("JIT \(jitenabled)") } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift index 077a3aac1..7aee87c93 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift @@ -7,7 +7,6 @@ import SwiftUI import GameController -import SwiftUIJoystick import CoreMotion struct ControllerView: View { @@ -15,6 +14,8 @@ struct ControllerView: View { @AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0 @AppStorage("stick-button") private var stickButton = false @State private var isPortrait = true + @State var hideDpad = false + @State var hideABXY = false @Environment(\.verticalSizeClass) var verticalSizeClass @@ -45,16 +46,22 @@ struct ControllerView: View { VStack(spacing: 15) { ShoulderButtonsViewLeft() ZStack { - Joystick() - DPadView() + JoystickController(showBackground: $hideDpad) + if !hideDpad { + DPadView() + .animation(.easeInOut(duration: 0.2), value: hideDpad) + } } } VStack(spacing: 15) { ShoulderButtonsViewRight() ZStack { - Joystick(iscool: true) - ABXYView() + JoystickController(iscool: true, showBackground: $hideABXY) + if !hideABXY { + ABXYView() + .animation(.easeInOut(duration: 0.2), value: hideABXY) + } } } } @@ -81,11 +88,14 @@ struct ControllerView: View { Spacer() HStack { - VStack(spacing: 15) { + VStack(spacing: 20) { ShoulderButtonsViewLeft() ZStack { - Joystick() - DPadView() + JoystickController(showBackground: $hideDpad) + if !hideDpad { + DPadView() + .animation(.easeInOut(duration: 0.2), value: hideDpad) + } } } @@ -95,11 +105,14 @@ struct ControllerView: View { Spacer() - VStack(spacing: 15) { + VStack(spacing: 20) { ShoulderButtonsViewRight() ZStack { - Joystick(iscool: true) - ABXYView() + JoystickController(iscool: true, showBackground: $hideABXY) + if !hideABXY { + ABXYView() + .animation(.easeInOut(duration: 0.2), value: hideABXY) + } } } } @@ -199,11 +212,11 @@ struct DPadView: View { @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 var body: some View { - VStack(spacing: 5) { + VStack(spacing: 7) { ButtonView(button: .dPadUp) - HStack(spacing: 20) { + HStack(spacing: 22) { ButtonView(button: .dPadLeft) - Spacer(minLength: 20) + Spacer(minLength: 22) ButtonView(button: .dPadRight) } ButtonView(button: .dPadDown) @@ -224,11 +237,11 @@ struct ABXYView: View { @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 var body: some View { - VStack(spacing: 5) { + VStack(spacing: 7) { ButtonView(button: .X) - HStack(spacing: 20) { + HStack(spacing: 22) { ButtonView(button: .Y) - Spacer(minLength: 20) + Spacer(minLength: 22) ButtonView(button: .A) } ButtonView(button: .B) @@ -259,19 +272,25 @@ struct ButtonView: View { .resizable() .scaledToFit() .frame(width: width, height: height) - .foregroundColor(true ? Color.white.opacity(0.9) : Color.black.opacity(0.9)) + .foregroundColor(true ? Color.white.opacity(0.5) : Color.black.opacity(0.5)) .background( Group { - if !button.isTrigger { + if !button.isTrigger && button != .leftStick && button != .rightStick { Circle() .fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3)) .frame(width: width * 1.25, height: height * 1.25) - } else { + } else if button == .leftStick || button == .rightStick { 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)) + } else if button.isTrigger { + Image(systemName: "" + String(turntobutton(buttonText))) + .resizable() + .scaledToFit() + .frame(width: width * 1.25, height: height * 1.25) + .foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3)) } } ) @@ -286,10 +305,24 @@ struct ButtonView: View { } ) .onAppear { + print(String(buttonText.dropFirst(2))) configureSizeForButton() } } + private func turntobutton(_ string: String) -> String { + var sting = string + if string.hasPrefix("zl") || string.hasPrefix("zr") { + sting = String(string.dropFirst(3)) + } else { + sting = String(string.dropFirst(2)) + } + sting = sting.replacingOccurrences(of: "rectangle", with: "button") + sting = sting.replacingOccurrences(of: ".fill", with: ".horizontal.fill") + + return sting + } + private func handleButtonPress() { if !isPressed { isPressed = true diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick.swift new file mode 100644 index 000000000..f7604f8d6 --- /dev/null +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick.swift @@ -0,0 +1,83 @@ +// +// Joystick.swift +// MeloNX +// +// Created by Stossy11 on 21/03/2025. +// + + +import SwiftUI + +struct Joystick: View { + @Binding var position: CGPoint + @State var joystickSize: CGFloat + var boundarySize: CGFloat + + @State private var offset: CGSize = .zero + @Binding var showBackground: Bool + + var dragGesture: some Gesture { + DragGesture() + .onChanged { value in + withAnimation(.easeIn) { + showBackground = true + } + + let translation = value.translation + let distance = sqrt(translation.width * translation.width + translation.height * translation.height) + let maxRadius = (boundarySize - joystickSize) / 2 + let extendedRadius = maxRadius + (joystickSize / 2) + + if distance <= extendedRadius { + offset = translation + } else { + let angle = atan2(translation.height, translation.width) + offset = CGSize(width: cos(angle) * extendedRadius, height: sin(angle) * extendedRadius) + } + position = CGPoint(x: offset.width / extendedRadius, y: offset.height / extendedRadius) + } + .onEnded { _ in + offset = .zero // Reset to center + position = .zero + withAnimation(.easeOut) { // Smooth animation when hiding the background + showBackground = false + } + } + } + + var body: some View { + ZStack { + Circle() + .fill(Color.clear.opacity(0)) + .frame(width: boundarySize, height: boundarySize) + + + if showBackground { + Circle() + .fill(Color.gray.opacity(0.4)) + .frame(width: boundarySize, height: boundarySize) + .animation(.easeInOut(duration: 0.05), value: showBackground) + .transition(.scale) + } + + Circle() + .fill(Color.white.opacity(0.5)) + .frame(width: joystickSize, height: joystickSize) + .background( + Circle() + .fill(Color.gray.opacity(0.3)) + .frame(width: joystickSize * 1.25, height: joystickSize * 1.25) + ) + .offset(offset) + .gesture(dragGesture) + } + .frame(width: boundarySize, height: boundarySize) + .onChange(of: showBackground) { newValue in + if newValue { + joystickSize *= 1.4 + } else { + joystickSize = (boundarySize * 0.2) + } + } + } +} 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 3bc838b97..fb258687e 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift @@ -7,13 +7,13 @@ // import SwiftUI -import SwiftUIJoystick -public struct Joystick: View { +struct JoystickController: View { @State var iscool: Bool? = nil @Environment(\.colorScheme) var colorScheme - @ObservedObject public var joystickMonitor = JoystickMonitor() + @Binding var showBackground: Bool @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 + @State var position: CGPoint = CGPoint(x: 0, y: 0) var dragDiameter: CGFloat { var selfs = CGFloat(160) selfs *= controllerScale @@ -23,39 +23,21 @@ public struct Joystick: View { 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(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 - 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) + VStack { + Joystick(position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground) + .onChange(of: position) { 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) + } } - } } } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameCompatibility/GameCompatibilityCache.swift b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameCompatibility/GameCompatibilityCache.swift new file mode 100644 index 000000000..4b757ff35 --- /dev/null +++ b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameCompatibility/GameCompatibilityCache.swift @@ -0,0 +1,44 @@ +// +// GameRequirementsCache.swift +// MeloNX +// +// Created by Stossy11 on 21/03/2025. +// + + +import Foundation + +class GameCompatibiliryCache { + static let shared = GameCompatibiliryCache() + private let cacheKey = "gameRequirementsCache" + private let timestampKey = "gameRequirementsCacheTimestamp" + + private let cacheDuration: TimeInterval = Double.random(in: 3...5) * 24 * 60 * 60 // Randomly pick 3-5 days + + func getCachedData() -> [GameRequirements]? { + guard let cachedData = UserDefaults.standard.data(forKey: cacheKey), + let timestamp = UserDefaults.standard.object(forKey: timestampKey) as? Date else { + return nil + } + + let timeElapsed = Date().timeIntervalSince(timestamp) + if timeElapsed > cacheDuration { + clearCache() + return nil + } + + return try? JSONDecoder().decode([GameRequirements].self, from: cachedData) + } + + func setCachedData(_ data: [GameRequirements]) { + if let encodedData = try? JSONEncoder().encode(data) { + UserDefaults.standard.set(encodedData, forKey: cacheKey) + UserDefaults.standard.set(Date(), forKey: timestampKey) + } + } + + func clearCache() { + UserDefaults.standard.removeObject(forKey: cacheKey) + UserDefaults.standard.removeObject(forKey: timestampKey) + } +} diff --git a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift index 6670b20f9..23ce27fd5 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift @@ -31,6 +31,7 @@ struct GameLibraryView: View { @State var isSelectingGameDLC: Bool = false @StateObject var ryujinx = Ryujinx.shared @State var gameInfo: Game? + @State var gameRequirements: [GameRequirements] = [] var games: Binding<[Game]> { Binding( get: { Ryujinx.shared.games }, @@ -79,7 +80,7 @@ struct GameLibraryView: View { if !isSearching && !realRecentGames.isEmpty { Section { ForEach(realRecentGames) { game in - GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo) + GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { removeFromRecentGames(game) @@ -94,14 +95,14 @@ struct GameLibraryView: View { Section { ForEach(filteredGames) { game in - GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo) + GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo) } } header: { Text("Others") } } else { ForEach(filteredGames) { game in - GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo) + GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo) } } } @@ -113,6 +114,15 @@ struct GameLibraryView: View { let firmware = Ryujinx.shared.fetchFirmwareVersion() firmwareversion = (firmware == "" ? "0" : firmware) + + pullGameCompatibility() { game in + switch game { + case .success(let sucees): + gameRequirements = sucees + case .failure(_): + print("uhohh stinki") + } + } } .fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in switch result { @@ -367,7 +377,6 @@ extension Game: Codable { 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 } @@ -386,10 +395,11 @@ extension Game: Codable { struct GameListRow: View { let game: Game @Binding var startemu: Game? - @Binding var games: [Game] // Add this binding + @Binding var games: [Game] @Binding var isViewingGameInfo: Bool @Binding var isSelectingGameUpdate: Bool @Binding var isSelectingGameDLC: Bool + @Binding var gameRequirements: [GameRequirements] @Binding var gameInfo: Game? @State var gametoDelete: Game? @State var showGameDeleteConfirmation: Bool = false @@ -433,8 +443,31 @@ struct GameListRow: View { .foregroundColor(.secondary) } + Spacer() + if let game = gameRequirements.first(where: { $0.game_id == game.titleId }) { + let totalMemory = ProcessInfo.processInfo.physicalMemory + + VStack(spacing: 10) { + Capsule().fill(game.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.green : Color.red) + .frame(width: 70 / 1.5, height: 35 / 1.5) + .overlay { + Text(game.device_memory) + .foregroundStyle(.white) + .font(.system(size: 11)) + } + + Capsule().fill(game.color) + .frame(width: 70 / 1.5, height: 35 / 1.5) + .overlay { + Text(game.compatibility) + .foregroundStyle(.white) + .font(.system(size: 10)) + } + } + } + Image(systemName: "play.circle.fill") .font(.title2) .foregroundColor(.accentColor) @@ -500,6 +533,7 @@ struct GameListRow: View { } } + private func deleteGame(game: Game) { let fileManager = FileManager.default do { @@ -510,3 +544,66 @@ struct GameListRow: View { } } } + +struct GameRequirements: Codable { + var game_id: String + var compatibility: String + var device_memory: String + var memoryInt: Int { + var devicemem = device_memory + devicemem.removeLast(2) + print(devicemem) + return Int(devicemem) ?? 0 + } + + var color: Color { + switch compatibility { + case "Perfect": + return .green + case "Playable": + return .yellow + case "Menu": + return .orange + case "Boots": + return .red + case "Nothing": + return .black + default: + return .clear + } + } +} + +func pullGameCompatibility(completion: @escaping (Result<[GameRequirements], Error>) -> Void) { + if let cachedData = GameCompatibiliryCache.shared.getCachedData() { + completion(.success(cachedData)) + return + } + + guard let url = URL(string: "https://melonx.org/api/game_entries") else { + completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil))) + return + } + + let task = URLSession.shared.dataTask(with: url) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let data = data else { + completion(.failure(NSError(domain: "No data", code: 0, userInfo: nil))) + return + } + + do { + let decodedData = try JSONDecoder().decode([GameRequirements].self, from: data) + GameCompatibiliryCache.shared.setCachedData(decodedData) + completion(.success(decodedData)) + } catch { + completion(.failure(error)) + } + } + + task.resume() +} diff --git a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift index c6a789233..5d288fe07 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift @@ -51,6 +51,8 @@ struct SettingsView: View { @AppStorage("stick-button") var stickButton = false @AppStorage("waitForVPN") var waitForVPN = false + @AppStorage("HideButtons") var hideButtonsJoy = false + @State private var showResolutionInfo = false @State private var showAnisotropicInfo = false @State private var showControllerInfo = false diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs index aa16d883e..fe73f3c7d 100644 --- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs +++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs @@ -606,26 +606,67 @@ namespace Ryujinx.Graphics.Vulkan { return new TextureView(_gd, _device, info, Storage, FirstLayer + firstLayer, FirstLevel + firstLevel); } - + public byte[] GetData(int x, int y, int width, int height) { + const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks + int size = width * height * Info.BytesPerPixel; - using var bufferHolder = _gd.BufferManager.Create(_gd, size); - - using (var cbs = _gd.CommandBufferPool.Rent()) - { - var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; - var image = GetImage().Get(cbs).Value; - - CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height); - } - - bufferHolder.WaitForFences(); byte[] bitmap = new byte[size]; - GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span.Empty).CopyTo(bitmap); + + if (size <= MaxChunkSize) + { + using var bufferHolder = _gd.BufferManager.Create(_gd, size); + using (var cbs = _gd.CommandBufferPool.Rent()) + { + var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; + var image = GetImage().Get(cbs).Value; + CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, size, true, 0, 0, x, y, width, height); + } + + bufferHolder.WaitForFences(); + GetDataFromBuffer(bufferHolder.GetDataStorage(0, size), size, Span.Empty).CopyTo(bitmap); + return bitmap; + } + + + int dataPerPixel = Info.BytesPerPixel; + int rowStride = width * dataPerPixel; + int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride); + int originalHeight = height; + int currentY = y; + int bitmapOffset = 0; + + while (currentY < y + originalHeight) + { + int chunkHeight = Math.Min(rowsPerChunk, y + originalHeight - currentY); + + if (chunkHeight <= 0) + break; + + int chunkSize = chunkHeight * rowStride; + + // Process this chunk + using var bufferHolder = _gd.BufferManager.Create(_gd, chunkSize); + using (var cbs = _gd.CommandBufferPool.Rent()) + { + var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; + var image = GetImage().Get(cbs).Value; + CopyFromOrToBuffer(cbs.CommandBuffer, buffer, image, chunkSize, true, 0, 0, x, currentY, width, chunkHeight); + } + + bufferHolder.WaitForFences(); + GetDataFromBuffer(bufferHolder.GetDataStorage(0, chunkSize), chunkSize, Span.Empty) + .CopyTo(new Span(bitmap, bitmapOffset, chunkSize)); + + currentY += chunkHeight; + bitmapOffset += chunkSize; + } + return bitmap; } + public PinnedSpan GetData() { BackgroundResource resources = _gd.BackgroundResources.Get(); @@ -738,14 +779,28 @@ namespace Ryujinx.Graphics.Vulkan return GetDataFromBuffer(result, size, result); } - private ReadOnlySpan GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer, int level) + private ReadOnlySpan GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer = 0, int level = 0) { + const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks + int size = GetBufferDataLength(Info.GetMipSize(level)); - Span result = flushBuffer.GetTextureData(cbp, this, size, layer, level); - return GetDataFromBuffer(result, size, result); + if (size <= MaxChunkSize) + { + Span result = flushBuffer.GetTextureData(cbp, this, size, layer, level); + return GetDataFromBuffer(result, size, result); + } + + byte[] fullResult = new byte[size]; + + Span fullTextureData = flushBuffer.GetTextureData(cbp, this, size, layer, level); + + GetDataFromBuffer(fullTextureData, size, fullTextureData).CopyTo(fullResult); + + return fullResult; } + /// public void SetData(MemoryOwner data) { @@ -769,7 +824,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; + const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks int bufferDataLength = GetBufferDataLength(data.Length); diff --git a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs index 0ffae6c2e..d69ea835a 100644 --- a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs +++ b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs @@ -9,8 +9,18 @@ namespace Ryujinx.Input.SDL2 { private readonly Dictionary _gamepadsInstanceIdsMapping; private readonly List _gamepadsIds; + private readonly object _lock = new object(); - public ReadOnlySpan GamepadsIds => _gamepadsIds.ToArray(); + public ReadOnlySpan GamepadsIds + { + get + { + lock (_lock) + { + return _gamepadsIds.ToArray(); + } + } + } public string DriverName => "SDL2"; @@ -35,7 +45,7 @@ namespace Ryujinx.Input.SDL2 } } - private static string GenerateGamepadId(int joystickIndex) + private string GenerateGamepadId(int joystickIndex) { Guid guid = SDL_JoystickGetDeviceGUID(joystickIndex); @@ -44,14 +54,16 @@ namespace Ryujinx.Input.SDL2 return null; } + // Include joystickIndex at the start of the ID to maintain compatibility with GetJoystickIndexByGamepadId return joystickIndex + "-" + guid; } - private static int GetJoystickIndexByGamepadId(string id) + private int GetJoystickIndexByGamepadId(string id) { string[] data = id.Split("-"); - if (data.Length != 6 || !int.TryParse(data[0], out int joystickIndex)) + // Parse the joystick index from the ID string + if (data.Length < 2 || !int.TryParse(data[0], out int joystickIndex)) { return -1; } @@ -64,7 +76,11 @@ namespace Ryujinx.Input.SDL2 if (_gamepadsInstanceIdsMapping.TryGetValue(joystickInstanceId, out string id)) { _gamepadsInstanceIdsMapping.Remove(joystickInstanceId); - _gamepadsIds.Remove(id); + + lock (_lock) + { + _gamepadsIds.Remove(id); + } OnGamepadDisconnected?.Invoke(id); } @@ -74,6 +90,13 @@ namespace Ryujinx.Input.SDL2 { if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE) { + if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId)) + { + // Sometimes a JoyStick connected event fires after the app starts even though it was connected before + // so it is rejected to avoid doubling the entries. + return; + } + string id = GenerateGamepadId(joystickDeviceId); if (id == null) @@ -81,16 +104,21 @@ namespace Ryujinx.Input.SDL2 return; } - // Sometimes a JoyStick connected event fires after the app starts even though it was connected before - // so it is rejected to avoid doubling the entries. - if (_gamepadsIds.Contains(id)) + // Check if we already have this gamepad ID in our list + lock (_lock) { - return; + if (_gamepadsIds.Contains(id)) + { + return; + } } if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id)) { - _gamepadsIds.Add(id); + lock (_lock) + { + _gamepadsIds.Add(id); + } OnGamepadConnected?.Invoke(id); } @@ -103,13 +131,17 @@ namespace Ryujinx.Input.SDL2 { SDL2Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected; SDL2Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected; - + + // Simulate a full disconnect when disposing foreach (string id in _gamepadsIds) { OnGamepadDisconnected?.Invoke(id); } - _gamepadsIds.Clear(); + lock (_lock) + { + _gamepadsIds.Clear(); + } SDL2Driver.Instance.Dispose(); } @@ -130,11 +162,6 @@ namespace Ryujinx.Input.SDL2 return null; } - if (id != GenerateGamepadId(joystickIndex)) - { - return null; - } - IntPtr gamepadHandle = SDL_GameControllerOpen(joystickIndex); if (gamepadHandle == IntPtr.Zero) @@ -145,4 +172,4 @@ namespace Ryujinx.Input.SDL2 return new SDL2Gamepad(gamepadHandle, id); } } -} +} \ No newline at end of file