Add Virtual Controller from Pomelo
@ -7,8 +7,8 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4E0DED332D05695D00FEF007 /* SwiftUIJoystick */; };
|
||||
4E551F202CF128540096A2DF /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E80AA622CD7122800029585 /* GameController.framework */; };
|
||||
4E80AA212CD705DD00029585 /* SDL in Frameworks */ = {isa = PBXBuildFile; productRef = 4E80AA202CD705DD00029585 /* SDL */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -146,8 +146,8 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4E0DED342D05695D00FEF007 /* SwiftUIJoystick in Frameworks */,
|
||||
4E551F202CF128540096A2DF /* GameController.framework in Frameworks */,
|
||||
4E80AA212CD705DD00029585 /* SDL in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -218,7 +218,7 @@
|
||||
);
|
||||
name = MeloNX;
|
||||
packageProductDependencies = (
|
||||
4E80AA202CD705DD00029585 /* SDL */,
|
||||
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */,
|
||||
);
|
||||
productName = MeloNX;
|
||||
productReference = 4E80A98D2CD6F54500029585 /* MeloNX.app */;
|
||||
@ -303,9 +303,9 @@
|
||||
mainGroup = 4E80A9842CD6F54500029585;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
4E80AA1F2CD705DD00029585 /* XCRemoteSwiftPackageReference "SwiftSDL2" */,
|
||||
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 60;
|
||||
preferredProjectObjectVersion = 56;
|
||||
productRefGroup = 4E80A98E2CD6F54500029585 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@ -612,6 +612,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 = 0.0.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||
@ -736,6 +748,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 = 0.0.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||
@ -861,21 +885,21 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
4E80AA1F2CD705DD00029585 /* XCRemoteSwiftPackageReference "SwiftSDL2" */ = {
|
||||
4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/ctreffs/SwiftSDL2";
|
||||
repositoryURL = "https://github.com/michael94ellis/SwiftUIJoystick";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.4.1;
|
||||
minimumVersion = 1.5.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
4E80AA202CD705DD00029585 /* SDL */ = {
|
||||
4E0DED332D05695D00FEF007 /* SwiftUIJoystick */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4E80AA1F2CD705DD00029585 /* XCRemoteSwiftPackageReference "SwiftSDL2" */;
|
||||
productName = SDL;
|
||||
package = 4E0DED322D05695D00FEF007 /* XCRemoteSwiftPackageReference "SwiftUIJoystick" */;
|
||||
productName = SwiftUIJoystick;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"originHash" : "188cbfb6a5b52c41d3df0f972db675022d152bd432fecbf1b5a68f66e3956cb5",
|
||||
"originHash" : "0c2b07bd02731650383ed39e41433281bed19b2ed6cc7103e7daec7fb1d05e44",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "swiftsdl2",
|
||||
"identity" : "swiftuijoystick",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ctreffs/SwiftSDL2",
|
||||
"location" : "https://github.com/michael94ellis/SwiftUIJoystick",
|
||||
"state" : {
|
||||
"revision" : "30a2886bd68e43fc19ba29b63ffe230ac0e4db7a",
|
||||
"version" : "1.4.1"
|
||||
"revision" : "5bd303cdafb369a70a45c902538b42dd3c5f4d65",
|
||||
"version" : "1.5.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -8,6 +8,9 @@
|
||||
#ifndef RyujinxHeader
|
||||
#define RyujinxHeader
|
||||
|
||||
|
||||
#import "SDL.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
@ -2,92 +2,109 @@
|
||||
// VirtualController.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 28/11/2024.
|
||||
// Created by Stossy11 on 8/12/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GameController
|
||||
import UIKit
|
||||
|
||||
public var controllerCallback: (() -> Void)?
|
||||
class VirtualController {
|
||||
private var instanceID: SDL_JoystickID = -1
|
||||
private var controller: OpaquePointer?
|
||||
|
||||
var VirtualController: GCVirtualController!
|
||||
func showVirtualController() {
|
||||
let config = GCVirtualController.Configuration()
|
||||
|
||||
var controllere = [
|
||||
GCInputLeftThumbstick,
|
||||
GCInputButtonA,
|
||||
GCInputButtonB,
|
||||
GCInputButtonX,
|
||||
GCInputButtonY,
|
||||
// GCInputRightThumbstick,
|
||||
GCInputRightTrigger,
|
||||
GCInputLeftTrigger,
|
||||
GCInputLeftShoulder,
|
||||
GCInputRightShoulder
|
||||
]
|
||||
|
||||
if !UserDefaults.standard.bool(forKey: "RyuDemoControls") {
|
||||
|
||||
controllere.append(GCInputRightThumbstick)
|
||||
init() {
|
||||
setupVirtualController()
|
||||
}
|
||||
|
||||
config.elements = Set(controllere)
|
||||
|
||||
VirtualController = GCVirtualController(configuration: config)
|
||||
VirtualController.connect { err in
|
||||
print("controller connect: \(String(describing: err))")
|
||||
patchMakeKeyAndVisible()
|
||||
if let controllerCallback {
|
||||
controllerCallback()
|
||||
private func setupVirtualController() {
|
||||
// Initialize SDL if not already initialized
|
||||
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
|
||||
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
|
||||
}
|
||||
|
||||
// Create virtual controller
|
||||
instanceID = SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
|
||||
if instanceID < 0 {
|
||||
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
|
||||
return
|
||||
}
|
||||
|
||||
// Open a game controller for the virtual joystick
|
||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
||||
controller = SDL_GameControllerOpen(Int32(instanceID))
|
||||
|
||||
if controller == nil {
|
||||
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
|
||||
guard controller != nil else { return }
|
||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
||||
SDL_JoystickSetVirtualAxis(joystick, axis.rawValue, value)
|
||||
}
|
||||
|
||||
func thumbstickMoved(_ stick: ThumbstickType, x: Float, y: Float) {
|
||||
// Convert float values (-1.0 to 1.0) to SDL axis values (-32768 to 32767)
|
||||
let scaledX = Sint16(x * 32767.0)
|
||||
let scaledY = Sint16(y * 32767.0)
|
||||
|
||||
if stick == .right {
|
||||
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
|
||||
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTY.rawValue))
|
||||
} else { // ThumbstickType.left
|
||||
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTX.rawValue))
|
||||
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTY.rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
|
||||
guard controller != nil else { return }
|
||||
|
||||
print("Button: \(button.rawValue) {state: \(state)}")
|
||||
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
|
||||
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
|
||||
let value: Int = (state == 1) ? 32767 : 0
|
||||
updateAxisValue(value: Sint16(value), forAxis: axis)
|
||||
} else {
|
||||
let joystick = SDL_JoystickFromInstanceID(instanceID)
|
||||
SDL_JoystickSetVirtualButton(joystick, Int32(button.rawValue), state)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
if let controller = controller {
|
||||
SDL_GameControllerClose(controller)
|
||||
self.controller = nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
func waitforcontroller() {
|
||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
|
||||
|
||||
if let window = UIApplication.shared.windows.first {
|
||||
// Function to recursively search for GCControllerView
|
||||
func findGCControllerView(in view: UIView) -> UIView? {
|
||||
// Check if current view is GCControllerView
|
||||
if String(describing: type(of: view)) == "GCControllerView" {
|
||||
return view
|
||||
}
|
||||
|
||||
// Search through subviews
|
||||
for subview in view.subviews {
|
||||
if let found = findGCControllerView(in: subview) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if let gcControllerView = findGCControllerView(in: window) {
|
||||
// Found the GCControllerView
|
||||
print("Found GCControllerView:", gcControllerView)
|
||||
|
||||
if let theWindow = theWindow, (findGCControllerView(in: theWindow) == nil) {
|
||||
theWindow.addSubview(gcControllerView)
|
||||
|
||||
theWindow.bringSubviewToFront(gcControllerView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
enum VirtualControllerButton: Int {
|
||||
case B
|
||||
case A
|
||||
case Y
|
||||
case X
|
||||
case back
|
||||
case guide
|
||||
case start
|
||||
case leftStick
|
||||
case rightStick
|
||||
case leftShoulder
|
||||
case rightShoulder
|
||||
case dPadUp
|
||||
case dPadDown
|
||||
case dPadLeft
|
||||
case dPadRight
|
||||
case leftTrigger
|
||||
case rightTrigger
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
func reconnectVirtualController() {
|
||||
VirtualController.disconnect()
|
||||
DispatchQueue.main.async {
|
||||
VirtualController.connect { err in
|
||||
print("reconnected: err \(String(describing: err))")
|
||||
}
|
||||
}
|
||||
enum ThumbstickType: Int {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
|
||||
|
||||
|
52
src/MeloNX/MeloNX/Core/Swift/Controller/WaitforVC.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// VirtualController.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 28/11/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GameController
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
func waitforcontroller() {
|
||||
if let window = theWindow {
|
||||
|
||||
|
||||
|
||||
// Function to recursively search for GCControllerView
|
||||
func findGCControllerView(in view: UIView) -> UIView? {
|
||||
// Check if current view is GCControllerView
|
||||
if String(describing: type(of: view)) == "ControllerView" {
|
||||
return view
|
||||
}
|
||||
|
||||
// Search through subviews
|
||||
for subview in view.subviews {
|
||||
if let found = findGCControllerView(in: subview) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let controllerView = ControllerView()
|
||||
|
||||
let hostingController = UIHostingController(rootView: controllerView)
|
||||
|
||||
hostingController.view.frame = window.bounds // Set the frame of the SwiftUI view
|
||||
hostingController.view.backgroundColor = .clear
|
||||
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
if findGCControllerView(in: window) == nil {
|
||||
window.addSubview(hostingController.view)
|
||||
}
|
||||
|
||||
window.bringSubviewToFront(hostingController.view)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -19,15 +19,14 @@ extension UIWindow {
|
||||
}
|
||||
self.wdb_makeKeyAndVisible()
|
||||
theWindow = self
|
||||
if #available(iOS 15.0, *) {
|
||||
// reconnectVirtualController()
|
||||
}
|
||||
|
||||
|
||||
if UserDefaults.standard.bool(forKey: "isVirtualController") {
|
||||
if let window = theWindow {
|
||||
waitforcontroller()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -32,6 +32,8 @@ struct iOSNav<Content: View>: View {
|
||||
class Ryujinx {
|
||||
private var isRunning = false
|
||||
|
||||
let virtualController = VirtualController()
|
||||
|
||||
@Published var controllerMap: [Controller] = []
|
||||
|
||||
static let shared = Ryujinx()
|
||||
|
@ -12,7 +12,7 @@ struct MeloNXApp: App {
|
||||
|
||||
init() {
|
||||
DispatchQueue.main.async {
|
||||
drmcheck()
|
||||
// drmcheck()
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ struct MeloNXApp: App {
|
||||
func drmcheck() {
|
||||
if let deviceid = UIDevice.current.identifierForVendor?.uuidString, let base64device = deviceid.data(using: .utf8)?.base64EncodedString() {
|
||||
if let value = Bundle.main.infoDictionary?["MeloID"] as? String {
|
||||
if let url = URL(string: "https://a0a5-175-32-163-60.ngrok-free.app/auth/\(value)/\(base64device)") {
|
||||
if let url = URL(string: "https://950e-175-32-92-74.ngrok-free.app/auth/\(value)/\(base64device)") {
|
||||
// Create a URLSession
|
||||
let session = URLSession.shared
|
||||
|
||||
|
@ -6,10 +6,11 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SDL2
|
||||
// import SDL2
|
||||
import GameController
|
||||
import Darwin
|
||||
import UIKit
|
||||
// import SDL
|
||||
|
||||
struct MoltenVKSettings: Codable, Hashable {
|
||||
let string: String
|
||||
@ -27,9 +28,9 @@ struct ContentView: View {
|
||||
@State private var config: Ryujinx.Configuration
|
||||
@State private var settings: [MoltenVKSettings]
|
||||
@State private var isVirtualControllerActive: Bool = false
|
||||
@AppStorage("isVirtualController") var isVCA: Bool = true
|
||||
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
||||
@AppStorage("JIT") var isJITEnabled: Bool = false
|
||||
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
||||
|
||||
// MARK: - Initialization
|
||||
init() {
|
||||
@ -58,13 +59,6 @@ struct ContentView: View {
|
||||
mainMenuView
|
||||
}
|
||||
}
|
||||
.onChange(of: isVirtualControllerActive) { newValue in
|
||||
if newValue {
|
||||
createVirtualController()
|
||||
} else {
|
||||
destroyVirtualController()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View Components
|
||||
@ -79,7 +73,6 @@ struct ContentView: View {
|
||||
HStack {
|
||||
GameListView(startemu: $game)
|
||||
.onAppear {
|
||||
createVirtualController()
|
||||
refreshControllersList()
|
||||
}
|
||||
|
||||
@ -126,59 +119,42 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Controller Management
|
||||
private func createVirtualController() {
|
||||
let configuration = GCVirtualController.Configuration()
|
||||
configuration.elements = [
|
||||
/*
|
||||
GCInputLeftThumbstick,
|
||||
GCInputRightThumbstick,
|
||||
GCInputButtonA,
|
||||
GCInputButtonB,
|
||||
GCInputButtonX,
|
||||
GCInputButtonY,
|
||||
*/
|
||||
]
|
||||
|
||||
virtualController = GCVirtualController(configuration: configuration)
|
||||
virtualController?.connect()
|
||||
|
||||
}
|
||||
|
||||
private func destroyVirtualController() {
|
||||
virtualController?.disconnect()
|
||||
virtualController = nil
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO;
|
||||
private func initializeSDL() {
|
||||
DispatchQueue.main.async {
|
||||
setMoltenVKSettings()
|
||||
SDL_SetMainReady()
|
||||
SDL_iPhoneSetEventPump(SDL_TRUE)
|
||||
SDL_Init(SDL_INIT_VIDEO)
|
||||
}
|
||||
SDL_Init(SdlInitFlags)
|
||||
}
|
||||
|
||||
private func setupEmulation() {
|
||||
virtualController?.disconnect()
|
||||
patchMakeKeyAndVisible()
|
||||
|
||||
controllerCallback = {
|
||||
DispatchQueue.main.async {
|
||||
if (currentControllers.first(where: { $0 == onscreencontroller }) != nil) {
|
||||
|
||||
isVCA = true
|
||||
|
||||
start(displayid: 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
showVirtualController()
|
||||
} else {
|
||||
isVCA = false
|
||||
|
||||
start(displayid: 1)
|
||||
|
||||
|
||||
}
|
||||
// controllerCallback!()
|
||||
}
|
||||
|
||||
private func refreshControllersList() {
|
||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
|
||||
controllersList = Ryujinx.shared.getConnectedControllers()
|
||||
|
||||
if let onscreen = controllersList.first(where: { $0.name.hasPrefix("Apple")}) {
|
||||
if let onscreen = controllersList.first(where: { $0.name == "Virtual Controller" }) {
|
||||
self.onscreencontroller = onscreen
|
||||
}
|
||||
|
||||
|
267
src/MeloNX/MeloNX/Views/ControllerView/ControllerView.swift
Normal file
@ -0,0 +1,267 @@
|
||||
//
|
||||
// ControllerView.swift
|
||||
// Pomelo-V2
|
||||
//
|
||||
// Created by Stossy11 on 16/7/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import GameController
|
||||
import SwiftUIJoystick
|
||||
import CoreMotion
|
||||
|
||||
struct ControllerView: View {
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
HStack {
|
||||
VStack {
|
||||
ShoulderButtonsViewLeft()
|
||||
ZStack {
|
||||
Joystick()
|
||||
DPadView()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
VStack {
|
||||
ShoulderButtonsViewRight()
|
||||
ZStack {
|
||||
Joystick(iscool: true) // hope this works
|
||||
ABXYView()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
HStack {
|
||||
ButtonView(button: .start) // Adding the + button
|
||||
.padding(.horizontal, 40)
|
||||
ButtonView(button: .back) // Adding the - button
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, geometry.size.height / 3.2) // very broken
|
||||
}
|
||||
} else {
|
||||
// could be landscape
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
HStack {
|
||||
|
||||
// gotta fuckin add + and - now
|
||||
VStack {
|
||||
ShoulderButtonsViewLeft()
|
||||
ZStack {
|
||||
Joystick()
|
||||
DPadView()
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
// Spacer()
|
||||
VStack {
|
||||
// Spacer()
|
||||
ButtonView(button: .start) // Adding the + button
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
// Spacer()
|
||||
ButtonView(button: .back) // Adding the - button
|
||||
}
|
||||
// Spacer()
|
||||
}
|
||||
VStack {
|
||||
ShoulderButtonsViewRight()
|
||||
ZStack {
|
||||
Joystick(iscool: true) // hope this work s
|
||||
ABXYView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// .padding(.bottom, geometry.size.height / 11) // also extremally broken (
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ShoulderButtonsViewLeft: View {
|
||||
@State var width: CGFloat = 160
|
||||
@State var height: CGFloat = 20
|
||||
var body: some View {
|
||||
HStack {
|
||||
ButtonView(button: .leftTrigger)
|
||||
.padding(.horizontal)
|
||||
ButtonView(button: .leftShoulder)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(width: width, height: height)
|
||||
.onAppear() {
|
||||
if UIDevice.current.systemName.contains("iPadOS") {
|
||||
width *= 1.2
|
||||
height *= 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ShoulderButtonsViewRight: View {
|
||||
@State var width: CGFloat = 160
|
||||
@State var height: CGFloat = 20
|
||||
var body: some View {
|
||||
HStack {
|
||||
ButtonView(button: .rightShoulder)
|
||||
.padding(.horizontal)
|
||||
ButtonView(button: .rightTrigger)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(width: width, height: height)
|
||||
.onAppear() {
|
||||
if UIDevice.current.systemName.contains("iPadOS") {
|
||||
width *= 1.2
|
||||
height *= 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DPadView: View {
|
||||
@State var size: CGFloat = 145
|
||||
var body: some View {
|
||||
VStack {
|
||||
ButtonView(button: .dPadUp)
|
||||
HStack {
|
||||
ButtonView(button: .dPadLeft)
|
||||
Spacer(minLength: 20)
|
||||
ButtonView(button: .dPadRight)
|
||||
}
|
||||
ButtonView(button: .dPadDown)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
.onAppear() {
|
||||
if UIDevice.current.systemName.contains("iPadOS") {
|
||||
size *= 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ABXYView: View {
|
||||
@State var size: CGFloat = 145
|
||||
var body: some View {
|
||||
VStack {
|
||||
ButtonView(button: .X)
|
||||
HStack {
|
||||
ButtonView(button: .Y)
|
||||
Spacer(minLength: 20)
|
||||
ButtonView(button: .A)
|
||||
}
|
||||
ButtonView(button: .B)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
.onAppear() {
|
||||
if UIDevice.current.systemName.contains("iPadOS") {
|
||||
size *= 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ButtonView: View {
|
||||
var button: VirtualControllerButton
|
||||
@State var width: CGFloat = 45
|
||||
@State var height: CGFloat = 45
|
||||
@State var isPressed = false
|
||||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: buttonText)
|
||||
.resizable()
|
||||
.frame(width: width, height: height)
|
||||
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
|
||||
.opacity(isPressed ? 0.4 : 0.7)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in
|
||||
if !self.isPressed {
|
||||
self.isPressed = true
|
||||
Ryujinx.shared.virtualController.setButtonState(1, for: button)
|
||||
Haptics.shared.play(.heavy)
|
||||
}
|
||||
}
|
||||
.onEnded { _ in
|
||||
self.isPressed = false
|
||||
Ryujinx.shared.virtualController.setButtonState(0, for: button)
|
||||
}
|
||||
)
|
||||
.onAppear() {
|
||||
if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder {
|
||||
width = 65
|
||||
}
|
||||
|
||||
|
||||
if button == .back || button == .start || button == .guide {
|
||||
width = 35
|
||||
height = 35
|
||||
}
|
||||
|
||||
if UIDevice.current.systemName.contains("iPadOS") {
|
||||
width *= 1.2
|
||||
height *= 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private var buttonText: String {
|
||||
switch button {
|
||||
case .A:
|
||||
return "a.circle.fill"
|
||||
case .B:
|
||||
return "b.circle.fill"
|
||||
case .X:
|
||||
return "x.circle.fill"
|
||||
case .Y:
|
||||
return "y.circle.fill"
|
||||
case .dPadUp:
|
||||
return "arrowtriangle.up.circle.fill"
|
||||
case .dPadDown:
|
||||
return "arrowtriangle.down.circle.fill"
|
||||
case .dPadLeft:
|
||||
return "arrowtriangle.left.circle.fill"
|
||||
case .dPadRight:
|
||||
return "arrowtriangle.right.circle.fill"
|
||||
case .leftTrigger:
|
||||
return"zl.rectangle.roundedtop.fill"
|
||||
case .rightTrigger:
|
||||
return "zr.rectangle.roundedtop.fill"
|
||||
case .leftShoulder:
|
||||
return "l.rectangle.roundedbottom.fill"
|
||||
case .rightShoulder:
|
||||
return "r.rectangle.roundedbottom.fill"
|
||||
case .start:
|
||||
return "plus.circle.fill" // System symbol for +
|
||||
case .back:
|
||||
return "minus.circle.fill" // System symbol for -
|
||||
case .guide:
|
||||
return "house.circle.fill"
|
||||
// This should be all the cases
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
27
src/MeloNX/MeloNX/Views/ControllerView/Haptics/Haptics.swift
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// Haptics.swift
|
||||
// Pomelo
|
||||
//
|
||||
// Created by Stossy11 on 11/9/2024.
|
||||
// Copyright © 2024 Stossy11. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class Haptics {
|
||||
static let shared = Haptics()
|
||||
|
||||
private init() { }
|
||||
|
||||
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||
print("haptics")
|
||||
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
|
||||
}
|
||||
|
||||
func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) {
|
||||
UINotificationFeedbackGenerator().notificationOccurred(feedbackType)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickBase@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickBase@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickBase@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 8.6 KiB |
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickHandle@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickHandle@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickHandle@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 4.4 KiB |
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickBaseCustom@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickBaseCustom@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickBaseCustom@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 6.4 KiB |
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickHandleCustom@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickHandleCustom@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "JoyStickHandleCustom@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 54 KiB |
@ -0,0 +1,53 @@
|
||||
//
|
||||
// JoystickView.swift
|
||||
// Pomelo
|
||||
//
|
||||
// Created by Stossy11 on 30/9/2024.
|
||||
// Copyright © 2024 Stossy11. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUIJoystick
|
||||
|
||||
public struct Joystick: View {
|
||||
@State var iscool: Bool? = nil
|
||||
|
||||
@ObservedObject public var joystickMonitor = JoystickMonitor()
|
||||
var dragDiameter: CGFloat {
|
||||
var selfs = CGFloat(160)
|
||||
if UIDevice.current.systemName.contains("iPadOS") {
|
||||
return selfs * 1.2
|
||||
}
|
||||
return selfs
|
||||
}
|
||||
private let shape: JoystickShape = .circle
|
||||
|
||||
public var body: some View {
|
||||
VStack{
|
||||
JoystickBuilder(
|
||||
monitor: self.joystickMonitor,
|
||||
width: self.dragDiameter,
|
||||
shape: .circle,
|
||||
background: {
|
||||
Text("")
|
||||
.hidden()
|
||||
},
|
||||
foreground: {
|
||||
Circle().fill(Color.gray)
|
||||
.opacity(0.7)
|
||||
},
|
||||
locksInPlace: false)
|
||||
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
|
||||
let scaledX = Float(newValue.x)
|
||||
let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/
|
||||
print("Joystick Position: (\(scaledX), \(scaledY))")
|
||||
|
||||
if iscool != nil {
|
||||
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: scaledX, y: scaledY)
|
||||
} else {
|
||||
Ryujinx.shared.virtualController.thumbstickMoved(.left, x: scaledX, y: scaledY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|