New 2.6 changes

This commit is contained in:
stossy11 2024-11-17 15:06:58 +11:00
parent 8dc2995ea8
commit c8ea75aa07
19 changed files with 681 additions and 135 deletions

View File

@ -82,6 +82,8 @@
4EA0AA602CB6845700B51C64 /* JITEnabler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EA0AA5F2CB6845700B51C64 /* JITEnabler.m */; };
4EC662B02CAA1229000DBC5F /* SwiftUIJoystick in Frameworks */ = {isa = PBXBuildFile; productRef = 4EC662AF2CAA1229000DBC5F /* SwiftUIJoystick */; };
4EC662B42CAA1257000DBC5F /* JoystickView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC662B32CAA1254000DBC5F /* JoystickView.swift */; };
4EDA92352CE830B10024A064 /* DeviceMemory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDA92342CE830AB0024A064 /* DeviceMemory.swift */; };
4EDA92BF2CE981220024A064 /* PomeloRPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDA92BE2CE981200024A064 /* PomeloRPC.swift */; };
4EE462B52CB548E800BF268E /* MTLViewExtentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE462B42CB548E200BF268E /* MTLViewExtentions.swift */; };
4EE462B92CB54F2100BF268E /* ScreenShotListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE462B82CB54F1800BF268E /* ScreenShotListView.swift */; };
4EE462BC2CB5552900BF268E /* MotionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE462BB2CB5552900BF268E /* MotionManager.swift */; };
@ -200,6 +202,8 @@
4EA0AA592CB6785B00B51C64 /* Sudachi.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Sudachi.xcodeproj; path = Sudachi/Sudachi.xcodeproj; sourceTree = "<group>"; };
4EA0AA5F2CB6845700B51C64 /* JITEnabler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JITEnabler.m; sourceTree = "<group>"; };
4EC662B32CAA1254000DBC5F /* JoystickView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoystickView.swift; sourceTree = "<group>"; };
4EDA92342CE830AB0024A064 /* DeviceMemory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMemory.swift; sourceTree = "<group>"; };
4EDA92BE2CE981200024A064 /* PomeloRPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PomeloRPC.swift; sourceTree = "<group>"; };
4EE462B42CB548E200BF268E /* MTLViewExtentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MTLViewExtentions.swift; sourceTree = "<group>"; };
4EE462B82CB54F1800BF268E /* ScreenShotListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShotListView.swift; sourceTree = "<group>"; };
4EE462BB2CB5552900BF268E /* MotionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionManager.swift; sourceTree = "<group>"; };
@ -385,6 +389,8 @@
384F188C2C1DCB4F0073375C /* Pomelo */ = {
isa = PBXGroup;
children = (
4EDA92BD2CE9811A0024A064 /* PomeloRPC */,
4EDA92322CE830A50024A064 /* CurrentDevice */,
4EE4734A2CDECAA3000A010C /* Navigation */,
4EE9B1EA2CC4700F008FA07B /* Intents */,
4EE462B62CB54F0400BF268E /* ScreenshotManager */,
@ -595,6 +601,22 @@
path = Joystick;
sourceTree = "<group>";
};
4EDA92322CE830A50024A064 /* CurrentDevice */ = {
isa = PBXGroup;
children = (
4EDA92342CE830AB0024A064 /* DeviceMemory.swift */,
);
path = CurrentDevice;
sourceTree = "<group>";
};
4EDA92BD2CE9811A0024A064 /* PomeloRPC */ = {
isa = PBXGroup;
children = (
4EDA92BE2CE981200024A064 /* PomeloRPC.swift */,
);
path = PomeloRPC;
sourceTree = "<group>";
};
4EE462B32CB548D800BF268E /* Extentions */ = {
isa = PBXGroup;
children = (
@ -788,6 +810,7 @@
4EE593FF2C5FA1D1000939C4 /* AppIconProvider.swift in Sources */,
386F6EEC2C42E0C800C62EBE /* PomeloApp.swift in Sources */,
4E1687CE2CC7A27000485EDB /* SudachiMetalView.swift in Sources */,
4EDA92352CE830B10024A064 /* DeviceMemory.swift in Sources */,
4E5855E32CB6770F00047C2A /* AskForJIT.swift in Sources */,
384194702C4E540500396613 /* SudachiEmulationHandler.swift in Sources */,
38020F562C43A02100029E9A /* BootOSView.swift in Sources */,
@ -811,6 +834,7 @@
4EE462BF2CB5708800BF268E /* Zoomable.swift in Sources */,
3841946C2C4E4D2B00396613 /* ControllerView.swift in Sources */,
38B7FE022C7610A600D274FB /* AirPlay.swift in Sources */,
4EDA92BF2CE981220024A064 /* PomeloRPC.swift in Sources */,
3841946D2C4E4D2B00396613 /* SudachiEmulationView.swift in Sources */,
386F6EEA2C42E0B900C62EBE /* SudachiGame.swift in Sources */,
4EE462C42CB576F400BF268E /* BottomMenuView.swift in Sources */,
@ -978,7 +1002,7 @@
INFOPLIST_KEY_NSAppleMusicUsageDescription = "We need access to use Sign in with Apple.";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Pomelo needs local network access for P2P Multiplayer ";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Pomelo needs Microphone so the games can access it.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Pomelo needs Microphone so the games can access it. (Don't ask me, SDL is weird. i think its trying to put audio through the mic)";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;
@ -1045,7 +1069,7 @@
INFOPLIST_KEY_NSAppleMusicUsageDescription = "We need access to use Sign in with Apple.";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Pomelo needs local network access for P2P Multiplayer ";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Pomelo needs Microphone so the games can access it.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Pomelo needs Microphone so the games can access it. (Don't ask me, SDL is weird. i think its trying to put audio through the mic)";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;

View File

@ -0,0 +1,28 @@
//
// DeviceMemory.swift
// Pomelo
//
// Created by Stossy11 on 16/11/2024.
// Copyright © 2024 Stossy11. All rights reserved.
//
import UIKit
import SwiftUI
import Foundation
enum DeviceMemory {
/// Check if device has 8GB or more RAM
static var has8GBOrMore: Bool {
#if targetEnvironment(simulator)
return ProcessInfo.processInfo.physicalMemory >= 7 * 1024 * 1024 * 1024 // 8GB in bytes
#else
return ProcessInfo.processInfo.physicalMemory >= 7 * 1024 * 1024 * 1024 // 8GB in bytes
#endif
}
/// Get total RAM in GB (rounded)
static var totalRAM: Int {
Int(ProcessInfo.processInfo.physicalMemory / 1024 / 1024 / 1024) + 1
}
}

View File

@ -174,7 +174,7 @@ struct ControllerView: View {
motion.valueChangedHandler = { motion in
// Get current time
let currentTimestamp = Date().timeIntervalSince1970
// let deltaTimestamp = Int32((currentTimestamp - lastTimestamp) * 1000) // Difference in milliseconds
let deltaTimestamp = Int32((currentTimestamp - lastTimestamp) * 1000) // Difference in milliseconds
// Update last timestamp
lastTimestamp = currentTimestamp
@ -192,7 +192,7 @@ struct ControllerView: View {
// print("\(gyroX), \(gyroY), \(gyroZ), \(accelX), \(accelY), \(accelZ)")
// Call your gyroMoved function with the motion data
sudachi.gyroMoved(x: Float(gyroX), y: Float(gyroY), z: Float(gyroZ), accelX: Float(accelX), accelY: Float(accelY), accelZ: Float(accelZ), controllerId: Int32(controllerId), deltaTimestamp: Int32(lastTimestamp))
sudachi.gyroMoved(x: Float(gyroX), y: Float(gyroY), z: Float(gyroZ), accelX: Float(accelX), accelY: Float(accelY), accelZ: Float(accelZ), controllerId: Int32(controllerId), deltaTimestamp: Int32(deltaTimestamp))
}
}
@ -313,6 +313,8 @@ struct OnScreenController: View {
}
struct ShoulderButtonsViewLeft: View {
@State var width: CGFloat = 160
@State var height: CGFloat = 20
var body: some View {
HStack {
ButtonView(button: .triggerZL)
@ -320,11 +322,19 @@ struct ShoulderButtonsViewLeft: View {
ButtonView(button: .triggerL)
.padding(.horizontal)
}
.frame(width: 160, height: 20)
.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: .triggerR)
@ -332,12 +342,18 @@ struct ShoulderButtonsViewRight: View {
ButtonView(button: .triggerZR)
.padding(.horizontal)
}
.frame(width: 160, height: 20)
.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: .directionalPadUp)
@ -349,11 +365,17 @@ struct DPadView: View {
ButtonView(button: .directionalPadDown)
.padding(.horizontal)
}
.frame(width: 145, height: 145)
.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)
@ -365,7 +387,12 @@ struct ABXYView: View {
ButtonView(button: .B)
.padding(.horizontal)
}
.frame(width: 145, height: 145)
.frame(width: size, height: size)
.onAppear() {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
}
}
}
@ -430,6 +457,11 @@ struct ButtonView: View {
width = 35
height = 35
}
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
}
}

