forked from MeloNX/MeloNX
Implement new Virtual Controller Joystick. Add Game Requirements
This commit is contained in:
parent
8917ebf708
commit
2b7e29fa21
@ -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" */;
|
||||
|
@ -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
|
||||
|
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
@ -62,10 +62,11 @@
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugXPCServices = "NO"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES"
|
||||
viewDebuggingEnabled = "No"
|
||||
queueDebuggingEnabled = "No"
|
||||
consoleMode = "0"
|
||||
structuredConsoleMode = "2">
|
||||
<BuildableProductRunnable
|
||||
|
@ -131,10 +131,8 @@ class VirtualController {
|
||||
}
|
||||
|
||||
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
|
||||
let scaleFactor = 32767.0 / 160
|
||||
|
||||
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
|
||||
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
|
||||
let scaledX = Int16(min(32767.0, max(-32768.0, x * 32767.0)))
|
||||
let scaledY = Int16(min(32767.0, max(-32768.0, y * 32767.0)))
|
||||
|
||||
if stick == .right {
|
||||
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
|
||||
|
@ -38,6 +38,7 @@ class Ryujinx : ObservableObject {
|
||||
|
||||
@Published var controllerMap: [Controller] = []
|
||||
@Published var metalLayer: CAMetalLayer? = nil
|
||||
@Published var isPortrait = false
|
||||
@Published var firmwareversion = "0"
|
||||
@Published var emulationUIView: MeloMTKView? = nil
|
||||
@Published var config: Ryujinx.Configuration? = nil
|
||||
@ -510,9 +511,16 @@ class Ryujinx : ObservableObject {
|
||||
print("[Ryujinx] \(message)")
|
||||
}
|
||||
|
||||
public func updateOrientation() -> 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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<byte>.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<byte>.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<byte>.Empty)
|
||||
.CopyTo(new Span<byte>(bitmap, bitmapOffset, chunkSize));
|
||||
|
||||
currentY += chunkHeight;
|
||||
bitmapOffset += chunkSize;
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
|
||||
public PinnedSpan<byte> GetData()
|
||||
{
|
||||
BackgroundResource resources = _gd.BackgroundResources.Get();
|
||||
@ -738,14 +779,28 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
return GetDataFromBuffer(result, size, result);
|
||||
}
|
||||
|
||||
private ReadOnlySpan<byte> GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer, int level)
|
||||
private ReadOnlySpan<byte> 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<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
|
||||
return GetDataFromBuffer(result, size, result);
|
||||
if (size <= MaxChunkSize)
|
||||
{
|
||||
Span<byte> result = flushBuffer.GetTextureData(cbp, this, size, layer, level);
|
||||
return GetDataFromBuffer(result, size, result);
|
||||
}
|
||||
|
||||
byte[] fullResult = new byte[size];
|
||||
|
||||
Span<byte> fullTextureData = flushBuffer.GetTextureData(cbp, this, size, layer, level);
|
||||
|
||||
GetDataFromBuffer(fullTextureData, size, fullTextureData).CopyTo(fullResult);
|
||||
|
||||
return fullResult;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetData(MemoryOwner<byte> data)
|
||||
{
|
||||
@ -769,7 +824,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null)
|
||||
{
|
||||
const int MaxChunkSize = 1024 * 1024;
|
||||
const int MaxChunkSize = 1024 * 1024 * 96; // 96MB Chunks
|
||||
|
||||
int bufferDataLength = GetBufferDataLength(data.Length);
|
||||
|
||||
|
@ -9,8 +9,18 @@ namespace Ryujinx.Input.SDL2
|
||||
{
|
||||
private readonly Dictionary<int, string> _gamepadsInstanceIdsMapping;
|
||||
private readonly List<string> _gamepadsIds;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
public ReadOnlySpan<string> GamepadsIds => _gamepadsIds.ToArray();
|
||||
public ReadOnlySpan<string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user