diff --git a/Cores/CrabEMU b/Cores/CrabEMU index cdec1cd4d7..62ce4397de 160000 --- a/Cores/CrabEMU +++ b/Cores/CrabEMU @@ -1 +1 @@ -Subproject commit cdec1cd4d7728ef539f5a0ed44a7c223d5801679 +Subproject commit 62ce4397dedbd39773f0b56b19967c9fc2a75b6d diff --git a/Cores/VisualBoyAdvance-M/Package.swift b/Cores/VisualBoyAdvance-M/Package.swift index 62d5bb7c6d..4a22883333 100644 --- a/Cores/VisualBoyAdvance-M/Package.swift +++ b/Cores/VisualBoyAdvance-M/Package.swift @@ -17,7 +17,7 @@ let package = Package( .tvOS(.v16), .watchOS(.v9), .macOS(.v11), - .macCatalyst(.v15), + .macCatalyst(.v17), .visionOS(.v1) ], products: [ diff --git a/Cores/emuThree/PVEmuThree.xcodeproj/project.pbxproj b/Cores/emuThree/PVEmuThree.xcodeproj/project.pbxproj index c39c11d5d5..7860552d84 100644 --- a/Cores/emuThree/PVEmuThree.xcodeproj/project.pbxproj +++ b/Cores/emuThree/PVEmuThree.xcodeproj/project.pbxproj @@ -637,11 +637,11 @@ B318E2702CAA86BD00D0E599 /* CorePlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = B318E26F2CAA86BD00D0E599 /* CorePlist.swift */; }; B318E2722CAA871B00D0E599 /* CorePlist-Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = B318E2712CAA871B00D0E599 /* CorePlist-Generated.swift */; }; B33350262078619C0036A448 /* Core.plist in Resources */ = {isa = PBXBuildFile; fileRef = B3C7622720783510009950E4 /* Core.plist */; }; - B351E7BA2CBE067E0000E087 /* libavcodec.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7B92CBE067E0000E087 /* libavcodec.xcframework */; }; - B351E7BE2CBE06C00000E087 /* libavformat.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7BD2CBE06C00000E087 /* libavformat.xcframework */; }; - B351E7C12CBE06C80000E087 /* libavutil.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C02CBE06C80000E087 /* libavutil.xcframework */; }; - B351E7C42CBE06CE0000E087 /* libswresample.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C32CBE06CE0000E087 /* libswresample.xcframework */; }; - B351E7C72CBE06E30000E087 /* libswscale.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C62CBE06E20000E087 /* libswscale.xcframework */; }; + B351E7BA2CBE067E0000E087 /* libavcodec.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7B92CBE067E0000E087 /* libavcodec.xcframework */; platformFilter = ios; }; + B351E7BE2CBE06C00000E087 /* libavformat.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7BD2CBE06C00000E087 /* libavformat.xcframework */; platformFilter = ios; }; + B351E7C12CBE06C80000E087 /* libavutil.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C02CBE06C80000E087 /* libavutil.xcframework */; platformFilter = ios; }; + B351E7C42CBE06CE0000E087 /* libswresample.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C32CBE06CE0000E087 /* libswresample.xcframework */; platformFilter = ios; }; + B351E7C72CBE06E30000E087 /* libswscale.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C62CBE06E20000E087 /* libswscale.xcframework */; platformFilter = ios; }; B35E6BF2207CD2680040709A /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B35E6BF1207CD2670040709A /* AudioToolbox.framework */; }; B35E6BF5207CD2740040709A /* CoreAudioKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B35E6BF3207CD2730040709A /* CoreAudioKit.framework */; platformFilters = (ios, maccatalyst, macos, watchos, ); }; B35E6BF6207CD2740040709A /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B35E6BF4207CD2740040709A /* CoreAudio.framework */; }; @@ -650,7 +650,7 @@ B37CDBAC2CAA6A950026E901 /* PVCoreObjCBridge in Frameworks */ = {isa = PBXBuildFile; productRef = B37CDBAB2CAA6A950026E901 /* PVCoreObjCBridge */; }; B37CDBAF2CAA6AF90026E901 /* PVPlists in Frameworks */ = {isa = PBXBuildFile; productRef = B37CDBAE2CAA6AF90026E901 /* PVPlists */; }; B3C7621F2078325C009950E4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3C7621E2078325C009950E4 /* Foundation.framework */; }; - B3CC2E232CBC245F00D90EB1 /* MoltenVK in Frameworks */ = {isa = PBXBuildFile; productRef = B3CC2E222CBC245F00D90EB1 /* MoltenVK */; }; + B3CC2E232CBC245F00D90EB1 /* MoltenVK in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = B3CC2E222CBC245F00D90EB1 /* MoltenVK */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ diff --git a/Cores/emuThree/PVEmuThreeCore/Core/PVEmuThreeCoreBridge+Saves.mm b/Cores/emuThree/PVEmuThreeCore/Core/PVEmuThreeCoreBridge+Saves.mm index af34420a60..ff6f003fca 100644 --- a/Cores/emuThree/PVEmuThreeCore/Core/PVEmuThreeCoreBridge+Saves.mm +++ b/Cores/emuThree/PVEmuThreeCore/Core/PVEmuThreeCoreBridge+Saves.mm @@ -15,14 +15,18 @@ -(BOOL)supportsSaveStates { - (BOOL)saveStateToFileAtPath:(NSString *)fileName { if( _isInitialized) - [CitraWrapper.sharedInstance requestSave:fileName]; + @autoreleasepool { + [CitraWrapper.sharedInstance requestSave:fileName]; + } return true; } - (BOOL)saveStateToFileAtPath:(NSString *)fileName completionHandler:(void (^)(NSError *))block { bool success=false; if( _isInitialized) { - [CitraWrapper.sharedInstance requestSave:fileName]; + @autoreleasepool { + [CitraWrapper.sharedInstance requestSave:fileName]; + } success=true; } if (success) { @@ -48,13 +52,17 @@ - (BOOL)loadStateFromFileAtPath:(NSString *)fileName { autoLoadStatefileName = fileName; [NSThread detachNewThreadSelector:@selector(autoloadWaitThread) toTarget:self withObject:nil]; } else { - [CitraWrapper.sharedInstance requestLoad:fileName]; + @autoreleasepool { + [CitraWrapper.sharedInstance requestLoad:fileName]; + } } return true; } - (BOOL)loadStateFromFileAtPath:(NSString *)fileName completionHandler:(void (^)(NSError *))block { - [CitraWrapper.sharedInstance requestLoad:fileName]; + @autoreleasepool { + [CitraWrapper.sharedInstance requestLoad:fileName]; + } bool success=true; if (success) { diff --git a/PVFeatureFlags/Sources/PVFeatureFlags/PVFeatureFlags.swift b/PVFeatureFlags/Sources/PVFeatureFlags/PVFeatureFlags.swift index 4399c9b9a9..4a4019125d 100644 --- a/PVFeatureFlags/Sources/PVFeatureFlags/PVFeatureFlags.swift +++ b/PVFeatureFlags/Sources/PVFeatureFlags/PVFeatureFlags.swift @@ -258,14 +258,26 @@ public struct FeatureFlagsConfiguration: Codable, Sendable { /// Dictionary of cached feature states @Published private var featureStates: [String: Bool] = [:] - /// Dictionary to store debug overrides - made internal for testing - public var debugOverrides: [String: Bool] = [:] + /// Dictionary to store debug overrides - persisted in UserDefaults + public var debugOverrides: [String: Bool] { + get { + UserDefaults.standard.dictionary(forKey: "PVFeatureFlagsDebugOverrides") as? [String: Bool] ?? [:] + } + set { + UserDefaults.standard.set(newValue, forKey: "PVFeatureFlagsDebugOverrides") + objectWillChange.send() + } + } /// Dictionary to store remote feature flags private var remoteFlags: [String: Bool] = [:] private init() { self.featureFlags = PVFeatureFlags() + // Load any persisted debug overrides + if let savedOverrides = UserDefaults.standard.dictionary(forKey: "PVFeatureFlagsDebugOverrides") as? [String: Bool] { + print("Loaded debug overrides from UserDefaults: \(savedOverrides)") + } } /// Initialize with custom parameters for testing @@ -302,13 +314,27 @@ public struct FeatureFlagsConfiguration: Codable, Sendable { return enabled } + /// Whether the romPathMigrator feature is enabled + public var romPathMigrator: Bool { + /// Check debug override first + if let override = debugOverrides["romPathMigrator"] { + print("Debug override active for romPathMigrator: \(override)") + return override + } + + /// Fall back to main feature flags system + let enabled = featureFlags.isEnabled("romPathMigrator") + print("No debug override, using feature flags system value for romPathMigrator: \(enabled)") + return enabled + } + /// Set a debug override for a feature flag public func setDebugOverride(feature: String, enabled: Bool) { print("Setting debug override for \(feature) to: \(enabled)") - debugOverrides[feature] = enabled + var currentOverrides = debugOverrides + currentOverrides[feature] = enabled + debugOverrides = currentOverrides print("Current debug overrides: \(debugOverrides)") - // Notify observers that the value has changed - objectWillChange.send() // Update cached states updateFeatureStates() } @@ -345,16 +371,16 @@ public struct FeatureFlagsConfiguration: Codable, Sendable { /// Clear all debug overrides public func clearDebugOverrides() { print("Clearing all debug overrides") - debugOverrides.removeAll() - objectWillChange.send() + debugOverrides = [:] updateFeatureStates() } /// Clear specific debug override public func clearDebugOverride(for feature: String) { print("Clearing debug override for \(feature)") - debugOverrides.removeValue(forKey: feature) - objectWillChange.send() + var currentOverrides = debugOverrides + currentOverrides.removeValue(forKey: feature) + debugOverrides = currentOverrides updateFeatureStates() } diff --git a/PVLibrary/Sources/PVLibrary/Database/PVGameLibrary+Migration.swift b/PVLibrary/Sources/PVLibrary/Database/PVGameLibrary+Migration.swift index 14f8f14d1d..b88987660f 100644 --- a/PVLibrary/Sources/PVLibrary/Database/PVGameLibrary+Migration.swift +++ b/PVLibrary/Sources/PVLibrary/Database/PVGameLibrary+Migration.swift @@ -15,7 +15,12 @@ import PVFileSystem /// Handles migration of ROM and BIOS files from old documents directory to new shared container directory public final class ROMLocationMigrator { - private let fileManager = FileManager.default + + private let fileManager: FileManager + + public init(fileManager: FileManager = FileManager.default) { + self.fileManager = fileManager + } /// Old paths that need migration private var oldPaths: [(source: URL, destination: URL)] { diff --git a/PVLibrary/Sources/PVLibrary/Database/PVGameLibrary.swift b/PVLibrary/Sources/PVLibrary/Database/PVGameLibrary.swift index 4ceeae9e8d..86876c1ef9 100644 --- a/PVLibrary/Sources/PVLibrary/Database/PVGameLibrary.swift +++ b/PVLibrary/Sources/PVLibrary/Database/PVGameLibrary.swift @@ -13,10 +13,11 @@ import RealmSwift import RxRealm import PVLogging import PVRealm +import PVFeatureFlags @_exported public import PVSettings public class PVGameLibrary where T: DatabaseDriver { - + public struct System { public let identifier: String public let manufacturer: String @@ -25,24 +26,26 @@ public class PVGameLibrary where T: DatabaseDriver { public let unsupported: Bool public let sortedGames: [T.GameType] } - + public let database: RomDatabase public let databaseDriver: T - public let romMigrator: ROMLocationMigrator - - public init(database: RomDatabase) { + public let romMigrator: ROMLocationMigrator + + public init(database: RomDatabase, migrator: ROMLocationMigrator = .init()) { self.database = database self.databaseDriver = .init(database: database) - self.romMigrator = ROMLocationMigrator() - + self.romMigrator = migrator + // Kick off ROM migration - Task.detached { - do { - try await self.romMigrator.migrateIfNeeded() - try await self.romMigrator.fixOrphanedFiles() - ILOG("ROM migration completed successfully") - } catch { - ELOG("ROM migration failed: \(error.localizedDescription)") + Task { + if await PVFeatureFlagsManager.shared.romPathMigrator { + do { + try await self.romMigrator.migrateIfNeeded() + try await self.romMigrator.fixOrphanedFiles() + ILOG("ROM migration completed successfully") + } catch { + ELOG("ROM migration failed: \(error.localizedDescription)") + } } } } @@ -67,7 +70,7 @@ extension RealmSwift.LinkingObjects where Element: PVGame { case .mostPlayed: sortDescriptors.append(SortDescriptor(keyPath: #keyPath(PVGame.playCount), ascending: false)) } - + sortDescriptors.append(SortDescriptor(keyPath: #keyPath(PVGame.title), ascending: true)) return sorted(by: sortDescriptors) } @@ -83,7 +86,7 @@ extension Array where Element == PVGameLibrary.System { return mc == .orderedAscending } } - + switch sortOptions { case .title: return sorted(by: titleSort) @@ -91,7 +94,7 @@ extension Array where Element == PVGameLibrary.System { return sorted(by: { (s1, s2) -> Bool in let l1 = s1.sortedGames.first?.lastPlayed let l2 = s2.sortedGames.first?.lastPlayed - + if let l1 = l1, let l2 = l2 { return l1.compare(l2) == .orderedDescending } else if l1 != nil { @@ -106,7 +109,7 @@ extension Array where Element == PVGameLibrary.System { return sorted(by: { (s1, s2) -> Bool in let l1 = s1.sortedGames.first?.importDate let l2 = s2.sortedGames.first?.importDate - + if let l1 = l1, let l2 = l2 { return l1.compare(l2) == .orderedDescending } else if l1 != nil { @@ -121,7 +124,7 @@ extension Array where Element == PVGameLibrary.System { return sorted(by: { (s1, s2) -> Bool in let l1 = s1.sortedGames.first?.playCount let l2 = s2.sortedGames.first?.playCount - + if let l1 = l1, let l2 = l2 { return l1 < l2 } else if l1 != nil { diff --git a/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase.swift b/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase.swift index ea5c4d40dd..673ff9bea1 100644 --- a/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase.swift +++ b/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase.swift @@ -641,6 +641,28 @@ public extension RomDatabase { NSLog("Failed to hide game \(game.title)\n\(error.localizedDescription)") } } + + func delete(bios: PVBIOS) throws { + guard let biosURL = bios.file?.url else { + ELOG("No path for BIOS") + throw RomDeletionError.relatedFiledDeletionError + } + if FileManager.default.fileExists(atPath: biosURL.path) { + do { + try FileManager.default.removeItem(at: biosURL) + ILOG("Deleted BIOS \(bios.expectedFilename)\n\(biosURL.path)") + // Remove the PVFile from the PVBios + let realm = try Realm() + let bios = bios.warmUp() + try realm.write { + bios.file = nil + } + } catch { + WLOG("Failed to delete BIOS \(bios.expectedFilename)\n\(error.localizedDescription)") + } + } + } + func delete(game: PVGame, deleteArtwork: Bool = false, deleteSaves: Bool = false) throws { let romURL = PVEmulatorConfiguration.path(forGame: game) if deleteArtwork, !game.customArtworkURL.isEmpty { diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVFile.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVFile.swift index a906c79349..fdb5969875 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVFile.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVFile.swift @@ -59,7 +59,7 @@ public class PVFile: Object, LocalFileProvider, Codable, DomainConvertibleType { self.lastSizeCheck = Date() } } - + public override static func ignoredProperties() -> [String] { return ["sizeCache", "lastSizeCheck"] } @@ -125,18 +125,16 @@ public extension PVFile { return nil } -// Task { - guard let realm = self.realm else { - return nil - } + // Cache the MD5 only if we're not frozen + if !self.isFrozen, let realm = self.realm { do { try realm.write { md5Cache = calculatedMD5 } } catch { - ELOG("\(error)") + ELOG("Failed to cache MD5: \(error)") } -// } + } return calculatedMD5 } diff --git a/PVLookup/Sources/ROMMetadataProvider/ROMMetadataProvider.swift b/PVLookup/Sources/ROMMetadataProvider/ROMMetadataProvider.swift index d461575b83..ba0c4725b1 100644 --- a/PVLookup/Sources/ROMMetadataProvider/ROMMetadataProvider.swift +++ b/PVLookup/Sources/ROMMetadataProvider/ROMMetadataProvider.swift @@ -43,6 +43,7 @@ public extension ROMMetadataProvider { .replacingOccurrences(of: "]", with: "\\]") .replacingOccurrences(of: "*", with: "\\*") // Wildcards .replacingOccurrences(of: "?", with: "\\?") + .replacingOccurrences(of: "#", with: "\\#") return escapedLike } diff --git a/PVUI/Sources/PVSwiftUI/Consoles/BiosRowView.swift b/PVUI/Sources/PVSwiftUI/Consoles/BiosRowView.swift index 5fd05de21c..6fd21baf1a 100644 --- a/PVUI/Sources/PVSwiftUI/Consoles/BiosRowView.swift +++ b/PVUI/Sources/PVSwiftUI/Consoles/BiosRowView.swift @@ -28,85 +28,157 @@ extension BIOSStatus.State { } struct BiosRowView: SwiftUI.View { + /// Primary key of the BIOS + let biosFilename: String - var bios: PVBIOS + /// Observed BIOS object fetched from Realm + @ObservedRealmObject private var bios: PVBIOS @MainActor - @State var biosState: BIOSStatus.State? = nil + @State private var biosState: BIOSStatus.State? = nil @State private var showMD5Alert = false - @ObservedObject private var themeManager = ThemeManager.shared - var body: some SwiftUI.View { - return HStack(alignment: .center, spacing: 0) { - Image(biosState?.biosStatusImageName ?? BIOSStatus.State.missing.biosStatusImageName, bundle: PVUIBase.BundleLoader.myBundle).resizable().scaledToFit() - .padding(.vertical, 4) - .padding(.horizontal, 12) - VStack(alignment: .leading) { - Text("\(bios.descriptionText)") - .font(.footnote) - .foregroundColor(themeManager.currentPalette.settingsCellTextDetail?.swiftUIColor) - Text("\(bios.expectedMD5.uppercased()) : \(bios.expectedSize) bytes") - .font(.caption2) - .foregroundColor(themeManager.currentPalette.gameLibraryText.swiftUIColor) - } - Spacer() - HStack(alignment: .center, spacing: 4) { - switch biosState { - case .match: - Image(systemName: "checkmark") - .foregroundColor(themeManager.currentPalette.gameLibraryText.swiftUIColor) - .font(.footnote.weight(.light)) - case .missing: - Text("Missing") - .font(.caption) - .foregroundColor(Color.red) - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(Color.red) - .font(.caption.weight(.light)) - case .mismatch(_): - Text("Mismatch") - .font(.caption) - .foregroundColor(Color.yellow) - .border(Color.yellow) - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(Color.yellow) - .font(.caption.weight(.medium)) - case .none: - Text("Loading...") - .font(.caption) - .foregroundColor(Color.red) - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(Color.red) - .font(.caption.weight(.medium)) - } - } - .padding(.horizontal, 12) + /// Computed property to get the current BIOS state + private var currentBiosState: BIOSStatus.State { + (bios as BIOSStatusProvider).status.state + } + + init?(biosFilename: String) { + self.biosFilename = biosFilename + let config = Realm.Configuration.defaultConfiguration + guard let realm = try? Realm(configuration: config), + let bios = realm.object(ofType: PVBIOS.self, forPrimaryKey: biosFilename) else { + return nil } - .frame(height: 40) - .task { @MainActor in - if biosState == nil { + _bios = ObservedRealmObject(wrappedValue: bios) + + } + + /// Action to copy MD5 to clipboard + private func copyMD5() { + UIPasteboard.general.string = bios.expectedMD5 + showMD5Alert = true + } + + /// Action to delete BIOS + private func deleteBIOS() { + do { + try RomDatabase.sharedInstance.delete(bios: bios) + // Update the biosState after deletion + Task { @MainActor in let biosStatus = (bios as BIOSStatusProvider).status self.biosState = biosStatus.state } + } catch { + ELOG("Failed to delete BIOS: \(error.localizedDescription)") } - #if !os(tvOS) - .onTapGesture { - if case .missing = biosState { - UIPasteboard.general.string = bios.expectedMD5 - showMD5Alert = true + } + + var body: some SwiftUI.View { + Group { + HStack(alignment: .center, spacing: 0) { + Image(biosState?.biosStatusImageName ?? BIOSStatus.State.missing.biosStatusImageName, bundle: PVUIBase.BundleLoader.myBundle).resizable().scaledToFit() + .padding(.vertical, 4) + .padding(.horizontal, 12) + VStack(alignment: .leading) { + Text("\(bios.descriptionText)") + .font(.footnote) + .foregroundColor(themeManager.currentPalette.settingsCellTextDetail?.swiftUIColor) + Text("\(bios.expectedMD5.uppercased()) : \(bios.expectedSize) bytes") + .font(.caption2) + .foregroundColor(themeManager.currentPalette.gameLibraryText.swiftUIColor) + } + Spacer() + HStack(alignment: .center, spacing: 4) { + switch biosState { + case .match: + Image(systemName: "checkmark") + .foregroundColor(themeManager.currentPalette.gameLibraryText.swiftUIColor) + .font(.footnote.weight(.light)) + case .missing: + if bios.optional { + Text("Optional") + .font(.caption) + .foregroundColor(Color.gray) + Image(systemName: "info.circle") + .foregroundColor(Color.gray) + .font(.caption.weight(.light)) + } else { + Text("Missing") + .font(.caption) + .foregroundColor(Color.red) + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(Color.red) + .font(.caption.weight(.light)) + } + case .mismatch(_): + Text("Mismatch") + .font(.caption) + .foregroundColor(Color.yellow) + .border(Color.yellow) + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(Color.yellow) + .font(.caption.weight(.medium)) + case .none: + Text("Loading...") + .font(.caption) + .foregroundColor(Color.red) + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(Color.red) + .font(.caption.weight(.medium)) + } + } + .padding(.horizontal, 12) } - } - #endif - .uiKitAlert("MD5 Copied", - message: "The MD5 for \(bios.expectedFilename) has been copied to the pasteboard", - isPresented: $showMD5Alert, - buttons: { - UIAlertAction(title: "OK", style: .default, handler: { - _ in - showMD5Alert = false + .frame(height: 40) + .onAppear { @MainActor in + // Update biosState whenever the view appears or bios changes + biosState = currentBiosState + } + .task { @MainActor in + // Update biosState whenever the view appears or bios changes + biosState = currentBiosState + } + .onChange(of: bios.file) { _ in + // Update biosState whenever the file property changes + biosState = currentBiosState + } +#if !os(tvOS) +// .onTapGesture { +// if case .missing = biosState { +// copyMD5() +// } +// } + .contextMenu { + Button(action: copyMD5) { + Label("Copy MD5", systemImage: "doc.on.doc") + } + + if bios.file != nil { + Button(role: .destructive, action: deleteBIOS) { + Label("Delete BIOS", systemImage: "trash") + } + } + } + #else + .contextMenu { + if bios.file != nil { + Button(role: .destructive, action: deleteBIOS) { + Label("Delete BIOS", systemImage: "trash") + } + } + } +#endif + .uiKitAlert("MD5 Copied", + message: "The MD5 for \(bios.expectedFilename) has been copied to the pasteboard", + isPresented: $showMD5Alert, + buttons: { + UIAlertAction(title: "OK", style: .default, handler: { _ in + showMD5Alert = false + }) }) - }) + } } } #endif diff --git a/PVUI/Sources/PVSwiftUI/Consoles/BiosesView.swift b/PVUI/Sources/PVSwiftUI/Consoles/BiosesView.swift index f756808a24..e2d39167eb 100644 --- a/PVUI/Sources/PVSwiftUI/Consoles/BiosesView.swift +++ b/PVUI/Sources/PVSwiftUI/Consoles/BiosesView.swift @@ -11,13 +11,13 @@ import PVLibrary import PVThemes struct BiosesView: View { - let console: PVSystem + @ObservedRealmObject var console: PVSystem var body: some View { VStack { GamesDividerView() - ForEach(console.bioses, id: \.self) { bios in - BiosRowView(bios: bios.warmUp()) + ForEach(console.bioses, id: \.expectedFilename) { bios in + BiosRowView(biosFilename: bios.expectedFilename) GamesDividerView() } } diff --git a/PVUI/Sources/PVSwiftUI/Settings/SettingsSwiftUI.swift b/PVUI/Sources/PVSwiftUI/Settings/SettingsSwiftUI.swift index c65bc0a2bd..67820f3fb9 100644 --- a/PVUI/Sources/PVSwiftUI/Settings/SettingsSwiftUI.swift +++ b/PVUI/Sources/PVSwiftUI/Settings/SettingsSwiftUI.swift @@ -15,6 +15,7 @@ import RxSwift import RealmSwift import Perception import PVFeatureFlags +import Defaults #if canImport(FreemiumKit) import FreemiumKit @@ -667,6 +668,7 @@ private struct FeatureFlagsDebugView: View { List { LoadingSection(isLoading: isLoading, flags: flags) FeatureFlagsSection(flags: flags, featureFlags: featureFlags) + UserDefaultsSection() ConfigurationSection() DebugControlsSection(featureFlags: featureFlags, flags: $flags, isLoading: $isLoading, errorMessage: $errorMessage) } @@ -871,6 +873,7 @@ private struct DebugControlsSection: View { @Binding var flags: [(key: String, flag: FeatureFlag, enabled: Bool)] @Binding var isLoading: Bool @Binding var errorMessage: String? + @AppStorage("showFeatureFlagsDebug") private var showFeatureFlagsDebug = false var body: some View { Section(header: Text("Debug Controls")) { @@ -891,13 +894,23 @@ private struct DebugControlsSection: View { Button("Reset to Default") { Task { do { + // Reset feature flags to default try await loadDefaultConfiguration() flags = featureFlags.getAllFeatureFlags() + + // Reset unlock status + showFeatureFlagsDebug = false + + // Reset all user defaults to their default values + Defaults.Keys.useAppGroups.reset() + Defaults.Keys.unsupportedCores.reset() + Defaults.Keys.iCloudSync.reset() } catch { errorMessage = "Failed to load default configuration: \(error.localizedDescription)" } } } + .foregroundColor(.red) // Make it stand out as a destructive action } } @@ -910,6 +923,13 @@ private struct DebugControlsSection: View { minBuildNumber: "100", allowedAppTypes: ["standard", "lite", "standard.appstore", "lite.appstore"], description: "Test configuration - enabled for all builds" + ), + "romPathMigrator": FeatureFlag( + enabled: true, + minVersion: "1.0.0", + minBuildNumber: "100", + allowedAppTypes: ["standard", "lite", "standard.appstore", "lite.appstore"], + description: "Test configuration - enabled for all builds" ) ] @@ -1092,3 +1112,54 @@ private struct SecretSettingsRow: View { } } } + +private struct UserDefaultsSection: View { + @Default(.useAppGroups) var useAppGroups + @Default(.unsupportedCores) var unsupportedCores + @Default(.iCloudSync) var iCloudSync + + var body: some View { + Section(header: Text("User Defaults")) { + UserDefaultToggle( + title: "useAppGroups", + subtitle: "Use App Groups for shared storage", + isOn: $useAppGroups + ) + + UserDefaultToggle( + title: "unsupportedCores", + subtitle: "Enable experimental and unsupported cores", + isOn: $unsupportedCores + ) + + UserDefaultToggle( + title: "iCloudSync", + subtitle: "Sync save states and settings with iCloud", + isOn: $iCloudSync + ) + } + } +} + +private struct UserDefaultToggle: View { + let title: String + let subtitle: String + @Binding var isOn: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + VStack(alignment: .leading) { + Text(title) + .font(.headline) + Text(subtitle) + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + Toggle("", isOn: $isOn) + } + } + .padding(.vertical, 4) + } +} diff --git a/PVUI/Sources/PVUIBase/GameLaunching/GameLaunchingViewController.swift b/PVUI/Sources/PVUIBase/GameLaunching/GameLaunchingViewController.swift index fd0b7f45be..ef1995209d 100644 --- a/PVUI/Sources/PVUIBase/GameLaunching/GameLaunchingViewController.swift +++ b/PVUI/Sources/PVUIBase/GameLaunching/GameLaunchingViewController.swift @@ -16,14 +16,15 @@ import PVPlists import PVRealm import PVSystems import PVFileSystem +import MBProgressHUD private let WIKI_BIOS_URL = "https://wiki.provenance-emu.com/installation-and-usage/bios-requirements" /* Protocol with default implimentation. - + This allows any UIViewController class to just inherit GameLaunchingViewController, and then it can call load(PVGame)! - + */ public protocol GameLaunchingViewController: AnyObject { @@ -46,34 +47,46 @@ extension GameLaunchingViewController { public extension GameLaunchingViewController where Self: UIViewController { - + //MARK: Default protocol implementation `GameLaunchingViewController` @MainActor - + func canLoad(_ game: PVGame) async throws { guard let system = game.system else { throw GameLaunchingError.systemNotFound } - + try await biosCheck(system: system) } - + @MainActor func load(_ game: PVGame, sender: Any? = nil, core: PVCore? = nil, saveState: PVSaveState? = nil) async { guard game.realm != nil else { return } - + + // Show loading HUD + let hud = MBProgressHUD.showAdded(to: self.view, animated: true) + hud.label.text = "Loading \(game.title)..." + hud.mode = .indeterminate + + defer { + // Ensure HUD is hidden when function exits + DispatchQueue.main.async { + hud.hide(animated: true, afterDelay: 0.1) + } + } + @ThreadSafe var game: PVGame! = game @ThreadSafe var core = core @ThreadSafe var saveState = saveState - + guard !(presentedViewController is PVEmulatorViewController) else { let currentGameVC = presentedViewController as! PVEmulatorViewController displayAndLogError(withTitle: "Cannot open new game", message: "A game is already running the game \(currentGameVC.game.title).") return } - + if saveState != nil { let path = saveState!.file.url.path ILOG("Opening with save state at path: \(path)") @@ -85,7 +98,7 @@ extension GameLaunchingViewController where Self: UIViewController { saveState = nil } } - + // Check if file exists let offline: Bool = !(game.file.online) if offline { @@ -97,43 +110,43 @@ extension GameLaunchingViewController where Self: UIViewController { return } } - + // Pre-flight guard let system = game.system else { displayAndLogError(withTitle: "Cannot open game", message: "Requested system cannot be found for game '\(game.title)'.") return } - + do { /// try await downloadFileIfNeeded(game.file.url) - + try await canLoad(game) VLOG("canLoad \(game.title)") // Init emulator VC - + guard let system = game.system else { displayAndLogError(withTitle: "Cannot open game", message: "No system found matching '\(game.systemIdentifier)'.") return } - + VLOG("\(game.title) matched system \(system.name)\n Cores: \(system.cores.map{$0.principleClass}.joined(separator: ", "))") - + // Are unsupported cr let unsupportedCores = Defaults[.unsupportedCores] - - + + let cores: [PVCore] = system.cores.filter { (!$0.disabled || unsupportedCores) && $0.hasCoreClass && !(AppState.shared.isAppStore && $0.appStoreDisabled) }.sorted(by: { $0.projectName < $1.projectName }) - + guard !cores.isEmpty else { displayAndLogError(withTitle: "Cannot open game", message: "No core found for game system '\(system.shortName)'.") return } - + var selectedCore: PVCore? - + // If a core is passed in and it's valid for this system, use it. if let saveState = saveState { if cores.contains(saveState.core) { @@ -144,17 +157,17 @@ extension GameLaunchingViewController where Self: UIViewController { return } } - + // See if the user chose a core if selectedCore == nil, let core = core, cores.contains(core) { selectedCore = core } - + // Check if multiple cores can launch this rom if selectedCore == nil, cores.count > 1 { let coresString: String = cores.map({ $0.projectName }).joined(separator: ", ") ILOG("Multiple cores found for system \(system.name). Cores: \(coresString)") - + // See if the system or game has a default selection already set if let userSelecion = game.userPreferredCoreID ?? system.userPreferredCoreID, let chosenCore = cores.first(where: { $0.identifier == userSelecion }) { @@ -165,7 +178,7 @@ extension GameLaunchingViewController where Self: UIViewController { } return } - + // User has no core preference, present dialogue to pick presentCoreSelection(forGame: game, sender: sender) } else { @@ -177,7 +190,7 @@ extension GameLaunchingViewController where Self: UIViewController { @ThreadSafe var core = core @ThreadSafe var theadsafeCore = game @ThreadSafe var saveState = saveState - + await presentEMU(withCore: selectedCore, forGame: game, fromSaveState: saveState, source: sender as? UIView ?? presentingView) // let contentId : String = "\(system.shortName):\(game.title)" // let customAttributes : [String : Any] = ["timeSpent" : game.timeSpentInGame, "md5" : game.md5Hash] @@ -189,10 +202,10 @@ extension GameLaunchingViewController where Self: UIViewController { } catch let GameLaunchingError.missingBIOSes(missingBIOSes) { // Create missing BIOS directory to help user out PVEmulatorConfiguration.createBIOSDirectory(forSystemIdentifier: system.enumValue) - + let missingFilesString = missingBIOSes.joined(separator: "\n") let relativeBiosPath = "Documents/BIOS/\(system.identifier)/" - + let message = "\(system.shortName) requires BIOS files to run games. Ensure the following files are inside \(relativeBiosPath)\n\(missingFilesString)" #if os(iOS) let guideAction = UIAlertAction(title: "Guide", style: .default, handler: { _ in @@ -212,19 +225,19 @@ extension GameLaunchingViewController where Self: UIViewController { displayAndLogError(withTitle: "Cannot open game", message: "Unknown error: \(error.localizedDescription)") } } - + @MainActor - + func openSaveState(_ saveState: PVSaveState) async { - + if let gameVC = presentedViewController as? PVEmulatorViewController { // try? RomDatabase.sharedInstance.writeTransaction { try? saveState.realm!.write { saveState.lastOpened = Date() } - + gameVC.core.setPauseEmulation(true) - + do { let path = saveState.file.url.path try await gameVC.core.loadState(fromFileAtPath: path) @@ -232,7 +245,7 @@ extension GameLaunchingViewController where Self: UIViewController { } catch { let description = error.localizedDescription let reason = (error as NSError).localizedFailureReason - + let msg = "Failed to load save state: \(description) \(reason ?? "")" self.presentError(msg, source: self.view) { gameVC.core.setPauseEmulation(false) @@ -242,14 +255,14 @@ extension GameLaunchingViewController where Self: UIViewController { presentWarning("No core loaded", source: self.view) } } - - + + func updateRecentGames(_ game: PVGame) { let database = RomDatabase.sharedInstance RomDatabase.refresh() - + let recents: Results = database.all(PVRecentGame.self) - + let recentsMatchingGame = database.all(PVRecentGame.self, where: #keyPath(PVRecentGame.game.md5Hash), value: game.md5Hash) let recentToDelete = recentsMatchingGame.first if let recentToDelete = recentToDelete { @@ -259,7 +272,7 @@ extension GameLaunchingViewController where Self: UIViewController { ELOG("Failed to delete recent: \(error.localizedDescription)") } } - + if recents.count >= PVMaxRecentsCount() { // TODO: This should delete more than just the last incase we had an overflow earlier if let oldestRecent: PVRecentGame = recents.sorted(byKeyPath: #keyPath(PVRecentGame.lastPlayedDate), ascending: false).last { @@ -270,7 +283,7 @@ extension GameLaunchingViewController where Self: UIViewController { } } } - + if let currentRecent = game.recentPlays.first { do { currentRecent.lastPlayedDate = Date() @@ -283,7 +296,7 @@ extension GameLaunchingViewController where Self: UIViewController { let newRecent = PVRecentGame(withGame: game) do { try database.add(newRecent, update: false) - + let activity = game.spotlightActivity // Make active, causes it to index also userActivity = activity @@ -292,14 +305,14 @@ extension GameLaunchingViewController where Self: UIViewController { } } } - - + + func presentCoreSelection(forGame game: PVGame, sender: Any?) { guard let system = game.system else { ELOG("No system for game \(game.title)") return } - + let cores = system.cores .sorted(byKeyPath: "projectName") .filter({ @@ -312,7 +325,7 @@ extension GameLaunchingViewController where Self: UIViewController { // .distinct(by: #keyPath(\PVSystem.name)) .sorted { $0.projectName > $1.projectName } // .sorted { $0.supportedSystems.count <= $1.supportedSystems.count } - + let coreChoiceAlert = UIAlertController(title: "Multiple cores found", message: "Select which core to use with this game.", preferredStyle: .actionSheet) @@ -330,7 +343,7 @@ extension GameLaunchingViewController where Self: UIViewController { coreChoiceAlert.popoverPresentationController?.sourceRect = senderView.bounds } #endif - + for core in cores { let action = UIAlertAction(title: core.projectName, style: .default) { [unowned self] _ in let message = "Open with \(core.projectName)…" @@ -349,7 +362,7 @@ extension GameLaunchingViewController where Self: UIViewController { alwaysUseAlert.popoverPresentationController?.sourceRect = senderView.bounds } #endif - + let thisTimeOnlyAction = UIAlertAction(title: "This time", style: .default, handler: { _ in Task { @MainActor in await self.presentEMU(withCore: core, forGame: game, source: sender as? UIView ?? self.view) @@ -371,22 +384,22 @@ extension GameLaunchingViewController where Self: UIViewController { await self.presentEMU(withCore: core, forGame: game, source: sender as? UIView ?? self.view) } }) - + alwaysUseAlert.addAction(thisTimeOnlyAction) alwaysUseAlert.addAction(alwaysThisGameAction) alwaysUseAlert.addAction(alwaysThisSystemAction) - + self.present(alwaysUseAlert, animated: true) } - + coreChoiceAlert.addAction(action) } - + coreChoiceAlert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .destructive, handler: nil)) - + present(coreChoiceAlert, animated: true) } - + // MARK: - Private private func getExpectedFilename(_ bios:BIOS) -> String { var expectedFilename=bios.expectedFilename @@ -395,21 +408,21 @@ extension GameLaunchingViewController where Self: UIViewController { } return expectedFilename } - + @MainActor private func biosCheck(system: PVSystem) async throws { guard system.requiresBIOS else { // Nothing to do return } - + // Check if requires a BIOS and has them all - only warns if md5's mismatch let biosEntries = system.bioses guard !biosEntries.isEmpty else { ELOG("System \(system.name) specifies it requires BIOS files but does not provide values for \(SystemDictionaryKeys.BIOSEntries)") throw GameLaunchingError.generic("Invalid configuration for system \(system.name). Missing BIOS dictionary in systems.plist") } - + let biosPathContents: [String] do { biosPathContents = try FileManager.default.contentsOfDirectory(at: system.biosDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]).compactMap { $0.isFileURL ? $0.lastPathComponent : nil } @@ -418,19 +431,19 @@ extension GameLaunchingViewController where Self: UIViewController { let biosFiles = await biosEntries.toArray().asyncMap { return self.getExpectedFilename($0.asDomain()) }.joined(separator: ", ") - + let documentsPath = URL.documentsPath.path let biosDirectory = system.biosDirectory.path.replacingOccurrences(of: documentsPath, with: "") - + let message = "This system requires BIOS files. Please upload '\(biosFiles)' to \(biosDirectory)." ELOG(message) throw GameLaunchingError.generic(message) } - + // Store the HASH : FILENAME of the BIOS directory contents // Only generated if needed for matching if filename fails var biosPathContentsMD5Cache: [String: String]? - + var missingBIOSES = [String]() var entries = await biosEntries.toArray().asyncMap({ $0.asDomain() }) // Search for additional conditional bios requirements stored as JSON file @@ -444,7 +457,7 @@ extension GameLaunchingViewController where Self: UIViewController { let size = biosInfo[md5] { let newBIOS = PVBIOS(withSystem: system, descriptionText: file, expectedMD5: md5, expectedSize: size, expectedFilename: file) newBIOS.optional = false - + let database = RomDatabase.sharedInstance /// Add the bios to the database if database.realm.isInWriteTransaction { @@ -458,12 +471,12 @@ extension GameLaunchingViewController where Self: UIViewController { ELOG("Failed to add BIOS: \(error)") } } - + entries.append(newBIOS.asDomain()) } }) } - + // Go through each BIOSEntry struct and see if all non-optional BIOS's were found in the BIOS dir // Try to match MD5s for files that don't match by name, and rename them to what's expected if found // Warn on files that have filename match but MD5 doesn't match expected @@ -473,7 +486,7 @@ extension GameLaunchingViewController where Self: UIViewController { // Check for a direct filename match and that it isn't an optional BIOS if we don't find it if !biosPathContents.contains(expectedFilename), !currentEntry.optional { // Didn't match by files name, now we generate all the md5's and see if any match, if they do, move the matching file to the correct filename - + // 1 - Lazily generate the hashes of files in the BIOS directory if biosPathContentsMD5Cache == nil { biosPathContentsMD5Cache = biosPathContents.reduce([String: String](), { (hashDictionary, filename) -> [String: String] in @@ -492,7 +505,7 @@ extension GameLaunchingViewController where Self: UIViewController { } }) } - + // 2 - See if any hashes in the BIOS directory match the current BIOS entry we're investigating. if let biosPathContentsMD5Cache = biosPathContentsMD5Cache, let filenameOfFoundFile = biosPathContentsMD5Cache[currentEntry.expectedMD5.uppercased()] { // Rename the file to what we expected @@ -526,22 +539,22 @@ extension GameLaunchingViewController where Self: UIViewController { } } } // End canLoad .all loop - + if !canLoad { throw GameLaunchingError.missingBIOSes(missingBIOSES) } } - + private func displayAndLogError(withTitle title: String, message: String, customActions: [UIAlertAction]? = nil) { ELOG(message) - + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.popoverPresentationController?.barButtonItem = navigationItem.leftBarButtonItem customActions?.forEach { alertController.addAction($0) } alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) present(alertController, animated: true) } - + @MainActor private func presentEMU(withCore core: PVCore, forGame game: PVGame, fromSaveState saveState: PVSaveState? = nil, source: UIView?) async { guard let game = RomDatabase.sharedInstance.realm.object(ofType: PVGame.self, forPrimaryKey: game.md5Hash) else { return @@ -554,14 +567,14 @@ extension GameLaunchingViewController where Self: UIViewController { ELOG("Failed to init core instance") return } - + let emulatorViewController = PVEmulatorViewController(game: game, core: coreInstance) - + // Check if Save State exists if saveState == nil, emulatorViewController.core.supportsSaveStates { @ThreadSafe var theadsafeCore = core @ThreadSafe var threadsafeGame = game - + await checkForSaveStateThenRun(withCore: theadsafeCore!, forGame: threadsafeGame!, source: source) { optionallyChosenSaveState in self.presentEMUVC(emulatorViewController, withGame: game, loadingSaveState: optionallyChosenSaveState) } @@ -569,22 +582,22 @@ extension GameLaunchingViewController where Self: UIViewController { presentEMUVC(emulatorViewController, withGame: game, loadingSaveState: saveState) } } - + // Used to just show and then optionally quickly load any passed in PVSaveStates @MainActor private func presentEMUVC(_ emulatorViewController: PVEmulatorViewController, withGame game: PVGame, loadingSaveState saveState: PVSaveState? = nil) { // Present the emulator VC emulatorViewController.modalTransitionStyle = .crossDissolve emulatorViewController.modalPresentationStyle = .fullScreen - + present(emulatorViewController, animated: true) { () -> Void in - + emulatorViewController.gpuViewController.screenType = game.system.screenType.rawValue - + // Open the save state after a bootup delay if the user selected one // Use a timer loop on ios 10+ to check if the emulator has started running if let saveState = saveState { let saveStateID = saveState.id - + emulatorViewController.gpuViewController.view.isHidden = true _ = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: {[weak self] timer in guard let self = self else { @@ -604,11 +617,11 @@ extension GameLaunchingViewController where Self: UIViewController { }) } } - + Task.detached { @MainActor in await PVControllerManager.shared.iCadeController?.refreshListener() } - + do { try RomDatabase.sharedInstance.writeTransaction { game.playCount += 1 @@ -618,13 +631,13 @@ extension GameLaunchingViewController where Self: UIViewController { ELOG("\(error.localizedDescription)") } } - + @MainActor private func checkForSaveStateThenRun(withCore core: PVCore, forGame game: PVGame, source: UIView?, completion: @escaping (PVSaveState?) -> Void) async { var foundSave = false var saves = game.saveStates.filter("core.identifier == \"\(core.identifier)\"").sorted(byKeyPath: "date", ascending: false).toArray() + game.autoSaves.filter("core.identifier == \"\(core.identifier)\"").sorted(byKeyPath: "date", ascending: false).toArray() saves = saves.sorted(by: { $0.date.compare($1.date) == .orderedDescending }) - + let saveState : PVSaveState? = await Task { for save in saves { let path = await save.file.url.path @@ -635,11 +648,11 @@ extension GameLaunchingViewController where Self: UIViewController { } return nil }.value - + if foundSave, let latestSaveState = saveState { let shouldAskToLoadSaveState: Bool = Defaults[.askToAutoLoad] let shouldAutoLoadSaveState: Bool = Defaults[.autoLoadSaves] - + if shouldAutoLoadSaveState { completion(latestSaveState) } else if shouldAskToLoadSaveState { @@ -654,7 +667,7 @@ extension GameLaunchingViewController where Self: UIViewController { let switchControl = UISwitch() switchControl.isOn = !Defaults[.askToAutoLoad] textEditBlocker.switchControl = switchControl - + // Add a save this setting toggle alert.addTextField { textField in textField.text = "Auto Load Saves" @@ -665,7 +678,7 @@ extension GameLaunchingViewController where Self: UIViewController { switchControl.transform = CGAffineTransform(scaleX: 0.90, y: 0.90) } #endif - + // Restart alert.addAction(UIAlertAction(title: "Restart", style: .default, handler: { (_: UIAlertAction) -> Void in #if os(iOS) @@ -676,7 +689,7 @@ extension GameLaunchingViewController where Self: UIViewController { #endif completion(nil) })) - + #if os(tvOS) alert.addAction(UIAlertAction(title: "Restart (Always)", style: .default, handler: { (_: UIAlertAction) -> Void in Defaults[.askToAutoLoad] = false @@ -684,7 +697,7 @@ extension GameLaunchingViewController where Self: UIViewController { completion(nil) })) #endif - + // Continue alert.addAction(UIAlertAction(title: "Continue", style: .default, handler: { (_: UIAlertAction) -> Void in #if os(iOS) @@ -696,7 +709,7 @@ extension GameLaunchingViewController where Self: UIViewController { completion(latestSaveState) })) alert.preferredAction = alert.actions.last - + #if os(tvOS) // Continue Always alert.addAction(UIAlertAction(title: "Continue (Always)", style: .default, handler: { (_: UIAlertAction) -> Void in @@ -705,13 +718,13 @@ extension GameLaunchingViewController where Self: UIViewController { completion(latestSaveState) })) #endif - + alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .cancel, handler: nil)) - + // Present the alert DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in guard let `self` = self else { return } - + self.present(alert, animated: true) } } @@ -723,12 +736,12 @@ extension GameLaunchingViewController where Self: UIViewController { completion(nil) } } - + func doLoad(_ game: PVGame) async throws { guard let system = game.system else { throw GameLaunchingError.systemNotFound } - + try await biosCheck(system: system) } } diff --git a/PVUI/Sources/PVUIBase/PVGLViewController/PVMetalViewController.swift b/PVUI/Sources/PVUIBase/PVGLViewController/PVMetalViewController.swift index fe59252845..4b170b1cea 100644 --- a/PVUI/Sources/PVUIBase/PVGLViewController/PVMetalViewController.swift +++ b/PVUI/Sources/PVUIBase/PVGLViewController/PVMetalViewController.swift @@ -504,8 +504,14 @@ class PVMetalViewController : PVGPUViewController, PVRenderDelegate, MTKViewDele ELOG("emulatorCore is nil in updateInputTexture()") return } + let screenRect = emulatorCore.screenRect + guard screenRect != .zero else { + ELOG("Screenrect was zero, exiting early") + return + } + let pixelFormat = getMTLPixelFormat(from: emulatorCore.pixelFormat, type: emulatorCore.pixelType) diff --git a/Provenance.xcodeproj/project.pbxproj b/Provenance.xcodeproj/project.pbxproj index e6990b4a80..d3898d07e4 100644 --- a/Provenance.xcodeproj/project.pbxproj +++ b/Provenance.xcodeproj/project.pbxproj @@ -198,6 +198,8 @@ B318E29E2CAB58A300D0E599 /* PVRSPCXD4.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B318E29C2CAB58A300D0E599 /* PVRSPCXD4.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B318E2A02CAB5A2600D0E599 /* PVProSystem-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = B318E29F2CAB5A2600D0E599 /* PVProSystem-Dynamic */; }; B318E2A12CAB5A2600D0E599 /* PVProSystem-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = B318E29F2CAB5A2600D0E599 /* PVProSystem-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + B31A9FEE2D1FA7440085789A /* libMoltenVK.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = B39C4B6D2BF0AB6000F199BB /* libMoltenVK.dylib */; platformFilter = maccatalyst; }; + B31A9FEF2D1FA7440085789A /* libMoltenVK.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B39C4B6D2BF0AB6000F199BB /* libMoltenVK.dylib */; platformFilter = maccatalyst; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; B32111A52CBC428700BE8D6C /* TopShelf.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BE9FDCB71C210B9E0046DF0E /* TopShelf.appex */; platformFilters = (tvos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B32111B12CBC42B500BE8D6C /* SpotlightImportExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B355E0112920AA9800E4C858 /* SpotlightImportExtension.appex */; platformFilters = (ios, maccatalyst, macos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; B323B0092BF46D2800CEA3CF /* PVLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = B323B0082BF46D2800CEA3CF /* PVLibrary */; }; @@ -263,6 +265,9 @@ B32C08F72D053F680024A3E2 /* MoltenVK in Frameworks */ = {isa = PBXBuildFile; productRef = B32C08F62D053F680024A3E2 /* MoltenVK */; }; B32C08F92D053FF60024A3E2 /* PVMupen64PlusBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B32C08F82D053FF60024A3E2 /* PVMupen64PlusBridge.framework */; }; B32C08FA2D053FF70024A3E2 /* PVMupen64PlusBridge.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B32C08F82D053FF60024A3E2 /* PVMupen64PlusBridge.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B32CEE3F2D20D40C0083552D /* PVFeatureFlags in Frameworks */ = {isa = PBXBuildFile; productRef = B32CEE3E2D20D40C0083552D /* PVFeatureFlags */; }; + B32CEE412D20D4530083552D /* PVFeatureFlags in Frameworks */ = {isa = PBXBuildFile; productRef = B32CEE402D20D4530083552D /* PVFeatureFlags */; }; + B32CEE432D20D45C0083552D /* PVFeatureFlags in Frameworks */ = {isa = PBXBuildFile; productRef = B32CEE422D20D45C0083552D /* PVFeatureFlags */; }; B32D45BD2CB730AE006B76C2 /* PVGambatte in Frameworks */ = {isa = PBXBuildFile; productRef = B32D45BC2CB730AE006B76C2 /* PVGambatte */; }; B32D45C12CB730FB006B76C2 /* PVGambatte in Frameworks */ = {isa = PBXBuildFile; productRef = B32D45C02CB730FB006B76C2 /* PVGambatte */; }; B32D45C32CB73101006B76C2 /* PVGambatte in Frameworks */ = {isa = PBXBuildFile; productRef = B32D45C22CB73101006B76C2 /* PVGambatte */; }; @@ -432,16 +437,16 @@ B35197FC2D1A5EF100D2B413 /* PVGenesis.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B35197FB2D1A5EF100D2B413 /* PVGenesis.framework */; }; B35197FD2D1A5EF100D2B413 /* PVGenesis.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B35197FB2D1A5EF100D2B413 /* PVGenesis.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B35197FF2D1A5F5300D2B413 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = B35197FE2D1A5F5300D2B413 /* Defaults */; }; - B351E7CE2CBE072D0000E087 /* libavformat.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C92CBE072C0000E087 /* libavformat.xcframework */; platformFilters = (ios, maccatalyst, ); }; - B351E7CF2CBE072D0000E087 /* libavformat.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C92CBE072C0000E087 /* libavformat.xcframework */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B351E7D02CBE072D0000E087 /* libswresample.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CA2CBE072C0000E087 /* libswresample.xcframework */; platformFilters = (ios, maccatalyst, ); }; - B351E7D12CBE072D0000E087 /* libswresample.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CA2CBE072C0000E087 /* libswresample.xcframework */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B351E7D22CBE072D0000E087 /* libavcodec.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CB2CBE072D0000E087 /* libavcodec.xcframework */; platformFilters = (ios, maccatalyst, ); }; - B351E7D32CBE072D0000E087 /* libavcodec.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CB2CBE072D0000E087 /* libavcodec.xcframework */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B351E7D42CBE072D0000E087 /* libavutil.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CC2CBE072D0000E087 /* libavutil.xcframework */; platformFilters = (ios, maccatalyst, ); }; - B351E7D52CBE072D0000E087 /* libavutil.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CC2CBE072D0000E087 /* libavutil.xcframework */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B351E7D62CBE072D0000E087 /* libswscale.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CD2CBE072D0000E087 /* libswscale.xcframework */; platformFilters = (ios, maccatalyst, ); }; - B351E7D72CBE072E0000E087 /* libswscale.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CD2CBE072D0000E087 /* libswscale.xcframework */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B351E7CE2CBE072D0000E087 /* libavformat.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C92CBE072C0000E087 /* libavformat.xcframework */; platformFilter = ios; }; + B351E7CF2CBE072D0000E087 /* libavformat.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7C92CBE072C0000E087 /* libavformat.xcframework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B351E7D02CBE072D0000E087 /* libswresample.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CA2CBE072C0000E087 /* libswresample.xcframework */; platformFilter = ios; }; + B351E7D12CBE072D0000E087 /* libswresample.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CA2CBE072C0000E087 /* libswresample.xcframework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B351E7D22CBE072D0000E087 /* libavcodec.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CB2CBE072D0000E087 /* libavcodec.xcframework */; platformFilter = ios; }; + B351E7D32CBE072D0000E087 /* libavcodec.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CB2CBE072D0000E087 /* libavcodec.xcframework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B351E7D42CBE072D0000E087 /* libavutil.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CC2CBE072D0000E087 /* libavutil.xcframework */; platformFilter = ios; }; + B351E7D52CBE072D0000E087 /* libavutil.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CC2CBE072D0000E087 /* libavutil.xcframework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B351E7D62CBE072D0000E087 /* libswscale.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CD2CBE072D0000E087 /* libswscale.xcframework */; platformFilter = ios; }; + B351E7D72CBE072E0000E087 /* libswscale.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B351E7CD2CBE072D0000E087 /* libswscale.xcframework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3532C3C21A9AC93006CDA0F /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3532C3B21A9AC93006CDA0F /* Services.swift */; }; B3532C3D21A9AC93006CDA0F /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3532C3B21A9AC93006CDA0F /* Services.swift */; }; B35573742CC4843600A4BA16 /* MoltenVK in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, tvos, ); productRef = B35573732CC4843600A4BA16 /* MoltenVK */; }; @@ -582,8 +587,8 @@ B377EBE62CB2B34C00E9B750 /* PVDesmume2015.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B377EBE42CB2B34C00E9B750 /* PVDesmume2015.framework */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B377ECA82CB33D5600E9B750 /* PVCoreBridgeRetro.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B377ECA72CB33D5600E9B750 /* PVCoreBridgeRetro.framework */; }; B377ECA92CB33D5600E9B750 /* PVCoreBridgeRetro.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B377ECA72CB33D5600E9B750 /* PVCoreBridgeRetro.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B377ECC92CB3533C00E9B750 /* PVVecX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B377ECC82CB3533C00E9B750 /* PVVecX.framework */; }; - B377ECCA2CB3533C00E9B750 /* PVVecX.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B377ECC82CB3533C00E9B750 /* PVVecX.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B377ECC92CB3533C00E9B750 /* PVVecX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B377ECC82CB3533C00E9B750 /* PVVecX.framework */; platformFilters = (ios, tvos, ); }; + B377ECCA2CB3533C00E9B750 /* PVVecX.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B377ECC82CB3533C00E9B750 /* PVVecX.framework */; platformFilters = (ios, tvos, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B37CE669293F23C20010B746 /* PVVecX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B37CE668293F23C20010B746 /* PVVecX.framework */; platformFilters = (ios, maccatalyst, tvos, ); }; B37CE66A293F23C30010B746 /* PVVecX.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B37CE668293F23C20010B746 /* PVVecX.framework */; platformFilters = (ios, maccatalyst, tvos, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B37CE66F293F23F70010B746 /* PVfMSX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B37CE66E293F23F70010B746 /* PVfMSX.framework */; }; @@ -617,7 +622,6 @@ B38FB4B32CBEF961006786C6 /* PVRetroArch.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 934BE9AE296F177B00FB9933 /* PVRetroArch.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B392B3E22CCC5BB700B1C760 /* PVVisualBoyAdvance-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = B392B3E12CCC5BB700B1C760 /* PVVisualBoyAdvance-Dynamic */; }; B39342142CC57837008AA673 /* MoltenVK in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, tvos, ); productRef = B39342132CC57837008AA673 /* MoltenVK */; }; - B39342162CC5784D008AA673 /* MoltenVK-Catalyst in Frameworks */ = {isa = PBXBuildFile; platformFilter = maccatalyst; productRef = B39342152CC5784D008AA673 /* MoltenVK-Catalyst */; }; B39343992CC5C432008AA673 /* MoltenVK-Catalyst in Frameworks */ = {isa = PBXBuildFile; platformFilter = maccatalyst; productRef = B39343982CC5C432008AA673 /* MoltenVK-Catalyst */; }; B394CCBB2BDEE53A006B63E8 /* Result+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A4FB65278FFB6600A65248 /* Result+Conveniences.swift */; }; B394CCD52BDEE53A006B63E8 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3532C3B21A9AC93006CDA0F /* Services.swift */; }; @@ -1012,8 +1016,8 @@ B3FAC9B6292B4785005E8B11 /* PVStella.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B305EF2E276B4CB0003AE510 /* PVStella.framework */; }; B3FAC9B7292B4786005E8B11 /* PVStella.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B305EF2E276B4CB0003AE510 /* PVStella.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3FB9679276DD7F600F7EDEE /* SteamController in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, tvos, ); productRef = B3FB9678276DD7F600F7EDEE /* SteamController */; settings = {ATTRIBUTES = (Weak, ); }; }; - B3FC5AC72CE4376D00D378C9 /* PVEmuThree.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3FC5AC62CE4376D00D378C9 /* PVEmuThree.framework */; platformFilters = (ios, maccatalyst, ); }; - B3FC5AC82CE4376D00D378C9 /* PVEmuThree.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3FC5AC62CE4376D00D378C9 /* PVEmuThree.framework */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B3FC5AC72CE4376D00D378C9 /* PVEmuThree.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3FC5AC62CE4376D00D378C9 /* PVEmuThree.framework */; platformFilter = ios; }; + B3FC5AC82CE4376D00D378C9 /* PVEmuThree.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3FC5AC62CE4376D00D378C9 /* PVEmuThree.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3FC97242CB8B68400C6DB31 /* PVVisualBoyAdvance-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = B3FC97232CB8B68400C6DB31 /* PVVisualBoyAdvance-Dynamic */; }; B3FC97252CB8B68400C6DB31 /* PVVisualBoyAdvance-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = B3FC97232CB8B68400C6DB31 /* PVVisualBoyAdvance-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; B3FC97272CB8B69100C6DB31 /* PVVisualBoyAdvance-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = B3FC97262CB8B69100C6DB31 /* PVVisualBoyAdvance-Dynamic */; }; @@ -1464,6 +1468,7 @@ B3E1C0462C96F84300EB4238 /* PVBliss-Dynamic in Embed Frameworks */, B3A479CF2C96507B0052500A /* PVSNES.framework in Embed Frameworks */, B3F8E0212CB379E500017D1B /* PVGME.framework in Embed Frameworks */, + B31A9FEF2D1FA7440085789A /* libMoltenVK.dylib in Embed Frameworks */, B3E2AA4F2CB638F400E2636D /* PVDesmume2015.framework in Embed Frameworks */, B3FC5AC82CE4376D00D378C9 /* PVEmuThree.framework in Embed Frameworks */, B30ED0DB2CFD3C5200AFCFCE /* PVCoreMednafen-Dynamic in Embed Frameworks */, @@ -2094,7 +2099,6 @@ B3A4FB61278FFA0F00A65248 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; B3A4FB65278FFB6600A65248 /* Result+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Conveniences.swift"; sourceTree = ""; }; B3A60E852CB7707F001D99C2 /* FrameworksRetro */ = {isa = PBXFileReference; lastKnownFileType = folder; name = FrameworksRetro; path = CoresRetro/RetroArch/modules/FrameworksRetro; sourceTree = ""; }; - B3A60E882CB77318001D99C2 /* TODO.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TODO.md; sourceTree = ""; }; B3AD689A1D6EA6180021B949 /* PicoDrive.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PicoDrive.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B3AEDE24293F34740050BDB6 /* PVProSystem.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PVProSystem.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B3AEDE27293F34B30050BDB6 /* PVPicoDrive.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PVPicoDrive.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2761,6 +2765,7 @@ B3EE55DE2C66EBC0004D06E4 /* PVSettings in Frameworks */, B39620982C66E1D70094F6F0 /* PVSupport in Frameworks */, B39620942C66E1C70094F6F0 /* PVLogging in Frameworks */, + B32CEE3F2D20D40C0083552D /* PVFeatureFlags in Frameworks */, B39620922C66E1BE0094F6F0 /* PVCoreBridge in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2784,6 +2789,7 @@ files = ( B3FE6DB22C93E60900792419 /* PVO2EM.framework in Frameworks */, B35EEFB12C6C6D1A00EBD1A8 /* libbz2.tbd in Frameworks */, + B31A9FEE2D1FA7440085789A /* libMoltenVK.dylib in Frameworks */, B33A763B2CC5168C00FC746F /* SiriusRating in Frameworks */, B3FC5AC72CE4376D00D378C9 /* PVEmuThree.framework in Frameworks */, B35EEFB22C6C6D1A00EBD1A8 /* RealmSwift in Frameworks */, @@ -2847,7 +2853,6 @@ B39680492CE720E300E1AB6D /* Perception in Frameworks */, B36C8E7E2C7ECF7700BD4377 /* PVPokeMini-Dynamic in Frameworks */, B3A477C52C941F120052500A /* PVFCEU.framework in Frameworks */, - B39342162CC5784D008AA673 /* MoltenVK-Catalyst in Frameworks */, B351E7CE2CBE072D0000E087 /* libavformat.xcframework in Frameworks */, B31553622CC2FD5500C472CA /* PVPPSSPP.framework in Frameworks */, B35EEFD72C6C6D1A00EBD1A8 /* Metal.framework in Frameworks */, @@ -2983,6 +2988,7 @@ B3F904A72C2002F9002283B3 /* PVCoreBridge in Frameworks */, B3A1867D2CBCB78C00BB1FFE /* PVLibrary in Frameworks */, B3AFCF962977A80700A01010 /* PVLogging in Frameworks */, + B32CEE412D20D4530083552D /* PVFeatureFlags in Frameworks */, 111962BE249FDCD2006EAA16 /* CoreServices.framework in Frameworks */, B3EE55E32C66EC14004D06E4 /* Defaults in Frameworks */, ); @@ -3139,6 +3145,7 @@ B35B177F2CCCDFC7007059CA /* PVAudio in Frameworks */, B3F8214E2C05429400232722 /* PVSupport in Frameworks */, B3AF705A21916F30000FA7F9 /* Foundation.framework in Frameworks */, + B32CEE432D20D45C0083552D /* PVFeatureFlags in Frameworks */, B3AFCF982977A80C00A01010 /* PVLogging in Frameworks */, B35B17852CCCE0BB007059CA /* Defaults in Frameworks */, ); @@ -3154,7 +3161,6 @@ B399119326C0F41300E54426 /* CHANGELOG.md */, B399119126C0F41300E54426 /* LICENSE.md */, B399119226C0F41300E54426 /* README.md */, - B3A60E882CB77318001D99C2 /* TODO.md */, B37769042CF6D47300A48B35 /* UITesting.xcodeproj */, 1A3D409D17B2DCE4004DFFFC /* Provenance */, 1AD481B51BA350A400FDA50A /* ProvenanceTV */, @@ -4424,7 +4430,6 @@ B3FC972F2CB8B6BA00C6DB31 /* PVVisualBoyAdvance-Dynamic */, B33A763A2CC5168C00FC746F /* SiriusRating */, B39342132CC57837008AA673 /* MoltenVK */, - B39342152CC5784D008AA673 /* MoltenVK-Catalyst */, B392B3E12CCC5BB700B1C760 /* PVVisualBoyAdvance-Dynamic */, B37228CB2CCDC7D300E6F627 /* FreemiumKit */, B39680482CE720E300E1AB6D /* Perception */, @@ -4584,6 +4589,7 @@ B39B53892C66E7BA00C220C6 /* PVSettings */, B3EE55E22C66EC14004D06E4 /* Defaults */, B3A1867C2CBCB78C00BB1FFE /* PVLibrary */, + B32CEE402D20D4530083552D /* PVFeatureFlags */, ); productName = Spotlight; productReference = B3E21D6B203211BE009939D3 /* Spotlight.appex */; @@ -4716,6 +4722,7 @@ B35B177E2CCCDFC7007059CA /* PVAudio */, B35B17822CCCE035007059CA /* PVLibrary */, B35B17842CCCE0BB007059CA /* Defaults */, + B32CEE422D20D45C0083552D /* PVFeatureFlags */, ); productName = TopShelf; productReference = BE9FDCB71C210B9E0046DF0E /* TopShelf.appex */; @@ -11300,6 +11307,18 @@ package = B3D1679D2CC481D600EBB132 /* XCLocalSwiftPackageReference "MoltenVK" */; productName = MoltenVK; }; + B32CEE3E2D20D40C0083552D /* PVFeatureFlags */ = { + isa = XCSwiftPackageProductDependency; + productName = PVFeatureFlags; + }; + B32CEE402D20D4530083552D /* PVFeatureFlags */ = { + isa = XCSwiftPackageProductDependency; + productName = PVFeatureFlags; + }; + B32CEE422D20D45C0083552D /* PVFeatureFlags */ = { + isa = XCSwiftPackageProductDependency; + productName = PVFeatureFlags; + }; B32D45BC2CB730AE006B76C2 /* PVGambatte */ = { isa = XCSwiftPackageProductDependency; productName = PVGambatte; @@ -11827,11 +11846,6 @@ package = B3D1679D2CC481D600EBB132 /* XCLocalSwiftPackageReference "MoltenVK" */; productName = MoltenVK; }; - B39342152CC5784D008AA673 /* MoltenVK-Catalyst */ = { - isa = XCSwiftPackageProductDependency; - package = B3D1679D2CC481D600EBB132 /* XCLocalSwiftPackageReference "MoltenVK" */; - productName = "MoltenVK-Catalyst"; - }; B39343982CC5C432008AA673 /* MoltenVK-Catalyst */ = { isa = XCSwiftPackageProductDependency; package = B3D1679D2CC481D600EBB132 /* XCLocalSwiftPackageReference "MoltenVK" */; diff --git a/Provenance/Main UI/PVAppDelegate.swift b/Provenance/Main UI/PVAppDelegate.swift index aa4d2c2ca0..f421b7d4a1 100644 --- a/Provenance/Main UI/PVAppDelegate.swift +++ b/Provenance/Main UI/PVAppDelegate.swift @@ -26,6 +26,7 @@ import Observation import Perception import SwiftUI import Defaults +import PVFeatureFlags // Conditionally import PVJIT and JITManager if available #if canImport(PVJIT) @@ -51,29 +52,29 @@ import FreemiumKit final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationDelegate, ObservableObject { /// This is set by the UIApplicationDelegateAdaptor internal var window: UIWindow? = nil - + static func main() { UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, NSStringFromClass(PVApplication.self), NSStringFromClass(PVAppDelegate.self)) } - + var shortcutItemGame: PVGame? var bootupState: AppBootupState? { appState?.bootupStateManager } - + /// This is set by the ContentView var appState: AppState? { didSet { ILOG("Did set appstate: currently is: \(appState?.bootupStateManager.currentState)") } } - + // Check if the app is running in App Store mode var isAppStore: Bool { guard let appType = Bundle.main.infoDictionary?["PVAppType"] as? String else { return false } return appType.lowercased().contains("appstore") } - + // JIT-related properties for iOS, non-App Store builds with PVJIT support #if os(iOS) && !APP_STORE && canImport(PVJIT) weak var jitScreenDelegate: JitScreenDelegate? @@ -81,7 +82,7 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD var cancellation_token = DOLCancellationToken() var is_presenting_alert = false #endif - + @MainActor weak var rootNavigationVC: UIViewController? = nil @MainActor weak var gameLibraryViewController: PVGameLibraryViewController? = nil { didSet { @@ -92,17 +93,17 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD } } } - + private var cancellables = Set() @MainActor func _initLibraryNotificationHandlers() { ILOG("Initializing library notification handlers") cancellables.forEach { $0.cancel() } - + /// Reimport the library NotificationCenter.default.publisher(for: .PVReimportLibrary) .flatMap { _ in - + Future { promise in Task.detached { @MainActor in RomDatabase.refresh() @@ -114,29 +115,31 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD } } RomDatabase.sharedInstance.recoverAllSaveStates() - #if false - do { - try await AppState.shared.gameLibrary?.romMigrator.fixOrphanedFiles() - try await AppState.shared.gameLibrary?.romMigrator.fixPartialPaths() - - } catch { - ELOG("Error: \(error.localizedDescription)") + if PVFeatureFlagsManager.shared.romPathMigrator { + Task { + do { + try await AppState.shared.gameLibrary?.romMigrator.fixOrphanedFiles() + try await AppState.shared.gameLibrary?.romMigrator.fixPartialPaths() + + } catch { + ELOG("Error: \(error.localizedDescription)") + } + } } - #endif } promise(.success(())) } } .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) - + /// Refresh the library NotificationCenter.default.publisher(for: .PVRefreshLibrary) .flatMap { _ in Future { promise in Task { @MainActor in do { -// try RomDatabase.sharedInstance.deleteAllGames() + // try RomDatabase.sharedInstance.deleteAllGames() if let _ = self.gameLibraryViewController { self.gameLibraryViewController?.checkROMs(false) } else { @@ -145,14 +148,16 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD } } RomDatabase.sharedInstance.recoverAllSaveStates() - #if false - do { - try await AppState.shared.gameLibrary?.romMigrator.fixOrphanedFiles() - try await AppState.shared.gameLibrary?.romMigrator.fixPartialPaths() - } catch { - ELOG("Error: \(error.localizedDescription)") + if PVFeatureFlagsManager.shared.romPathMigrator { + Task { + do { + try await AppState.shared.gameLibrary?.romMigrator.fixOrphanedFiles() + try await AppState.shared.gameLibrary?.romMigrator.fixPartialPaths() + } catch { + ELOG("Error: \(error.localizedDescription)") + } + } } - #endif promise(.success(())) } catch { ELOG("Failed to refresh all objects. \(error.localizedDescription)") @@ -163,7 +168,7 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD } .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) - + /// Reset the library NotificationCenter.default.publisher(for: .PVResetLibrary) .flatMap { _ in @@ -196,20 +201,20 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD func _initUITheme() { ThemeManager.applySavedTheme() themeAppUI(withPalette: ThemeManager.shared.currentPalette) - #if os(tvOS) - UIWindow.appearance().tintColor = .provenanceBlue - #endif +#if os(tvOS) + UIWindow.appearance().tintColor = .provenanceBlue +#endif } - + /// Setup the side navigation fileprivate func setupSideNavigation(mainViewController: UIViewController, - gameLibrary: PVGameLibrary, - viewModel: PVRootViewModel, - rootViewController: PVRootViewController) -> SideNavigationController { + gameLibrary: PVGameLibrary, + viewModel: PVRootViewModel, + rootViewController: PVRootViewController) -> SideNavigationController { let sideNav = SideNavigationController(mainViewController: mainViewController) let traits = UITraitCollection.current let isIpad = UIDevice.current.userInterfaceIdiom == .pad - + /// Calculate width percentage based on device and size class let widthPercentage: CGFloat = { switch (isIpad, traits.horizontalSizeClass) { @@ -221,23 +226,23 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD case (false, .unspecified): return 0.7 // iPhone fallback } }() - + let overlayColor: UIColor = ThemeManager.shared.currentPalette.menuHeaderBackground - + sideNav.leftSide( viewController: SideMenuView.instantiate(gameLibrary: gameLibrary, - viewModel: viewModel, - delegate: rootViewController, - rootDelegate: rootViewController), + viewModel: viewModel, + delegate: rootViewController, + rootDelegate: rootViewController), options: .init(widthPercent: widthPercentage, - animationDuration: 0.18, - overlayColor: overlayColor, - overlayOpacity: 0.1, - shadowOpacity: 0.2) + animationDuration: 0.18, + overlayColor: overlayColor, + overlayOpacity: 0.1, + shadowOpacity: 0.2) ) - + /// Add trait collection observer to update width when orientation changes - #if !os(tvOS) +#if !os(tvOS) NotificationCenter.default.addObserver(forName: UIApplication.didChangeStatusBarOrientationNotification, object: nil, queue: .main) { _ in let newWidth: CGFloat = { switch (isIpad, UITraitCollection.current.horizontalSizeClass) { @@ -251,10 +256,10 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD }() sideNav.updateSideMenuWidth(percent: newWidth) } - #endif +#endif return sideNav } - + /// Setup JIT if needed /// /// This is called from the ContentView @@ -263,22 +268,22 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD private func setupJITIfNeeded() { #if os(iOS) && !APP_STORE if Defaults[.autoJIT] { - DOLJitManager.shared.attemptToAcquireJitOnStartup() - } - DispatchQueue.main.async { [unowned self] in - self.showJITWaitScreen() - } + DOLJitManager.shared.attemptToAcquireJitOnStartup() + } + DispatchQueue.main.async { [unowned self] in + self.showJITWaitScreen() + } #endif - } - + } + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { ILOG("PVAppDelegate: Application did finish launching") - + initializeAppComponents() configureApplication(application) return true } - + // TODO: Move to ProvenanceApp @MainActor private func initializeAppComponents() { @@ -290,33 +295,33 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD _initUITheme() _initThemeListener() } - + private func configureApplication(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) { // Handle if started from shortcut - #if !os(tvOS) +#if !os(tvOS) if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem, shortcut.type == "kRecentGameShortcut", let md5Value = shortcut.userInfo?["PVGameHash"] as? String, let matchedGame = ((try? Realm().object(ofType: PVGame.self, forPrimaryKey: md5Value)) as PVGame??) { shortcutItemGame = matchedGame } - #endif - +#endif + Task { for await value in Defaults.updates(.disableAutoLock) { application.isIdleTimerDisabled = value } } } - + private func initializeAdditionalComponents() { _initSteamControllers() - + #if os(iOS) && !targetEnvironment(macCatalyst) && !APP_STORE ApplicationMonitor.shared.start() #endif } - + private func scheduleDelayedTasks() { DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in self?.startOptionalWebDavServer() @@ -327,14 +332,14 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD #endif } } - + func _initSteamControllers() { #if !targetEnvironment(macCatalyst) && canImport(SteamController) && !targetEnvironment(simulator) // SteamController is built with STEAMCONTROLLER_NO_PRIVATE_API, so we don't call this // SteamControllerManager.listenForConnections() #endif } - + func _initICloud() { PVEmulatorConfiguration.initICloud() DispatchQueue.global(qos: .background).async { @@ -347,11 +352,11 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD } } } - + var currentThemeObservation: Any? // AnyCancellable? var userInterfaceStyleObservation: Any? var oldPalette: (any UXThemePalette)? - + @MainActor func _initThemeListener() { if #available(iOS 17.0, tvOS 17.0, *) { @@ -362,13 +367,13 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD Task.detached { @MainActor in self._initUITheme() if self.isAppStore { - #if !os(tvOS) +#if !os(tvOS) self.appRatingSignifigantEvent() - #endif +#endif } } } - + currentThemeObservation = ThemeManager.shared.$currentPalette .dropFirst() // Skip the initial value .sink { [weak self] newPalette in @@ -400,7 +405,7 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD } } } - + currentThemeObservation = withPerceptionTracking { _ = ThemeManager.shared.currentPalette } onChange: { [unowned self] in @@ -420,7 +425,7 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD } } } - + // TODO: Move to ProvenanceApp func saveCoreState() async throws { if let core = appState?.emulationState.core, core.isOn, let emulator = appState?.emulationState.emulator { @@ -435,7 +440,7 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD #endif } } - + // TODO: Move to ProvenanceApp func pauseCore() { if let core = appState?.emulationState.core, core.isOn && core.isRunning { @@ -448,7 +453,7 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD #endif } } - + // TODO: Move to ProvenanceApp func stopCore() { if let core = appState?.emulationState.core, core.isOn { @@ -456,8 +461,8 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD core.stopEmulation() } } - - + + func applicationWillResignActive(_ application: UIApplication) { let emulationState = appState?.emulationState emulationState?.isInBackground = true @@ -467,52 +472,52 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD try await self.saveCoreState() } } - + // TODO: Move to ProvenanceApp func applicationDidEnterBackground(_ application: UIApplication) { appState?.emulationState.isInBackground = true pauseCore() } - + func applicationWillEnterForeground(_: UIApplication) {} - + // TODO: Move to ProvenanceApp func applicationDidBecomeActive(_ application: UIApplication) { appState?.emulationState.isInBackground = false } - + // TODO: Move to ProvenanceApp func applicationWillTerminate(_ application: UIApplication) { stopCore() } - + @MainActor func setupUIKitInterface() -> UIViewController { guard let appState = appState else { ELOG("`appState` was nil. Never set?") return .init() } - + ILOG("PVAppDelegate: Setting up UIKit interface") let storyboard = UIStoryboard(name: "Provenance", bundle: PVUIKit.BundleLoader.bundle) guard let rootNavigation = storyboard.instantiateInitialViewController() as? UINavigationController else { fatalError("No root nav controller") } - + self.rootNavigationVC = rootNavigation guard let gameLibraryViewController = rootNavigation.viewControllers.first as? PVGameLibraryViewController else { fatalError("No gameLibraryViewController") } - + gameLibraryViewController.updatesController = appState.libraryUpdatesController gameLibraryViewController.gameImporter = appState.gameImporter gameLibraryViewController.gameLibrary = appState.gameLibrary - + self.gameLibraryViewController = gameLibraryViewController - + return rootNavigation } - + @MainActor func setupSwiftUIInterface() -> UIViewController { ILOG("PVAppDelegate: Starting SwiftUI interface setup") @@ -520,10 +525,10 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD ELOG("PVAppDelegate: `appState` was nil. Never set?") return .init() } - + ILOG("PVAppDelegate: AppState is set") let viewModel = PVRootViewModel() - + ILOG("PVAppDelegate: Checking required components") if appState.libraryUpdatesController == nil { ELOG("PVAppDelegate: libraryUpdatesController is nil") @@ -534,20 +539,20 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD if appState.gameImporter == nil { ELOG("PVAppDelegate: gameImporter is nil") } - + guard let libraryUpdatesController = appState.libraryUpdatesController, let gameLibrary = appState.gameLibrary, let gameImporter = appState.gameImporter else { ELOG("PVAppDelegate: Required components in appState are nil") return .init() } - + // Refresh the library -// Task.detached(priority: .background) { -// await libraryUpdatesController.updateConflicts() -// await libraryUpdatesController.importROMDirectories() -// } - + // Task.detached(priority: .background) { + // await libraryUpdatesController.updateConflicts() + // await libraryUpdatesController.importROMDirectories() + // } + ILOG("PVAppDelegate: All required components are available") let rootViewController = PVRootViewController.instantiate( updatesController: libraryUpdatesController, @@ -556,26 +561,26 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD viewModel: viewModel) self.rootNavigationVC = rootViewController let sideNavHostedNavigationController = PVRootViewNavigationController(rootViewController: rootViewController) - + let sideNav = setupSideNavigation(mainViewController: sideNavHostedNavigationController, gameLibrary: gameLibrary, viewModel: viewModel, rootViewController: rootViewController) - + _initLibraryNotificationHandlers() return sideNav } - + private func loadRocketSimConnect() { - #if DEBUG +#if DEBUG guard (Bundle(path: "/Applications/RocketSim.app/Contents/Frameworks/RocketSimConnectLinker.nocache.framework")?.load() == true) else { print("Failed to load linker framework") return } print("RocketSim Connect successfully linked") - #endif +#endif } - + func runDetachedTaskWithCompletion( priority: TaskPriority? = nil, operation: @escaping () async throws -> T, @@ -590,11 +595,11 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD } } } - -// func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { -// let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) -// sceneConfig.delegateClass = PVSceneDelegate.self -// return sceneConfig -// } - + + // func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + // sceneConfig.delegateClass = PVSceneDelegate.self + // return sceneConfig + // } + } diff --git a/TODO.md b/TODO.md deleted file mode 100644 index ab985a4868..0000000000 --- a/TODO.md +++ /dev/null @@ -1,129 +0,0 @@ -# TODO.MD -_My personal TODO notes_ - -## Show stoppers - -- [ ] ugly retroarch ui in app, a bit unresponsive -- [ ] Retroarch core options not showing up - -## Major bugs - -- [ ] Mednafen FDS sometimes crashes on load (it worked then it didn't) -- [ ] Mednafen loads Famicom Disks but gets stuck on bios -- [ ] Opening roms from md5/siri search doesn't work (Claude had good sample code) -- [ ] Artwork ratios are wrong -- [ ] Share on old UI crashes app (realm threading issue) -- [X] Checking Import UI is hanging (removed for now) - - [ ] fix public func addImportedGames(to spotlightIndex: CSSearchableIndex, database: RomDatabase) async -- [ ] Fix layout, layout button and touch controls of Desmume2015 or remove - - Partially done WIP -- [ ] Native scale on mupen shows in wrong area on ipad -- [ ] Flycast crashes with error `NSInvalidArgumentException', reason: '-[MetalView naturalDrawableSizeMVK]: unrecognized selector sent` - - [X] Try downgrading MoltenVK.xcframework to fix dolphin, flycast others -- [ ] (Lite) Intellivision (PVBliss) audio crashes on button press -- [ ] (Lite) Intellivision video is glitched -- [ ] (Lite) Odyssey2 needs a way to enter game number 1,2 (3,4)? -- [ ] (Lite) SuperVision video dimensions wrong -- [ ] (Lite) Turbo GFX 16 no video for some roms -- [ ] (Lite) Sega CD is borked - -## Minor bugs - -- [ ] Atari 2600 not using our controller for retroarch -- [ ] Spotlight no worky, crashes -- [ ] archive extraction HUD doens't show % progress updates -- [ ] Screensots for retroarch cores is the wrong space (3ds too) -- [ ] BIOS importer should work when multiple systems match the same bios -- [ ] Dark mode toggle doens't refresh all views if theme set to auto -- [ ] Box art is clipped in swift ui -- need better aspect ratios -- [ ] Game Info in swift ui crashes on next game scroll -- [ ] Mupen CoreOptions code is trash -- [ ] N64 onscreen controls are kind of high -- [ ] Add a way to delete a bios -- [ ] Add core option for mGBA low pass filter -- [ ] See if psx mednafen has more options -- [ ] Should add loading screen for starting emulator -- [ ] Spotlignt/extensions can't build with spm modules (this is working now? was an xcode bug?) -- [ ] SwiftUI game long press menu missing item - - [ ] View save states - - [ ] Share - - [ ] Hide - - [ ] Choose disc -- [ ] Microphone input for cores that support it -- [ ] Swift UI should open on home and be scrollable to systems -- [ ] When switching from SwiftUI to old UI, the game lib is zoomed way too much, need to change how it uses Scaling Factor Defaults[.gameLibraryScale] -- [ ] should store last page view for next open -- [ ] theme switching doesn't update nav bar color -- [ ] Themes are ugly on old UI - - [ ] Fix default no-artwork background - - [ ] Fix section header colors - - [ ] Top bar not themed - - [ ] Main background not themed - - [ ] Game text not themed - - [ ] New import indicator not themed -- [ ] Make GameMoreInfoVC and it's equivlant PageViewController into native swifttUI with editing of properties -- [ ] App Group containers in Catalyst "public class var appGroupContainer" -- [ ] look at the displaylink thing in retroarch - -## Features to Add - -### Really want - -- [ ] GameImportQueue - - [ ] Pause/play on game start/stop - - [ ] clicking an item should import it - - [ ] indicator, pause/start in queue UI - - [ ] Inidicator, pause/start in main UI -- [ ] Add an option to use AppGroups -- [ ] Gamepad navigation in swiftUI - - Partially implimented -- [ ] iCloud sync -- [ ] Save state share in savestate manager swift ui view -- [X] Add more artwork lookups - -### Kind of want - -- [X] New save states management page - - [X] (New design)[https://discord.com/channels/@me/1034683216059179069/1307885448030326877] - -### Cores to translate / fix - -- [ ] Gearcoloco - - Video bad -- [ ] PVEP128 -- [ ] PVVecX -- [ ] Flycast / Reicast -- [ ] PVFreeIntV -- [ ] PVfMSX -- [ ] Duckstation -- [ ] PCSXRearmed -- [ ] Mupen retroarch no video - -### Retroarch cores to add - -- holani - >>> Holani is a cycle-stepped Atari Lynx video game system emulator that can be used as a libretro core. Holani's primary goal is to get closer to the Lynx hardware and provide a better emulation experience. - https://docs.libretro.com/library/holani/#background -- puae & puea 2021 - Amiga - https://docs.libretro.com/library/puae/ -- bsnes-hd-beta -- neocd - https://docs.libretro.com/library/neocd/ -- melondsds - --------------------------------------- - -## AppStore Review - -- [\] Update screenshots without copyright material - -## Provenance Plus - -## High Priority - -- [ ] Add app rating with SiruisRating - -### Low Priority -- [ ] Finish themes -- [ ] Add Shiragame