View File

@ -23,7 +23,13 @@ public struct Joystick: View {
let sudachi = Sudachi.shared
@ObservedObject public var joystickMonitor = JoystickMonitor()
private let dragDiameter: CGFloat = 160
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 {

View File

@ -10,6 +10,7 @@ import Sudachi
import Metal
import Foundation
class SudachiEmulationViewModel: ObservableObject {
@Published var isShowingCustomButton = true
@State var should = false
@ -24,6 +25,12 @@ class SudachiEmulationViewModel: ObservableObject {
var doesneedresources = false
@State var iscustom: Bool = false
private var gameTimer: Timer?
private var elapsedTime: TimeInterval = 0
private var startTime: Date?
private var rpctimer: Timer?
init(game: PomeloGame?) {
self.device = MTLCreateSystemDefaultDevice()
self.sudachiGame = game
@ -38,22 +45,26 @@ class SudachiEmulationViewModel: ObservableObject {
iscustom = ((sudachiGame?.fileURL.startAccessingSecurityScopedResource()) != nil)
print("Is outside URL? \(iscustom ? "Yes" : "No")")
print("is outside url? \(iscustom ? "Yes" : "no")")
// startGameTimer() // Start the timer when the game starts
DispatchQueue.global(qos: .userInteractive).async { [self] in
if let sudachiGame = self.sudachiGame {
if sudachiGame.fileURL == URL(string: "BootMii") {
self.sudachi.bootMii()
} else {
self.sudachi.insert(game: sudachiGame.fileURL)
}
} else {
self.sudachi.bootOS()
}
}
if UserDefaults.standard.bool(forKey: "pomeloRPC") {
startRPCTimer()
}
thread = .init(block: self.step)
thread.name = "Pomelo"
thread.qualityOfService = .userInteractive
@ -74,6 +85,9 @@ class SudachiEmulationViewModel: ObservableObject {
private func stopEmulation() {
if isRunning {
isRunning = false
stopGameTimer() // Stop the timer and save elapsed time
rpctimer?.invalidate()
rpctimer = nil
sudachi.exit()
thread.cancel()
if iscustom {
@ -82,6 +96,77 @@ class SudachiEmulationViewModel: ObservableObject {
}
}
public func startRPCTimer() {
if let sudachiGame {
startRPC(game: sudachiGame)
} else {
startRPC(game: PomeloGame(programid: 0, ishomebrew: false, developer: "Nintendo", fileURL: URL(string: "/")!, imageData: Data(), title: "Home Menu"))
}
rpctimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.runEveryFiveSeconds()
}
}
private func runEveryFiveSeconds() {
rpcHeartBeat()
}
public func startGameTimer() {
startTime = Date()
gameTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
if let startTime = self.startTime {
self.elapsedTime = Date().timeIntervalSince(startTime)
saveElapsedTimeToJSON()
}
}
}
private func stopGameTimer() {
gameTimer?.invalidate()
gameTimer = nil
saveElapsedTimeToJSON()
}
private func saveElapsedTimeToJSON() {
var newEntry: [String: TimeInterval]
if let game = sudachiGame {
newEntry = [game.title: elapsedTime]
} else {
newEntry = ["Home Menu": elapsedTime]
}
let fileURL = getDocumentsDirectory().appendingPathComponent("PomeloInfo.json")
do {
var existingData: [String: TimeInterval] = [:]
// Check if the file exists
if FileManager.default.fileExists(atPath: fileURL.path) {
// Read existing data
let data = try Data(contentsOf: fileURL)
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: TimeInterval] {
existingData = json
}
}
// Merge new data with existing data
for (key, value) in newEntry {
existingData[key] = value
}
// Write updated data back to the file
let updatedJsonData = try JSONSerialization.data(withJSONObject: existingData, options: .prettyPrinted)
try updatedJsonData.write(to: fileURL, options: .atomic)
print("Successfully saved elapsed time to \(fileURL.path)")
} catch {
print("Error saving elapsed time: \(error.localizedDescription)")
}
}
func handleOrientationChange() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
@ -105,3 +190,7 @@ class SudachiEmulationViewModel: ObservableObject {
}
}
}
func getDocumentsDirectory() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
}

View File

@ -24,6 +24,8 @@ struct SudachiEmulationView: View {
@State private var thread: Thread!
@State var uiTabBarController: UITabBarController?
@State private var isFirstFrameShown = false
@State private var switched = false
@State private var wasairplay = false
@State private var timer: Timer?
@Environment(\.scenePhase) var scenePhase
@AppStorage("isairplay") private var isairplay: Bool = true
@ -31,6 +33,10 @@ struct SudachiEmulationView: View {
@AppStorage("showfunnibackground") private var showbackground: Bool = false
let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
@Environment(\.presentationMode) var presentationMode
@State private var isInBackground = false
@AppStorage("disableMainScreen") var disableMainScreen: Bool = false
@AppStorage("disableElapsedTime") var disableElapsedTime: Bool = false
init(game: PomeloGame?) {
_game = State(wrappedValue: game)
@ -38,83 +44,103 @@ struct SudachiEmulationView: View {
}
var body: some View {
ZStack {
if #available(iOS 18.0, *), showbackground {
BackgroundView()
.ignoresSafeArea(.all)
if (!disableMainScreen && !isairplay) {
if #available(iOS 18.0, *), showbackground {
BackgroundView()
.ignoresSafeArea(.all)
}
if !isairplay, !switched {
SudachiMetalView(viewModel: viewModel, mtkview: $mtkview, device: $device, airplaylater: false)
}
}
if !isairplay {
SudachiMetalView(viewModel: viewModel, mtkview: $mtkview, device: $device)
if disableMainScreen, isairplay {
ControllerView()
.hidden()
} else {
ControllerView()
}
ControllerView()
if ShowMenuButton {
VStack {
HStack {
Spacer()
Menu {
Button {
if let metalView = mtkview {
print("button pressed")
let lastDrawableDisplayed = metalView.currentDrawable?.texture
if let imageRef = lastDrawableDisplayed?.toImage() {
let uiImage: UIImage = UIImage.init(cgImage: imageRef)
if (!disableMainScreen && !isairplay) {
if ShowMenuButton {
VStack {
HStack {
Spacer()
Menu {
Button {
if let metalView = mtkview {
print("button pressed")
if let pngData = uiImage.pngData() {
// Define the path to save the PNG file
if let game = self.game {
let fileURL = documentsDir.appendingPathComponent("screenshots/\(game.title)-(\(Date())).png")
print(Date())
do {
// Write the PNG data to the file
try pngData.write(to: fileURL)
print("Image saved to: \(fileURL.path)")
} catch {
print("Error saving PNG: \(error)")
let lastDrawableDisplayed = metalView.currentDrawable?.texture
if let imageRef = lastDrawableDisplayed?.toImage() {
let uiImage: UIImage = UIImage.init(cgImage: imageRef)
if let pngData = uiImage.pngData() {
// Define the path to save the PNG file
if let game = self.game {
let fileURL = documentsDir.appendingPathComponent("screenshots/\(game.title)-(\(Date())).png")
print(Date())
do {
// Write the PNG data to the file
try pngData.write(to: fileURL)
print("Image saved to: \(fileURL.path)")
} catch {
print("Error saving PNG: \(error)")
}
} else {
let fileURL = documentsDir.appendingPathComponent("screenshots/Home_Menu-(\(Date())).png")
do {
// Write the PNG data to the file
try pngData.write(to: fileURL)
print("Image saved to: \(fileURL.path)")
} catch {
print("Error saving PNG: \(error)")
}
}
} else {
let fileURL = documentsDir.appendingPathComponent("screenshots/Home_Menu-(\(Date())).png")
do {
// Write the PNG data to the file
try pngData.write(to: fileURL)
print("Image saved to: \(fileURL.path)")
} catch {
print("Error saving PNG: \(error)")
}
print("Failed to convert UIImage to PNG data")
}
} else {
print("Failed to convert UIImage to PNG data")
}
}
} label: {
Text("Take Screenshot")
.font(.title)
.padding()
}
Button {
viewModel.customButtonTapped()
presentationMode.wrappedValue.dismiss()
} label: {
Text("Exit (Unstable)")
}
} label: {
Text("Take Screenshot")
.font(.title)
.padding()
}
Button {
viewModel.customButtonTapped()
presentationMode.wrappedValue.dismiss()
} label: {
Text("Exit (Unstable)")
Image(systemName: "ellipsis.circle.fill")
.resizable()
.frame(width: 45, height: 45)
.foregroundColor(Color.gray)
}
} label: {
Image(systemName: "ellipsis.circle.fill")
.resizable()
.frame(width: 45, height: 45)
.foregroundColor(Color.gray)
}
Spacer()
}
Spacer()
}
}
if (disableMainScreen && isairplay) {
Color.black
.edgesIgnoringSafeArea(.all)
}
}
.overlay(
// Loading screen overlay on top of MetalView
@ -129,37 +155,54 @@ struct SudachiEmulationView: View {
UIApplication.shared.isIdleTimerDisabled = true
print("AirPlay + \(Air.shared.connected)")
print(disableMainScreen)
print(isairplay)
// Your existing variables
isairplay = Air.shared.connected
DispatchQueue.main.async {
Air.connection({ fun in
print("AirPlay + \(fun)")
isairplay = fun
})
if isairplay {
Air.play(AnyView(SudachiMetalView(viewModel: viewModel, mtkview: $mtkview, device: $device)))
}
}
// Set up initial AirPlay view
airplayafter(airplay: false)
if !isairplay {
startPollingFirstFrameShowed()
}
startPollingFirstFrameShowed()
}
.onDisappear {
if !isairplay {
stopPollingFirstFrameShowed()
}
stopPollingFirstFrameShowed()
UIApplication.shared.isIdleTimerDisabled = false
uiTabBarController?.tabBar.isHidden = false
}
.navigationBarBackButtonHidden(true)
}
private func airplayafter(airplay: Bool) {
isairplay = Air.shared.connected
// Set up initial AirPlay view
if isairplay, !isFirstFrameShown {
let airplayView = AnyView(SudachiMetalView(
viewModel: viewModel,
mtkview: $mtkview,
device: $device,
airplaylater: airplay
))
DispatchQueue.main.async {
Air.play(airplayView)
}
}
}
private func startPollingFirstFrameShowed() {
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
if sudachi.FirstFrameShowed() {
withAnimation {
isFirstFrameShown = true
}
if !disableElapsedTime {
viewModel.startGameTimer()
}
stopPollingFirstFrameShowed()
}
}

View File

@ -17,15 +17,19 @@ struct SudachiMetalView: View {
@Binding var mtkview: MTKView?
@Binding var device: MTLDevice?
@AppStorage("isairplay") private var isairplay: Bool = true
@State var airplaylater: Bool
var body: some View {
if #available(iOS 16.0, *) {
MetalView(device: device) { view in
DispatchQueue.main.async {
if let metalView = view as? MTKView {
mtkview = metalView
viewModel.configureSudachi(with: metalView)
} else {
print("Error: view is not of type MTKView")
if !airplaylater {
mtkview = metalView
viewModel.configureSudachi(with: metalView)
} else {
let sudachi = Sudachi.shared
sudachi.configure(layer: metalView.layer as! CAMetalLayer, with: metalView.frame.size)
}
}
}
}
@ -40,10 +44,14 @@ struct SudachiMetalView: View {
MetalView(device: device) { view in
DispatchQueue.main.async {
if let metalView = view as? MTKView {
mtkview = metalView
viewModel.configureSudachi(with: metalView)
} else {
print("Error: view is not of type MTKView")
if !airplaylater {
mtkview = metalView
viewModel.configureSudachi(with: metalView)
} else {
let sudachi = Sudachi.shared
sudachi.configure(layer: metalView.layer as! CAMetalLayer, with: metalView.frame.size)
}
}
}
}

View File

@ -53,14 +53,18 @@ class SudachiScreenView: UIView {
return
}
print("Location: \(touch.location(in: primaryScreen))")
sudachi.touchBegan(at: touch.location(in: primaryScreen), for: 0)
if !userDefaults.bool(forKey: "disabletouch") {
print("Location: \(touch.location(in: primaryScreen))")
sudachi.touchBegan(at: touch.location(in: primaryScreen), for: 0)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
print("Touch Ended")
sudachi.touchEnded(for: 0)
if !userDefaults.bool(forKey: "disabletouch") {
print("Touch Ended")
sudachi.touchEnded(for: 0)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
@ -69,15 +73,22 @@ class SudachiScreenView: UIView {
return
}
let location = touch.location(in: primaryScreen)
print("Location Moved: \(location)")
sudachi.touchMoved(at: location, for: 0)
if !userDefaults.bool(forKey: "disabletouch") {
let location = touch.location(in: primaryScreen)
print("Location Moved: \(location)")
sudachi.touchMoved(at: location, for: 0)
}
}
func setupSudachiScreen2() {
primaryScreen = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice())
primaryScreen.translatesAutoresizingMaskIntoConstraints = false
primaryScreen.clipsToBounds = true
if userDefaults.bool(forKey: "isairplay") {
primaryScreen.layer.backgroundColor = UIColor.black.cgColor
} else {
primaryScreen.layer.backgroundColor = UIColor.secondarySystemBackground.cgColor
}
addSubview(primaryScreen)
fullscreenconstraints = [

View File

@ -7,7 +7,6 @@ __attribute__((constructor)) static void entry(int argc, char **argv)
double systemVersion = [[[UIDevice currentDevice] systemVersion] doubleValue];
if (isJITEnabled()) {
NSLog(@"yippee");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
@ -20,10 +19,10 @@ __attribute__((constructor)) static void entry(int argc, char **argv)
[defaults synchronize]; // Ensure the value is saved immediately
}
if (!getEntitlementValue(@"com.apple.developer.kernel.increased-memory-limit")) {
NSLog(@"Entitlement Does Not Exist");
if (getEntitlementValue(@"com.apple.developer.kernel.increased-memory-limit")) {
NSLog(@"Entitlement Does Exist");
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:@"entitlementNotExists"];
[defaults setBool:YES forKey:@"entitlementExists"];
[defaults synchronize]; // Ensure the value is saved immediately
}

View File

@ -17,6 +17,8 @@ struct GameGridView: View {
@State var selectedGame: PomeloGame?
@State var alertMessage: Alert? = nil
@State var alapsedtime: [String: TimeInterval] = loadElapsedTimeFromJSON()
var body: some View {
let filteredGames = core.games.filter { game in
return searchText.isEmpty || game.title.localizedCaseInsensitiveContains(searchText)
@ -28,14 +30,47 @@ struct GameGridView: View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200))], spacing: 2) {
ForEach(0..<filteredGames.count, id: \.self) { index in
let game = filteredGames[index] // Use filteredGames here
var gametime: TimeInterval?
NavigationLink(destination: SudachiEmulationView(game: game)) {
GameIconView(game: game, selectedGame: $selectedGame)
.frame(maxWidth: 200, minHeight: 250)
}
.onAppear {
selectedGame = filteredGames.first
if let game = alapsedtime.first(where: { $0.key == game.title }) {
gametime = game.value
}
}
.contextMenu {
var hourminssecs: Int = 0
if let gametime {
if hourminssecs == 0 {
Text("Time Elapsed: \(String(gametime / 3600))")
.onTapGesture {
hourminssecs = 1
}
} else if hourminssecs == 1 {
Text("Time Elapsed: \(String(gametime / 60))")
.onTapGesture {
hourminssecs = 2
}
} else if hourminssecs == 2 {
Text("Time Elapsed: \(String(gametime))")
.onTapGesture {
hourminssecs = 0
}
}
} else {
Text("Time Elapsed: 0")
}
Button(action: {
do {
try LibraryManager.shared.removerom(filteredGames[index])
@ -76,6 +111,7 @@ struct GameGridView: View {
}
}
}
// urlgame = core.games.first(where: { String(describing: $0.programid) == text })
func refreshcore() {
do {
@ -86,3 +122,25 @@ struct GameGridView: View {
}
}
}
func loadElapsedTimeFromJSON() -> [String: TimeInterval] {
let fileURL = getDocumentsDirectory().appendingPathComponent("PomeloInfo.json")
do {
// Check if the file exists
if FileManager.default.fileExists(atPath: fileURL.path) {
// Read data from the file
let data = try Data(contentsOf: fileURL)
// Parse the JSON data into a dictionary
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: TimeInterval] {
return json
}
}
} catch {
print("Error loading elapsed time: \(error.localizedDescription)")
}
// Return an empty dictionary if the file doesn't exist or an error occurs
return [:]
}

View File

@ -13,22 +13,57 @@ import UniformTypeIdentifiers
import Sudachi
struct GameListView: View {
// let games: [PomeloGame]
@Binding var core: Core
@Binding var selectedGame: PomeloGame?
@State private var alapsedtime: [String: TimeInterval] = [:] // Loaded later
@State private var hourminssecs: Int = 0 // Tracks display format for elapsed time
@State var timeString: String = ""
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(core.games.prefix(12)) { game in
GameIconView(game: game, selectedGame: $selectedGame)
.onAppear {
if let gametime = alapsedtime[game.title] {
switch hourminssecs {
case 0:
timeString = String(format: "%.2f hours", gametime / 3600)
case 1:
timeString = String(format: "%.2f minutes", gametime / 60)
case 2:
timeString = String(format: "%.2f seconds", gametime)
default:
timeString = "0"
}
}
}
.contextMenu {
if let gametime = alapsedtime[game.title] {
Button {
hourminssecs = (hourminssecs + 1) % 3
switch hourminssecs {
case 0:
timeString = String(format: "%.2f hours", gametime / 3600)
case 1:
timeString = String(format: "%.2f minutes", gametime / 60)
case 2:
timeString = String(format: "%.2f seconds", gametime)
default:
timeString = "0"
}
} label: {
Text("Time Elapsed: \(timeString)")
}
} else {
Text("Time Elapsed: 0")
}
Button {
UIPasteboard.general.string = String(describing: game.programid)
} label: {
Text("Game ID: \(String(describing: game.programid))")
}
Button {
saveImageToIconsFolder(gameImageData: game.imageData, imageName: game.title)
} label: {
@ -36,7 +71,7 @@ struct GameListView: View {
}
}
}
if core.games.count > 12 {
NavigationLink {
GameGridView(core: core)
@ -49,16 +84,20 @@ struct GameListView: View {
.foregroundColor(.white)
.frame(width: 70, height: 70)
}
.foregroundColor(Color.init(uiColor: .darkGray))
.foregroundColor(Color(uiColor: .darkGray))
}
}
}
}
.onAppear {
loadElapsedTime()
selectedGame = core.games.first
}
}
// Function to load elapsed time
private func loadElapsedTime() {
alapsedtime = loadElapsedTimeFromJSON()
}
func saveImageToIconsFolder(gameImageData: Data, imageName: String) {

View File

@ -30,7 +30,7 @@ struct TopBarView: View {
HStack {
NavigationLink {
SudachiEmulationView(game: PomeloGame(programid: 0, ishomebrew: false, developer: "", fileURL: URL(string: "BootMii")!, imageData: Data(), title: "BootOS"))
SudachiEmulationView(game: PomeloGame(programid: 0, ishomebrew: false, developer: "", fileURL: URL(string: "BootMii")!, imageData: Data(), title: "Mii Maker"))
} label: {
Image(systemName: "person.crop.circle.fill")
.resizable()

View File

@ -7,6 +7,7 @@
import SwiftUI
import AppIntents
infix operator --: LogicalDisjunctionPrecedence
func --(lhs: Bool, rhs: Bool) -> Bool {
@ -17,22 +18,7 @@ func --(lhs: Bool, rhs: Bool) -> Bool {
struct PomeloApp: App {
init() {
// setenv("MVK_DEBUG", "1", 1)
// setenv("MVK_CONFIG_TRACE_VULKAN_CALLS", "1", 1)
// setenv("MVK_CONFIG_LOG_LEVEL", "4", 1)
// setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "3", 1)
// setenv("MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", "1024", 1)
setenv("MVK_USE_METAL_PRIVATE_API", "1", 1)
setenv("MVK_CONFIG_RESUME_LOST_DEVICE", "1", 1)
//MVK_CONFIG_RESUME_LOST_DEVICE
setenv("MVK_CONFIG_USE_METAL_PRIVATE_API", "1", 1)
setMoltenVKSettings()
}
var body: some Scene {
@ -45,5 +31,26 @@ struct PomeloApp: App {
}
}
}
private func setMoltenVKSettings() {
let settings: [String: String] = [
"MVK_DEBUG": "0",
"MVK_CONFIG_DEBUG": "0",
"MVK_CONFIG_VK_SEMAPHORE_SUPPORT_STYLE": "0",
"MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS": "1",
"MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE": "512",
"MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS": "1",
"MVK_USE_METAL_PRIVATE_API": "1",
"MVK_CONFIG_RESUME_LOST_DEVICE": "1",
"MVK_CONFIG_USE_METAL_PRIVATE_API": "1"
]
settings.forEach { strins in
setenv(strins.key, strins.value, 1)
}
}
}
// .appShortcuts([])

View File

@ -0,0 +1,77 @@
//
// PomeloRPC.swift
// Pomelo
//
// Created by Stossy11 on 17/11/2024.
// Copyright © 2024 Stossy11. All rights reserved.
//
import Foundation
func startRPC(game: PomeloGame) {
// The URL of the server endpoint
let urlString = UserDefaults.standard.string(forKey: "pomeloRPCIP")
guard let url = URL(string: urlString ?? "") else {
print("Invalid URL")
return
}
// The data to send in the request
let json: [String: Any] = [
"name": game.title,
"id": String(game.programid),
"developer": game.developer
]
// Convert the dictionary to JSON data
do {
let jsonData = try JSONSerialization.data(withJSONObject: json, options: [])
// Create the POST request
var request = URLRequest(url: url.appendingPathComponent("start-rpc"))
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = jsonData
// Create a URLSession data task
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Error: \(error.localizedDescription)")
return
}
if let response = response as? HTTPURLResponse, response.statusCode == 200 {
print("Data successfully sent to server")
} else {
print("Failed to send data. Server responded with: \(String(describing: response))")
}
}
// Start the task
task.resume()
} catch {
print("Error encoding JSON: \(error.localizedDescription)")
}
}
func rpcHeartBeat() {
let urlString = UserDefaults.standard.string(forKey: "pomeloRPCIP")
guard let url = URL(string: urlString ?? "") else {
print("Invalid URL string")
return
}
let urlRequest = URLRequest(url: url.appendingPathComponent("heartbeat"))
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
print(data)
print(response)
print(error)
}
task.resume()
}

View File

@ -23,6 +23,16 @@ struct AdvancedSettingsView: View {
@AppStorage("canShowMetalHUD") var canShowMetalHUD: Bool = false
@AppStorage("ShowMenuButton") private var ShowMenuButton: Bool = true
@AppStorage("hideLastNameTop") var hideLastName: Bool = false
@AppStorage("disableMainScreen") var disableMainScreen: Bool = false
@AppStorage("disabletouch") var disableTouch: Bool = false
@AppStorage("pomeloRPC") var pomeloRPC: Bool = false
@AppStorage("pomeloRPCIP") var pomeloRPCip: String = ""
@Environment(\.colorScheme) var colorScheme
@State var isshowing = false
var body: some View {
@ -57,6 +67,23 @@ struct AdvancedSettingsView: View {
.font(.footnote)
.foregroundColor(.gray)
if colorScheme == .light {
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(width: .infinity, height: 50)
.overlay() {
HStack {
Toggle("Disable \(deviceType()) Screen", isOn: $disableMainScreen)
.padding()
}
}
Text("When AirPlaying the \(deviceType()) Screen will be disabled (Including On Screen Controller).")
.padding(.bottom)
.font(.footnote)
.foregroundColor(.gray)
}
if canShowMetalHUD {// (Int(UIDevice.current.systemVersion) ?? 0 >= 16) || canShowMetalHUD {
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
@ -157,6 +184,38 @@ struct AdvancedSettingsView: View {
.font(.footnote)
.foregroundColor(.gray)
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(width: .infinity, height: 50)
.overlay() {
HStack {
Toggle("Pomelo RPC", isOn: $pomeloRPC)
.padding()
}
}
Text("This adds support for Discord Rich Presence (RPC). (This needs a desktop companion app)")
.padding(.bottom)
.font(.footnote)
.foregroundColor(.gray)
if pomeloRPC {
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(width: .infinity, height: 50)
.overlay() {
HStack {
Text("Pomelo RPC IP")
.padding()
TextField("Server Address", text: $pomeloRPCip)
}
}
Text("Please enter the Pomelo RPC Desktop Companion App Server Address.")
.padding(.bottom)
.font(.footnote)
.foregroundColor(.gray)
}
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
@ -164,11 +223,32 @@ struct AdvancedSettingsView: View {
.frame(width: .infinity, height: 50)
.overlay() {
HStack {
Toggle("Memory Usage Increase", isOn: $kpagetable)
Toggle("Disable Touch", isOn: $disableTouch)
.padding()
}
}
Text("This makes games way more stable but a lot of games will crash as you will run out of Memory way quicker. (Don't Enable this on devices with less then 8GB of memory as most games will crash)")
Text("This option disables the touch screen in games.")
.padding(.bottom)
.font(.footnote)
.foregroundColor(.gray)
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(width: .infinity, height: 50)
.overlay() {
HStack {
if DeviceMemory.has8GBOrMore {
Toggle("Memory Usage Increase", isOn: $kpagetable)
.padding()
} else {
Toggle("Memory Usage Increase", isOn: $kpagetable)
.padding()
.disabled(true)
}
}
}
Text("This makes games a little more stable but a lot of games will crash as you will run out of Memory way quicker, 8GB Memory is needed for this feature.")
.padding(.bottom)
.font(.footnote)
.foregroundColor(.gray)
@ -296,3 +376,12 @@ func restoreFoldersFromiCloud() {
print("Could not find iCloud Drive.")
}
}
func deviceType() -> String {
if UIDevice.current.userInterfaceIdiom == .phone {
return "iPhone"
} else if UIDevice.current.userInterfaceIdiom == .pad {
return "iPad"
} else {
return "Mac"
}
}

View File

@ -9,7 +9,7 @@
import SwiftUI
struct InfoView: View {
@AppStorage("entitlementNotExists") private var entitlementNotExists: Bool = false
@AppStorage("entitlementExists") private var entitlementExists: Bool = false
@AppStorage("increaseddebugmem") private var increaseddebugmem: Bool = false
@AppStorage("extended-virtual-addressing") private var extended: Bool = false
@State var gd = false
@ -33,7 +33,7 @@ struct InfoView: View {
.font(Font.headline.weight(.bold))
Spacer()
.frame(height: 10)
Text("Increased Memory Limit: \(String(describing: !entitlementNotExists))")
Text("Increased Memory Limit: \(String(describing: entitlementExists))")
Spacer()
.frame(height: 10)
}
@ -50,6 +50,24 @@ struct InfoView: View {
Text("Extended Virtual Addressing: \(String(describing: extended))")
}
Divider()
Text("Memory:")
.font(.title)
.font(Font.headline.weight(.bold))
Spacer()
.frame(height: 10)
Group {
Text("Current:")
.font(.title2)
.foregroundColor(.blue)
.font(Font.headline.weight(.bold))
Spacer()
.frame(height: 10)
Text(String(DeviceMemory.totalRAM) + "GB")
Spacer()
.frame(height: 10)
}
}
.padding()

View File

@ -41,6 +41,15 @@ public struct Sudachi {
public func bootMii() {
sudachiObjC.bootMii()
}
public func halt() {
sudachiObjC.halt()
}
public func start() {
sudachiObjC.start()
}
public func pause() {
sudachiObjC.pause()
}
@ -87,7 +96,6 @@ public struct Sudachi {
}
public func gyroMoved(x: Float, y: Float, z: Float, accelX: Float, accelY: Float, accelZ: Float, controllerId: Int32, deltaTimestamp: Int32) {
// Calling the Objective-C function with both gyroscope and accelerometer data
sudachiObjC.virtualControllerGyro(controllerId,
deltaTimestamp: deltaTimestamp,
gyroX: x,

View File

@ -53,6 +53,8 @@ typedef NS_ENUM(NSUInteger, VirtualControllerButtonType) {
-(void) pause;
-(void) play;
-(BOOL) ispaused;
-(BOOL) halt;
-(BOOL) start;
-(BOOL) canGetFullPath;
-(void) bootMii;
-(void) quit;

View File

@ -112,6 +112,14 @@
EmulationSession::GetInstance().PauseEmulation();
}
-(void) halt {
EmulationSession::GetInstance().HaltEmulation();
}
-(void) start {
EmulationSession::GetInstance().RunEmulation();
}
-(void) play {
EmulationSession::GetInstance().UnPauseEmulation();
}