diff --git a/Cores/Atari800/Sources/PVAtari800Bridge/PVAtari800Bridge.m b/Cores/Atari800/Sources/PVAtari800Bridge/PVAtari800Bridge.m index 4cbc42270b..d06378fb9a 100644 --- a/Cores/Atari800/Sources/PVAtari800Bridge/PVAtari800Bridge.m +++ b/Cores/Atari800/Sources/PVAtari800Bridge/PVAtari800Bridge.m @@ -733,10 +733,10 @@ - (void)pollControllers { } else if (gamepad.rightThumbstick.down.isPressed) { //4 button INPUT_key_code = AKEY_5200_4; - } else if (gamepad.leftThumbstickButton.isPressed) { + } else if (gamepad.leftThumbstickButton != nil && gamepad.leftThumbstickButton.isPressed) { //5 button INPUT_key_code = AKEY_5200_5; - } else if (gamepad.rightThumbstickButton.isPressed) { + } else if (gamepad.rightThumbstickButton != nil && gamepad.rightThumbstickButton.isPressed) { //6 button INPUT_key_code = AKEY_5200_6; } else if (gamepad.buttonX.isPressed) { diff --git a/Extensions/Spotlight/Spotlight.entitlements b/Extensions/Spotlight/Spotlight.entitlements index 267cbe6e36..d81f3d833e 100644 --- a/Extensions/Spotlight/Spotlight.entitlements +++ b/Extensions/Spotlight/Spotlight.entitlements @@ -6,7 +6,7 @@ com.apple.security.application-groups - group.org.provenance-emu.provenance + $(APP_GROUP_IDENTIFIER) com.apple.security.assets.pictures.read-only diff --git a/Extensions/TopShelf/TopShelf.entitlements b/Extensions/TopShelf/TopShelf.entitlements index 5ae8701f1b..7c83d2b2dd 100644 --- a/Extensions/TopShelf/TopShelf.entitlements +++ b/Extensions/TopShelf/TopShelf.entitlements @@ -6,7 +6,7 @@ development com.apple.developer.icloud-container-identifiers - iCloud.org.provenance.provenance + $(ICLOUD_CONTAINER_IDENTIFIER) com.apple.developer.icloud-services @@ -16,7 +16,7 @@ $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.application-groups - group.org.provenance-emu.provenance + $(APP_GROUP_IDENTIFIER) diff --git a/PVLibrary/Sources/PVLibrary/Database/Notifications.swift b/PVLibrary/Sources/PVLibrary/Database/Notifications.swift index 0c2b7045a5..95cee7c5fe 100644 --- a/PVLibrary/Sources/PVLibrary/Database/Notifications.swift +++ b/PVLibrary/Sources/PVLibrary/Database/Notifications.swift @@ -10,4 +10,6 @@ import Foundation public extension Notification.Name { static let DatabaseMigrationStarted = Notification.Name("DatabaseMigrarionStarted") static let DatabaseMigrationFinished = Notification.Name("DatabaseMigrarionFinished") + static let RomDatabaseInitialized = Notification.Name("RomDatabaseInitialized") + static let RomsFinishedImporting = Notification.Name("RomsFinishedImporting") } diff --git a/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase+Saves.swift b/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase+Saves.swift index e16163a138..ce954c372d 100644 --- a/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase+Saves.swift +++ b/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase+Saves.swift @@ -198,8 +198,8 @@ public extension RomDatabase { guard let core = realm.object(ofType: PVCore.self, forPrimaryKey: core.coreIdentifier) else { throw SaveStateError.noCoreFound(core.coreIdentifier ?? "null") } - let imgFile = PVImageFile(withURL: URL(fileURLWithPath: url.path.replacingOccurrences(of: "svs", with: "jpg"))) - let saveFile = PVFile(withURL: url) + let imgFile = PVImageFile(withURL: URL(fileURLWithPath: url.path.replacingOccurrences(of: "svs", with: "jpg")), relativeRoot: .iCloud) + let saveFile = PVFile(withURL: url, relativeRoot: .iCloud) let newState = PVSaveState(withGame: game, core: core, file: saveFile, image: imgFile, isAutosave: false) try realm.write { realm.add(newState) diff --git a/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase.swift b/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase.swift index 8861c7e5c1..fcd90394e2 100644 --- a/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase.swift +++ b/PVLibrary/Sources/PVLibrary/Database/Realm Database/RomDatabase.swift @@ -341,6 +341,7 @@ public final class RomDatabase { ILOG("Database initialization completed") databaseInitialized = true + NotificationCenter.default.post(name: .RomDatabaseInitialized, object: nil, userInfo: nil) } else { ILOG("Database already initialized") } @@ -762,6 +763,9 @@ public extension RomDatabase { #if os(iOS) deleteFromSpotlight(game: game) #endif + defer { + RomDatabase.reloadGamesCache() + } do { deleteRelatedFilesGame(game) game.saveStates.forEach { try? $0.delete() } @@ -771,7 +775,8 @@ public extension RomDatabase { try game.delete() } catch { // Delete the DB entry anyway if any of the above files couldn't be removed - do { try game.delete() } catch { + do { try game.delete() + } catch { ELOG("\(error.localizedDescription)") } ELOG("\(error.localizedDescription)") @@ -824,6 +829,15 @@ public extension RomDatabase { throw RomDeletionError.fileManagerDeletionError(error) } } + if let jsonFile = actualSavePath?.pathDecoded.appending(".json"), + FileManager.default.fileExists(atPath: jsonFile) { + do { + try FileManager.default.removeItem(atPath: jsonFile) + } catch { + ELOG("Unable to delete json at path: \(jsonFile) because: \(error.localizedDescription)") + throw RomDeletionError.fileManagerDeletionError(error) + } + } } func deleteRelatedFilesGame(_ game: PVGame) { @@ -841,6 +855,48 @@ public extension RomDatabase { ELOG(error.localizedDescription) } } + //attempt to delete files with the same name. There's an issue when importing that the files do NOT get associated, so if we assume the user imported properly, the name should just be the same with different extensions. + let fileManager: FileManager = .default + guard let gameFileUrl = game.file?.url + else { + return + } + let parentDirectory = gameFileUrl.deletingLastPathComponent() + guard fileManager.fileExists(atPath: parentDirectory.pathDecoded) + else { + return + } + let children: [String] + do { + children = try fileManager.subpathsOfDirectory(atPath: parentDirectory.pathDecoded) + } catch { + ELOG("error retrieving files at directory: \(parentDirectory), \(error)") + return + } + guard !children.isEmpty + else { + return + } + DLOG("children: \(children)") + let fileName = gameFileUrl.deletingPathExtension().lastPathComponent + DLOG("fileName without extension: \(fileName)") + children.forEach { child in + let currentChildUrl = parentDirectory.appendingPathComponent(child) + let currentExtension = currentChildUrl.pathExtension + let currentChildFileName = currentChildUrl.deletingPathExtension().lastPathComponent + DLOG("current extension: \(currentExtension), current file name: \(currentChildFileName), current url: \(currentChildUrl)") + if !currentExtension.isEmpty + && (currentChildFileName == fileName + || currentChildFileName.starts(with: "\(fileName) (Track ") + || currentChildFileName.starts(with: "\(fileName) (Disc ") + ) { + do { + try fileManager.removeItem(at: currentChildUrl) + } catch { + ELOG("error deleting file: \(currentChildUrl)") + } + } + } } } diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporter.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporter.swift index c4e94f0e84..e8208910f4 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporter.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporter.swift @@ -423,6 +423,27 @@ public final class GameImporter: GameImporting, ObservableObject { importQueue.remove(atOffsets: offsets) } + + /// Searches for successful imports filtered by files and removes from importQueue and files. This is so that only files imported by iCloud can be removed + /// - Parameter files: set of files to check + public func removeSuccessfulImports(from files: inout Set) { + guard !files.isEmpty + else { + return + } + importQueueLock.lock() + defer { + importQueueLock.unlock() + } + let offsets = IndexSet(importQueue.enumerated().compactMap { index, item in + if item.status == .success && files.contains(item.url) { + files.remove(item.url) + return index + } + return nil + }) + importQueue.remove(atOffsets: offsets) + } // Public method to manually start processing if needed public func startProcessing() { @@ -658,15 +679,18 @@ public final class GameImporter: GameImporting, ObservableObject { // Processes each ImportItem in the queue sequentially private func processQueue() async { + defer { + DispatchQueue.main.async { + self.processingState = .idle + NotificationCenter.default.post(name: .RomsFinishedImporting, object: nil) + } + } // Check for items that are either queued or have a user-chosen system let itemsToProcess = importQueue.filter { $0.status == .queued || $0.userChosenSystem != nil } guard !itemsToProcess.isEmpty else { - DispatchQueue.main.async { - self.processingState = .idle - } return } @@ -683,9 +707,6 @@ public final class GameImporter: GameImporting, ObservableObject { await processItem(item) } - DispatchQueue.main.async { - self.processingState = .idle - } ILOG("GameImportQueue - processQueue complete Import Processing") } diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift index 3cf6083fb5..b0909b111f 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift @@ -135,7 +135,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing { throw GameImporterError.noSystemMatched } - let file = PVFile(withURL: queueItem.destinationUrl!) + let file = PVFile(withURL: queueItem.destinationUrl!, relativeRoot: .iCloud) let game = PVGame(withFile: file, system: system) game.romPath = partialPath game.title = title @@ -205,7 +205,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing { } if PVMediaCache.fileExists(forKey: url) { if let localURL = PVMediaCache.filePath(forKey: url) { - let file = PVImageFile(withURL: localURL, relativeRoot: .iCloud) + let file = PVImageFile(withURL: localURL, relativeRoot: .documents) game.originalArtworkFile = file return game } @@ -229,7 +229,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing { if let artwork = NSImage(data: data) { do { let localURL = try PVMediaCache.writeImage(toDisk: artwork, withKey: url) - let file = PVImageFile(withURL: localURL, relativeRoot: .iCloud) + let file = PVImageFile(withURL: localURL, relativeRoot: .documents) game.originalArtworkFile = file } catch { ELOG("\(error.localizedDescription)") } } @@ -237,7 +237,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing { if let artwork = UIImage(data: data) { do { let localURL = try PVMediaCache.writeImage(toDisk: artwork, withKey: url) - let file = PVImageFile(withURL: localURL, relativeRoot: .iCloud) + let file = PVImageFile(withURL: localURL, relativeRoot: .documents) game.originalArtworkFile = file } catch { ELOG("\(error.localizedDescription)") } } diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterSystemsService.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterSystemsService.swift index 74860f2787..f2af6ddecb 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterSystemsService.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterSystemsService.swift @@ -42,7 +42,12 @@ class GameImporterSystemsService: GameImporterSystemsServicing { } func determineSystems(for item: ImportQueueItem) async throws -> [SystemIdentifier] { - // First try MD5 lookup + // if syncing from icloud, we have the system, so try to get the system this way + if let system = SystemIdentifier(rawValue: item.url.deletingLastPathComponent().lastPathComponent) { + DLOG("found system: \(system)") + return [system] + } + // next try MD5 lookup if let md5 = item.md5 { if let systemID = try await lookup.systemIdentifier(forRomMD5: md5, or: item.url.lastPathComponent) { return [systemID] diff --git a/PVLibrary/Sources/PVLibrary/Importer/iCloud/iCloudSync.swift b/PVLibrary/Sources/PVLibrary/Importer/iCloud/iCloudSync.swift index c934fb565f..962ffb6ace 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/iCloud/iCloudSync.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/iCloud/iCloudSync.swift @@ -25,6 +25,7 @@ public enum SyncResult { case saveFailure case fileNotExist case success + case indeterminate } public protocol Container { @@ -36,244 +37,390 @@ extension Container { var documentsURL: URL? { get { return URL.iCloudDocumentsDirectory }} } -public protocol SyncFileToiCloud: Container { - var metadataQuery: NSMetadataQuery { get } - func syncToiCloud(completionHandler: @escaping (SyncResult) -> Void) // -> Single - func queryFile(completionHandler: @escaping (URL?) -> Void) // -> Single - func downloadingFile(completionHandler: @escaping (SyncResult) -> Void) // -> Single -} - public protocol iCloudTypeSyncer: Container { + var directories: Set { get } var metadataQuery: NSMetadataQuery { get } - var metadataQueryPredicate: NSPredicate { get } // ex NSPredicate(format: "%K like 'PHOTO*'", NSMetadataItemFSNameKey) - func loadAllFromICloud() -> Completable - func removeAllFromICloud() -> Completable + func loadAllFromICloud(iterationComplete: (() -> Void)?) -> Completable + func insertDownloadingFile(_ file: URL) + func insertDownloadedFile(_ file: URL) + func insertUploadedFile(_ file: URL) + func deleteFromDatastore(_ file: URL) + func setNewCloudFilesAvailable() } -final class NotificationObserver { +enum iCloudSyncStatus { + case initialUpload + case filesAlreadyMoved +} - var name: Notification.Name - var observer: NSObjectProtocol - var center = NotificationCenter.default - var object: Any? +enum GamePurgeStatus { + case incomplete + case complete +} - init(forName name: Notification.Name, object: Any? = nil, queue: OperationQueue? = nil, block: @escaping (Notification) -> Void) { - self.name = name - observer = center.addObserver(forName: name, object: object, queue: queue, using: block) +class iCloudContainerSyncer: iCloudTypeSyncer { + lazy var pendingFilesToDownload: Set = [] + lazy var newFiles: Set = [] + lazy var uploadedFiles: Set = [] + let directories: Set + let fileManager = FileManager.default + let notificationCenter: NotificationCenter + var status: iCloudSyncStatus = .initialUpload + let errorHandler: ErrorHandler + var initialSyncResult: SyncResult = .indeterminate + let queue = DispatchQueue(label: "com.provenance.newFiles") + let fileImportQueueMaxCount = 100 + + init(directories: Set, + notificationCenter: NotificationCenter, + errorHandler: ErrorHandler) { + self.notificationCenter = notificationCenter + self.directories = directories + self.errorHandler = errorHandler } - + deinit { - center.removeObserver(observer, name: name, object: object) + metadataQuery.disableUpdates() + metadataQuery.stop() + notificationCenter.removeObserver(self, name: .NSMetadataQueryDidFinishGathering, object: metadataQuery) + notificationCenter.removeObserver(self, name: .NSMetadataQueryDidUpdate, object: metadataQuery) + let removed = removeFromiCloud() + DLOG("removed: \(removed)") + DLOG("dying") } -} - -extension iCloudTypeSyncer { - public func loadAllFromICloud() -> Completable { - return Completable.create { completable in - Task { - guard self.containerURL != nil else { - completable(.error(SyncError.noUbiquityURL)) - return Disposables.create {} + + func printCurrentThread(function: String = #function) { + DLOG("\(function): current thread main? \(Thread.isMainThread)") + } + + var localAndCloudDirectories: [URL: URL] { + var alliCloudDirectories = [URL: URL]() + guard let actualContainrUrl = containerURL + else { + return alliCloudDirectories + } + let parentContainer = actualContainrUrl.appendDocumentsDirectory + directories.forEach { directory in + alliCloudDirectories[URL.documentsDirectory.appendingPathComponent(directory)] = parentContainer.appendingPathComponent(directory) + } + return alliCloudDirectories + } + + let metadataQuery: NSMetadataQuery = .init() + + func insertDownloadingFile(_ file: URL) { + guard !uploadedFiles.contains(file) + else { + return + } + pendingFilesToDownload.insert(file.absoluteString) + } + + func insertDownloadedFile(_ file: URL) { + pendingFilesToDownload.remove(file.absoluteString) + } + + func insertUploadedFile(_ file: URL) { + uploadedFiles.insert(file) + } + + func setNewCloudFilesAvailable() { + if pendingFilesToDownload.isEmpty { + status = .filesAlreadyMoved + DLOG("set status to \(status) and removing all uploaded files in \(directories)") + uploadedFiles.removeAll() + } + } + + func deleteFromDatastore(_ file: URL) { + //no-op + } + + func prepareNextBatchToProcess() -> any Collection { + DLOG("\(directories): newFiles: (\(newFiles.count)):") + DLOG("\(directories): \(newFiles)") + let nextFilesToProcess = newFiles.prefix(fileImportQueueMaxCount) + newFiles.subtract(nextFilesToProcess) + DLOG("\(directories): newFiles minus processing files: (\(newFiles.count)):") + DLOG("\(directories): \(newFiles)") + if newFiles.isEmpty { + uploadedFiles.removeAll() + } + return nextFilesToProcess + } + + func loadAllFromICloud(iterationComplete: (() -> Void)? = nil) -> Completable { + return Completable.create { [weak self] completable in + self?.setupObservers(completable: completable, iterationComplete: iterationComplete) + return Disposables.create() + } + } + + func setupObservers(completable: PrimitiveSequenceType.CompletableObserver, iterationComplete: (() -> Void)? = nil) { + guard containerURL != nil, + directories.count > 0 + else { + completable(.error(SyncError.noUbiquityURL)) + return + } + + initialSyncResult = syncToiCloud() + DLOG("syncToiCloud result: \(initialSyncResult)") + guard initialSyncResult != .saveFailure, + initialSyncResult != .denied + else { + ELOG("error moving files to iCloud container") + return + } + + metadataQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] + DLOG("directories: \(directories)") + var predicateFormat = "" + var predicateArgs = [CVarArg]() + directories.forEach { directory in + if !predicateFormat.isEmpty { + predicateFormat += " OR " + } + predicateFormat += "%K CONTAINS[c] %@" + predicateArgs.append(NSMetadataItemPathKey) + predicateArgs.append("/Documents/\(directory)/") + } + metadataQuery.predicate = NSPredicate(format: predicateFormat, argumentArray: predicateArgs) + //TODO: update to use Publishers.MergeMany + notificationCenter.addObserver( + forName: .NSMetadataQueryDidFinishGathering, + object: metadataQuery, + queue: nil) { [weak self] notification in + Task { + await self?.queryFinished(notification: notification) + iterationComplete?() } - return Disposables.create {} } - // metadataQuery = NSMetadataQuery() - self.metadataQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] - self.metadataQuery.predicate = self.metadataQueryPredicate - - let _: NotificationObserver = .init( - forName: Notification.Name.NSMetadataQueryDidFinishGathering, - object: self.metadataQuery, - queue: nil) {[self] notification in - self.queryFinished(notification: notification) - completable(.completed) + //listen for deletions and new files. what about conflicts? + notificationCenter.addObserver( + forName: .NSMetadataQueryDidUpdate, + object: metadataQuery, + queue: nil) { [weak self] notification in + Task { + await self?.queryFinished(notification: notification) + iterationComplete?() } - - self.metadataQuery.start() - return Disposables.create {} + } + //TODO: unsure if the Task doesn't work with NSMetadataQuery or if there's some other issue. +// Task { @MainActor [weak self] in + DispatchQueue.main.async { [weak self] in + self?.metadataQuery.start() } } - - public func removeAllFromICloud() -> Completable { - return Completable.create { completable in - Task { - - guard self.containerURL != nil else { - completable(.error(SyncError.noUbiquityURL)) - return Disposables.create {} + + func queryFinished(notification: Notification) async { + DLOG("directories: \(directories)") + guard (notification.object as? NSMetadataQuery) == metadataQuery + else { + return + } + let fileManager = FileManager.default + var files: Set = [] + var filesDownloaded: Set = [] + let removedObjects = notification.userInfo?[NSMetadataQueryUpdateRemovedItemsKey] + if let actualRemovedObjects = removedObjects as? [NSMetadataItem] { + DLOG("\(directories): actualRemovedObjects: (\(actualRemovedObjects.count)) \(actualRemovedObjects)") + await actualRemovedObjects.concurrentForEach { [weak self] item in + if let file = item.value(forAttribute: NSMetadataItemURLKey) as? URL { + DLOG("file DELETED from iCloud: \(file)") + self?.deleteFromDatastore(file) } - return Disposables.create {} } - // metadataQuery = NSMetadataQuery() - self.metadataQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] - self.metadataQuery.predicate = self.metadataQueryPredicate - - let token: NSObjectProtocol? = NotificationCenter.default.addObserver( - forName: Notification.Name.NSMetadataQueryDidFinishGathering, - object: self.metadataQuery, - queue: nil) { notification in - self.removeQueryFinished(notification: notification) -// if let token = token { -// NotificationCenter.default.removeObserver(token) -// } - completable(.completed) - } - -// token = NotificationCenter.default.addObserver( -// forName: Notification.Name.NSMetadataQueryDidUpdate, -// object: self.metadataQuery, -// queue: nil) { notification in -// self.queryFinished(notification: notification) -// } - self.metadataQuery.start() - return Disposables.create { - if let token = token { - NotificationCenter.default.removeObserver(token) + } + DLOG("\(directories) -> number of items: \(metadataQuery.results.count)") + //accessing results automatically pauses updates and resumes after deallocated + /*await*/ metadataQuery.results.forEach/*concurrentForEach*/ { [weak self] item in + if let fileItem = item as? NSMetadataItem, + let file = fileItem.value(forAttribute: NSMetadataItemURLKey) as? URL, + let isDirectory = try? file.resourceValues(forKeys: [.isDirectoryKey]).isDirectory, + !isDirectory,//we only + let downloadStatus = fileItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String { + DLOG("Found: \(file), download status: \(downloadStatus)") + switch downloadStatus { + case NSMetadataUbiquitousItemDownloadingStatusNotDownloaded: + do { + try fileManager.startDownloadingUbiquitousItem(at: file) + //self?.queue.async(flags: .barrier) { + files.insert(file) + self?.insertDownloadingFile(file) + //} + DLOG("Download started for: \(file)") + } catch { + self?.errorHandler.handleError(error, file: file) + ELOG("Failed to start download on file \(file): \(error)") + } + case NSMetadataUbiquitousItemDownloadingStatusCurrent: + DLOG("item up to date: \(file)") + if !fileManager.fileExists(atPath: file.pathDecoded) { + DLOG("file DELETED from iCloud: \(file)") + self?.deleteFromDatastore(file) + } else { + //self?.queue.async(flags: .barrier) { + //in the case when we are initially turning on iCloud or the app is opened and coming into the foreground for the first time, we try to import any files already downloaded + if self?.status == .initialUpload { + self?.insertDownloadingFile(file) + } + filesDownloaded.insert(file) + self?.insertDownloadedFile(file) + //} + } + default: DLOG("\(file): download status: \(downloadStatus)") } } } + //TODO: for ROMs and saves, perhaps we need to store the downloaded files that need to be process in the case of a crash or the user puts the app in the background. + setNewCloudFilesAvailable() + DLOG("\(directories): current iteration: files pending to be downloaded: \(files.count), files downloaded : \(filesDownloaded.count)") } - - func removeQueryFinished(notification: Notification) { - let mq = notification.object as! NSMetadataQuery - mq.disableUpdates() - mq.stop() - - for i in 0.. SyncResult { + let allDirectories = localAndCloudDirectories + guard allDirectories.count > 0 + else { + return .denied + } + var moveResult: SyncResult? = nil + allDirectories.forEach { (localDirectory: URL, iCloudDirectory: URL) in + + let moved = moveFiles(at: localDirectory, + containerDestination: iCloudDirectory, + existingClosure: { existing in + do { + try fileManager.removeItem(atPath: existing.pathDecoded) + } catch { + errorHandler.handleError(error, file: existing) + ELOG("error deleting existing file \(existing) that already exists in iCloud: \(error)") + } + }) { currentSource, currentDestination in + try fileManager.setUbiquitous(true, itemAt: currentSource, destinationURL: currentDestination) + } + if moved == .saveFailure { + moveResult = .saveFailure } } + return moveResult ?? .success } - - func queryFinished(notification: Notification) { - let mq = notification.object as! NSMetadataQuery - mq.disableUpdates() - mq.stop() - - // for i in 0 ..< mq.resultCount { - // let result = mq.result(at: i) as! NSMetadataItem - // let name = result.value(forAttribute: NSMetadataItemFSNameKey) as! String - // let url = result.value(forAttribute: NSMetadataItemURLKey) as! URL - // TODO: Some kind of observable rx? - // let document: Self.Type! = DocumentPhoto(fileURL: url) - // document?.open(completionHandler: {(success) -> Void in - // - // if (success) { - // print("Image loaded with name \(name)") - // self.cells.append(document.image) - // self.collectionView.reloadData() - // } - // }) - // } + + func removeFromiCloud() -> SyncResult { + let allDirectories = localAndCloudDirectories + guard allDirectories.count > 0 + else { + return .denied + } + var moveResult: SyncResult? + allDirectories.forEach { (localDirectory: URL, iCloudDirectory: URL) in + let moved = moveFiles(at: iCloudDirectory, + containerDestination: localDirectory, + existingClosure: { existing in + do { + try fileManager.evictUbiquitousItem(at: existing) + } catch {//this happens when a file is being presented on the UI (saved states image) and thus we can't remove the icloud download + errorHandler.handleError(error, file: existing) + ELOG("error evicting iCloud file: \(existing), \(error)") + } + }) { currentSource, currentDestination in + try fileManager.copyItem(at: currentSource, to: currentDestination) + do { + try fileManager.evictUbiquitousItem(at: currentSource) + } catch {//this happens when a file is being presented on the UI (saved states image) and thus we can't remove the icloud download + errorHandler.handleError(error, file: currentSource) + ELOG("error evicting iCloud file: \(currentSource), \(error)") + } + } + if moved == .saveFailure { + moveResult = .saveFailure + } + } + return moveResult ?? .success } -} - -extension SyncFileToiCloud where Self: LocalFileInfoProvider { - private var destinationURL: URL? { get async { - await Task { - guard let containerURL = containerURL, let relativePath = url?.relativePath else { return nil } - return containerURL.appendingPathComponent(relativePath) - }.value - }} - - func syncToiCloud() async -> SyncResult { - await Task { - guard let destinationURL = await self.destinationURL else { - return SyncResult.denied + + func moveFiles(at source: URL, + containerDestination: URL, + existingClosure: ((URL) -> Void), + moveClosure: (URL, URL) throws -> Void) -> SyncResult { + DLOG("source: \(source)") + guard fileManager.fileExists(atPath: source.pathDecoded) + else { + return .fileNotExist + } + let subdirectories: [String] + do { + subdirectories = try fileManager.subpathsOfDirectory(atPath: source.pathDecoded) + } catch { + errorHandler.handleError(error, file: source) + ELOG("failed to get directory contents \(source): \(error)") + return .saveFailure + } + DLOG("subdirectories of \(source): \(subdirectories)") + for currentChild in subdirectories { + let currentItem = source.appendingPathComponent(currentChild) + + var isDirectory: ObjCBool = false + let exists = fileManager.fileExists(atPath: currentItem.pathDecoded, isDirectory: &isDirectory) + DLOG("\(currentItem) isDirectory?\(isDirectory) exists?\(exists)") + let destination = containerDestination.appendingPathComponent(currentChild) + DLOG("new destination: \(destination)") + if isDirectory.boolValue && !fileManager.fileExists(atPath: destination.pathDecoded) { + DLOG("\(destination) does NOT exist") + do { + try fileManager.createDirectory(atPath: destination.pathDecoded, withIntermediateDirectories: true) + } catch { + errorHandler.handleError(error, file: destination) + ELOG("error creating directory: \(destination), \(error)") + } } - - let url = self.url - - self.metadataQuery.disableUpdates() - defer { - self.metadataQuery.enableUpdates() + if isDirectory.boolValue { + continue } - - let fm = FileManager.default - if let url = url, fm.fileExists(atPath: url.path) { - try! await fm.removeItem(at: url) + if fileManager.fileExists(atPath: destination.pathDecoded) { + existingClosure(currentItem) + continue } - do { - ILOG("Trying to set Ubiquitious from local (\(url?.path ?? "")) to ICloud (\(destinationURL.path))") - if let url = url { - try fm.setUbiquitous(true, itemAt: url, destinationURL: destinationURL) - } - return .success + ILOG("Trying to move \(currentItem.pathDecoded) to \(destination.pathDecoded)") + try moveClosure(currentItem, destination) + insertUploadedFile(destination) } catch { - ELOG("iCloud failed to set Ubiquitous: \(error.localizedDescription)") - return .saveFailure + errorHandler.handleError(error, file: currentItem) + //this could indicate no more space is left when moving to iCloud + ELOG("failed to move \(currentItem.pathDecoded) to \(destination.pathDecoded): \(error)") } - }.value - } - - /// - Parameter completionHandler: Non-main - func queryFile(completionHandler: @escaping (URL?) -> Void) { - metadataQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] - - let center = NotificationCenter.default - - center.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: metadataQuery, queue: nil) { _ in - // guard let `self` = self else {return} - - self.metadataQuery.disableUpdates() - defer { - self.metadataQuery.enableUpdates() - } - - guard self.metadataQuery.resultCount >= 1, - let item = self.metadataQuery.results.first as? NSMetadataItem, - let fileURL = item.value(forAttribute: NSMetadataItemURLKey) as? URL - else { - self.metadataQuery.enableUpdates() - return completionHandler(nil) - } - - completionHandler(fileURL) - self.metadataQuery.enableUpdates() } - - metadataQuery.start() + return .success } +} - /// - Parameters: - /// - completionHandler: Non-main - func downloadingFile(completionHandler: @escaping (SyncResult) -> Void) async { - guard let destinationURL = await destinationURL else { - completionHandler(.denied) - return - } - - DispatchQueue.global(qos: .utility).async { - if !FileManager.default.isUbiquitousItem(at: destinationURL) { - completionHandler(.fileNotExist) - return - } - - self.metadataQuery.disableUpdates() - defer { - self.metadataQuery.enableUpdates() - } +extension Int64 { + var toGb: String { + String(format: "%.2f GBs", Double(self / (1024 * 1024 * 1024))) + } +} - let fm = FileManager.default +extension URL { + /// calls URL.path(percentEncoded: false) which is the same as the upcoming deprecation of URL.path + var pathDecoded: String { + path(percentEncoded: false) + } + var appendDocumentsDirectory: URL { + appendingPathComponent("Documents") + } +} - do { - // TODO: Should really wait and listen for it to finish downloading, this call is async - try fm.startDownloadingUbiquitousItem(at: destinationURL) - completionHandler(.success) - } catch { - ELOG("iCloud Download error: \(error.localizedDescription)") - completionHandler(.saveFailure) - return - } +extension Realm { + func deleteGame(_ game: PVGame) throws { + try write { + game.saveStates.forEach { try? $0.delete() } + game.cheats.forEach { try? $0.delete() } + game.recentPlays.forEach { try? $0.delete() } + game.screenShots.forEach { try? $0.delete() } + delete(game) + RomDatabase.reloadGamesCache() } } } @@ -283,123 +430,219 @@ enum iCloudError: Error { } public enum iCloudSync { - static var disposeBag: DisposeBag? + case initialAppLoad + case appLoaded + + static var disposeBag: DisposeBag! + static var gameImporter = GameImporter.shared + static var state: iCloudSync = .initialAppLoad + static let errorHandler: ErrorHandler = iCloudErrorHandler.shared + public static func initICloudDocuments() { + Task { + for await value in Defaults.updates(.iCloudSync) { + iCloudSyncChanged(value) + } + } + } + + static func iCloudSyncChanged(_ newValue: Bool) { + DLOG("new iCloudSync value: \(newValue)") + guard newValue + else { + turnOff() + return + } + + guard URL.supportsICloud else { + DLOG("attempted to turn on iCloud, but iCloud is NOT setup on the device") + return + } + turnOn() + } + + static func turnOn() { + DLOG("turning on iCloud") + //reset ROMs path + gameImporter.gameImporterDatabaseService.setRomsPath(url: gameImporter.romsPath) + errorHandler.clear() let fm = FileManager.default if let currentiCloudToken = fm.ubiquityIdentityToken { do { let newTokenData = try NSKeyedArchiver.archivedData(withRootObject: currentiCloudToken, requiringSecureCoding: false) UserDefaults.standard.set(newTokenData, forKey: UbiquityIdentityTokenKey) } catch { - ELOG("\(error.localizedDescription)") + errorHandler.handleError(error, file: nil) + ELOG("error serializing iCloud token: \(error)") } } else { UserDefaults.standard.removeObject(forKey: UbiquityIdentityTokenKey) } - let saveStateSyncer = SaveStateSyncer() - let disposeBag = DisposeBag() - self.disposeBag = disposeBag - saveStateSyncer.loadAllFromICloud() + //TODO: should we pause when a game starts so we don't interfere with the game and continue listening when no game is running? + disposeBag = DisposeBag() + var nonDatabaseFileSyncer: iCloudContainerSyncer! = .init(directories: ["BIOS", "Battery States", "Screenshots"], + notificationCenter: .default, + errorHandler: iCloudErrorHandler.shared) + nonDatabaseFileSyncer.loadAllFromICloud() .observe(on: MainScheduler.instance) - .subscribe(onCompleted: { - importNewSaves() - self.disposeBag = nil - }) { error in - ELOG("\(error.localizedDescription)") + .subscribe(onError: { error in + ELOG(error.localizedDescription) + }) { + DLOG("disposing nonDatabaseFileSyncer") + nonDatabaseFileSyncer = nil + }.disposed(by: disposeBag) + var saveStateSyncer: SaveStateSyncer! = .init(notificationCenter: .default, errorHandler: iCloudErrorHandler.shared) + saveStateSyncer.loadAllFromICloud() { + saveStateSyncer.importNewSaves() + }.observe(on: MainScheduler.instance) + .subscribe(onError: { error in + ELOG(error.localizedDescription) + }) { + DLOG("disposing saveStateSyncer") + saveStateSyncer = nil + }.disposed(by: disposeBag) + + var romsSyncer: RomsSyncer! = .init(notificationCenter: .default, errorHandler: iCloudErrorHandler.shared) + romsSyncer.loadAllFromICloud() { + romsSyncer.handleImportNewRomFiles() + }.observe(on: MainScheduler.instance) + .subscribe(onError: { error in + ELOG(error.localizedDescription) + }) { + DLOG("disposing romsSyncer") + romsSyncer = nil }.disposed(by: disposeBag) } + + static func turnOff() { + DLOG("turning off iCloud") + errorHandler.clear() + disposeBag = nil + //reset ROMs path + gameImporter.gameImporterDatabaseService.setRomsPath(url: gameImporter.romsPath) + } +} - public static func importNewSaves() { - if !RomDatabase.databaseInitialized { - // Keep trying // TODO: Add a notification for this - // instead of dumb loop - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - self.importNewSaves() - } +//MARK: - iCloud syncers + +//TODO: perhaps 1 generic class since a lot of this code is similar and move the extension onto generic class. we could just add a protocol delegate dependency for ROMs and SaveState classes that does specific code +class SaveStateSyncer: iCloudContainerSyncer { + let jsonDecorder = JSONDecoder() + convenience init(notificationCenter: NotificationCenter, errorHandler: ErrorHandler) { + self.init(directories: ["Save States"], notificationCenter: notificationCenter, errorHandler: errorHandler) + jsonDecorder.dataDecodingStrategy = .deferredToData + notificationCenter.addObserver(forName: .RomDatabaseInitialized, object: nil, queue: nil) { [weak self] _ in + self?.importNewSaves() + } + } + + deinit { + notificationCenter.removeObserver(self) + } + + override func insertDownloadedFile(_ file: URL) { + guard let _ = pendingFilesToDownload.remove(file.absoluteString), + "json".caseInsensitiveCompare(file.pathExtension) == .orderedSame + else { return } - - Task { - let savesDirectory = Paths.saveSavesPath - let legacySavesDirectory = Paths.Legacy.saveSavesPath - let fm = FileManager.default - guard let subDirs = try? fm.contentsOfDirectory(at: savesDirectory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) else { - ELOG("Failed to read saves path: \(savesDirectory.path)") + DLOG("downloaded save file: \(file)") + let newFilesCount: Int = queue.sync { [weak self] in + self?.newFiles.insert(file) + return self?.newFiles.count ?? 0 + } + if newFilesCount >= fileImportQueueMaxCount { + importNewSaves() + } + } + + override func deleteFromDatastore(_ file: URL) { + guard "jpg".caseInsensitiveCompare(file.pathExtension) == .orderedSame + else { + return + } + do { + let realm = try Realm() + DLOG("attempting to query PVSaveState by file: \(file)") + let gameDirectory = file.deletingLastPathComponent().lastPathComponent + let savesDirectory = file.deletingLastPathComponent().deletingLastPathComponent().lastPathComponent + let partialPath = "\(savesDirectory)/\(gameDirectory)/\(file.lastPathComponent)" + let imageField = NSExpression(forKeyPath: \PVSaveState.image.self).keyPath + let partialPathField = NSExpression(forKeyPath: \PVImageFile.partialPath.self).keyPath + let results = realm.objects(PVSaveState.self).filter(NSPredicate(format: "\(imageField).\(partialPathField) CONTAINS[c] %@", partialPath)) + DLOG("saves found: \(results.count)") + guard let save: PVSaveState = results.first + else { return } - - let saveFiles = subDirs.compactMap { - try? fm.contentsOfDirectory(at: $0, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) - }.joined() - let jsonFiles = saveFiles.filter { $0.pathExtension == "json" } - let jsonDecorder = JSONDecoder() - jsonDecorder.dataDecodingStrategy = .deferredToData - - let legacySubDirs: [URL]? - do { - legacySubDirs = try fm.contentsOfDirectory(at: legacySavesDirectory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) - } catch { - ELOG("\(error.localizedDescription)") - legacySubDirs = nil + try realm.write { + realm.delete(save) } + } catch { + errorHandler.handleError(error, file: file) + ELOG("error delating \(file) from database: \(error)") + } + } + + func getSaveFrom(_ json: URL) throws -> SaveState? { + guard fileManager.fileExists(atPath: json.pathDecoded) + else { + return nil + } + let secureDoc = json.startAccessingSecurityScopedResource() - await legacySubDirs?.asyncForEach { - do { - let destinationURL = Paths.saveSavesPath.appendingPathComponent($0.lastPathComponent, isDirectory: true) - if !fm.isUbiquitousItem(at: destinationURL) { - try fm.setUbiquitous(true, - itemAt: $0, - destinationURL: destinationURL) - } else { - // var resultURL: NSURL? - // try fm.replaceItem(at: destinationURL, withItemAt: $0, backupItemName: nil, resultingItemURL: &resultURL) - // try fm.evictUbiquitousItem(at: destinationURL) - try fm.startDownloadingUbiquitousItem(at: destinationURL) - } - } catch { - ELOG("Error: \(error)") - } - } - // let saves = realm.objects(PVSaveState.self) - // saves.forEach { - // fm.setUbiquitous(true, itemAt: $0.file.url, destinationURL: Paths.saveSavesPath.appendingPathComponent($0.game.file.fileNameWithoutExtension, isDirectory: true).app) - // } - Task.detached { - jsonFiles.forEach { json in - do { - try FileManager.default.startDownloadingUbiquitousItem(at: json) - } catch { - ELOG("Download error: " + error.localizedDescription) - } - } + defer { + if secureDoc { + json.stopAccessingSecurityScopedResource() } + } + + var dataMaybe = fileManager.contents(atPath: json.pathDecoded) + if dataMaybe == nil { + dataMaybe = try Data(contentsOf: json, options: [.uncached]) + } + guard let data = dataMaybe else { + throw iCloudError.dataReadFail + } + DLOG("Data read \(String(data: data, encoding: .utf8) ?? "Nil")") + let save = try jsonDecorder.decode(SaveState.self, from: data) + DLOG("Read JSON data at (\(json.absoluteString)") + return save + } + + func importNewSaves() { + guard RomDatabase.databaseInitialized + else { + return + } + guard !newFiles.isEmpty + else { + return + } + queue.async(flags: .barrier) { [weak self] in + guard let jsonFiles = self?.prepareNextBatchToProcess(), + !jsonFiles.isEmpty + else { + return + } + self?.processJsonFiles(jsonFiles) + } + } + + func processJsonFiles(_ jsonFiles: any Collection) { + //TODO: try to change this to a single task and can we do this on a background thread instead of the main? + Task { Task.detached { // @MainActor in - await jsonFiles.concurrentForEach { @MainActor json in - let realm = try! await Realm() + await jsonFiles.concurrentForEach { @MainActor [weak self] json in do { - - let secureDoc = json.startAccessingSecurityScopedResource() - - defer { - if secureDoc { - json.stopAccessingSecurityScopedResource() - } + let realm = try await Realm() + guard let save = try self?.getSaveFrom(json) + else { + return } - var dataMaybe = FileManager.default.contents(atPath: json.path) - if dataMaybe == nil { - dataMaybe = try Data(contentsOf: json, options: [.uncached]) - } - guard let data = dataMaybe else { - throw iCloudError.dataReadFail - } - - DLOG("Data read \(String(data: data, encoding: .utf8) ?? "Nil")") - let save = try jsonDecorder.decode(SaveState.self, from: data) - DLOG("Read JSON data at (\(json.absoluteString)") - let existing = realm.object(ofType: PVSaveState.self, forPrimaryKey: save.id) if let existing = existing { // Skip if Save already exists @@ -411,7 +654,8 @@ public enum iCloudSync { existing.game = game } } catch { - ELOG("Failed to update game: \(error.localizedDescription)") + self?.errorHandler.handleError(error, file: json) + ELOG("Failed to update game \(json): \(error)") } } // TODO: Maybe any other missing data updates or update values in general? @@ -425,14 +669,16 @@ public enum iCloudSync { realm.add(newSave, update: .all) } } catch { - ELOG(error.localizedDescription) + self?.errorHandler.handleError(error, file: json) + ELOG("error adding new save \(json): \(error)") } } else { realm.add(newSave, update: .all) } ILOG("Added new save \(newSave.debugDescription)") } catch { - ELOG("Decode error: " + error.localizedDescription) + self?.errorHandler.handleError(error, file: json) + ELOG("Decode error on \(json): \(error)") return } } @@ -441,9 +687,261 @@ public enum iCloudSync { } } -class SaveStateSyncer: iCloudTypeSyncer { - public var metadataQuery: NSMetadataQuery = .init() - public var metadataQueryPredicate: NSPredicate { - return NSPredicate(format: "%K CONTAINS[c] 'Save States'", NSMetadataItemPathKey) +class RomsSyncer: iCloudContainerSyncer { + let gameImporter = GameImporter.shared + var processingFiles = Set() + var purgeStatus: GamePurgeStatus = .incomplete + + convenience init(notificationCenter: NotificationCenter, errorHandler: ErrorHandler) { + self.init(directories: ["ROMs"], notificationCenter: notificationCenter, errorHandler: errorHandler) + notificationCenter.addObserver(forName: .RomDatabaseInitialized, object: nil, queue: nil) { [weak self] _ in + Task { + self?.removeGamesDeletedWhileApplicationClosed() + self?.handleImportNewRomFiles() + } + } + notificationCenter.addObserver(forName: .RomsFinishedImporting, object: nil, queue: nil) { [weak self] _ in + Task { + self?.handleImportNewRomFiles() + } + } + } + + deinit { + notificationCenter.removeObserver(self) + } + + func removeGamesDeletedWhileApplicationClosed() { + guard purgeStatus == .incomplete, + initialSyncResult == .success, + errorHandler.numberOfErrors == 0 + else { + return + } + + defer { + purgeStatus = .complete + } + guard let actualContainrUrl = containerURL, + let romsDirectoryName = directories.first + else { + return + } + printCurrentThread() + let romsPath = actualContainrUrl.appendDocumentsDirectory.appendingPathComponent(romsDirectoryName) + DLOG("romsPath: \(romsPath)") + let realm: Realm + do { + realm = try Realm() + } catch { + ELOG("error removing game entries that do NOT exist in the cloud container \(romsPath)") + return + } + var games = realm.objects(PVGame.self) + games.forEach { game in + let gameUrl = romsPath.appendingPathComponent(game.romPath) + if !fileManager.fileExists(atPath: gameUrl.pathDecoded) { + do { + try realm.deleteGame(game) + } catch { + ELOG("error deleting \(gameUrl), \(error)") + } + } + } + } + + override func insertDownloadedFile(_ file: URL) { + guard let _ = pendingFilesToDownload.remove(file.absoluteString) + else { + return + } + + let parentDirectory = file.deletingLastPathComponent().lastPathComponent + DLOG("attempting to add file to game import queue: \(file), parent directory: \(parentDirectory)") + //we should only add to the import queue files that are actual ROMs, anything else can be ignored. + guard parentDirectory.range(of: "com.provenance.", + options: [.caseInsensitive, .anchored]) != nil, + let fileName = file.lastPathComponent.removingPercentEncoding + else { + return + } + + do { + printCurrentThread() + let realm = try Realm() + let romPath = "\(parentDirectory)/\(fileName)" + DLOG("attempting to query PVGame by romPath: \(romPath)") + let results = realm.objects(PVGame.self).filter(NSPredicate(format: "\(NSExpression(forKeyPath: \PVGame.romPath.self).keyPath) == %@", romPath)) + guard results.first == nil + else { + DLOG("\(file) already exists in database") + return + } + } catch { + errorHandler.handleError(error, file: file) + ELOG("error searching existing ROM \(file): \(error)") + } + DLOG("\(file) does NOT exist in database, adding to import set") + let newFilesCount: Int = queue.sync(flags: .barrier) { [weak self] in + self?.newFiles.insert(file) + return self?.newFiles.count ?? 0 + } + if newFilesCount >= fileImportQueueMaxCount { + handleImportNewRomFiles() + } + } + + override func deleteFromDatastore(_ file: URL) { + //TODO: remove cloud download, but keep in iCloud. this way the ROM isn't deleted from all devices + guard let fileName = file.lastPathComponent.removingPercentEncoding, + let parentDirectory = file.deletingLastPathComponent().lastPathComponent.removingPercentEncoding + else { + return + } + do { + let realm = try Realm() + let romPath = "\(parentDirectory)/\(fileName)" + DLOG("attempting to query PVGame by romPath: \(romPath)") + let results = realm.objects(PVGame.self).filter(NSPredicate(format: "\(NSExpression(forKeyPath: \PVGame.romPath.self).keyPath) == %@", romPath)) + guard let game: PVGame = results.first + else { + return + } + + try realm.deleteGame(game) + } catch { + errorHandler.handleError(error, file: file) + ELOG("error deleting ROM \(file) from database: \(error)") + } + } + + func handleImportNewRomFiles() { + guard RomDatabase.databaseInitialized + else { + return + } + clearProcessedFiles() + guard !newFiles.isEmpty + else { + return + } + tryToImportNewRomFiles() + } + + func tryToImportNewRomFiles() { + //if the importer is currently importing files, we have to wait + guard gameImporter.processingState == .idle + else { + return + } + printCurrentThread() + queue.async(flags: .barrier) { [weak self] in + self?.importNewRomFiles() + } + + } + + func clearProcessedFiles() { + gameImporter.removeSuccessfulImports(from: &processingFiles) + } + + func importNewRomFiles() { + printCurrentThread() + let nextFilesToProcess = prepareNextBatchToProcess() + DLOG("\(directories): processingFiles: (\(processingFiles.count)):") + DLOG("\(processingFiles)") + processingFiles.formUnion(nextFilesToProcess) + DLOG("\(directories): processingFiles plus new files: (\(processingFiles.count)):") + DLOG("\(directories): \(processingFiles)") + let importPaths = [URL](nextFilesToProcess) + if newFiles.isEmpty { + uploadedFiles.removeAll() + } + gameImporter.addImports(forPaths: importPaths) + gameImporter.startProcessing() + } +} + +struct iCloudSyncError { + let file: String? + var summary: String { + error.localizedDescription + } + let error: Error +} + +protocol Queue { + associatedtype Entry + var count: Int { get } + mutating func enqueue(entry: Entry) + mutating func dequeue() -> Entry? + func peek() -> Entry? + mutating func clear() +} + +struct iCloudErrorsQueue: Queue { + var errors = [iCloudSyncError]() + + var count: Int { + errors.count + } + + mutating func enqueue(entry: iCloudSyncError) { + errors.insert(entry, at: 0) + } + + mutating func dequeue() -> iCloudSyncError? { + guard !errors.isEmpty + else { + return nil + } + return errors.removeFirst() + } + + func peek() -> iCloudSyncError? { + errors.first + } + + mutating func clear() { + errors.removeAll() + } +} + +protocol ErrorHandler { + var allErrorSummaries: [String] { get } + var allFullErrors: [String] { get } + var allErrors: [iCloudSyncError] { get } + var numberOfErrors: Int { get } + func handleError(_ error: Error, file: URL?) + func clear() +} + +class iCloudErrorHandler: ErrorHandler { + static let shared = iCloudErrorHandler() + var queue = iCloudErrorsQueue() + + var allErrorSummaries: [String] { + queue.errors.map { $0.summary } + } + + var allFullErrors: [String] { + queue.errors.map { "\($0.error)" } + } + + var allErrors: [iCloudSyncError] { + queue.errors + } + + var numberOfErrors: Int { + queue.count + } + + func handleError(_ error: any Error, file: URL?) { + let syncError = iCloudSyncError(file: file?.path(percentEncoded: false), error: error) + queue.enqueue(entry: syncError) + } + + func clear() { + queue.clear() } } diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Conversion/Extensions/SaveState+PVSaveState.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Conversion/Extensions/SaveState+PVSaveState.swift index 4a7ef3fbe8..948bd791c9 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Conversion/Extensions/SaveState+PVSaveState.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Conversion/Extensions/SaveState+PVSaveState.swift @@ -67,22 +67,20 @@ extension SaveState: RealmRepresentable { } else { object.core = core.asRealm() } - - Task { - let path = game.file.fileName.saveStatePath.appendingPathComponent(file.fileName) - object.file = PVFile(withURL: path) - DLOG("file path: \(path)") - - object.date = date - object.lastOpened = lastOpened - if let image = image { - let dir = path.deletingLastPathComponent() - let imagePath = dir.appendingPathComponent(image.fileName) - DLOG("path: \(imagePath)") - object.image = PVImageFile(withURL: imagePath, relativeRoot: .iCloud) - } - object.isAutosave = isAutosave + //we remove the extension in order to get the correct path + let path = game.file.fileName.saveStatePath.deletingPathExtension().appendingPathComponent(file.fileName) + object.file = PVFile(withURL: path) + DLOG("file path: \(path)") + + object.date = date + object.lastOpened = lastOpened + if let image = image { + let dir = path.deletingLastPathComponent() + let imagePath = dir.appendingPathComponent(image.fileName) + DLOG("path: \(imagePath)") + object.image = PVImageFile(withURL: imagePath, relativeRoot: .iCloud) } + object.isAutosave = isAutosave } } } diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVFile.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVFile.swift index 253527a254..71f9fbed28 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVFile.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVFile.swift @@ -52,7 +52,12 @@ public class PVFile: Object, LocalFileProvider, Codable, DomainConvertibleType { public convenience init(withURL url: URL, relativeRoot: RelativeRoot = RelativeRoot.platformDefault, size: Int = 0, md5: String? = nil) { self.init() self.relativeRoot = relativeRoot + //TODO: this isn't working to get the partial path in all cases partialPath = relativeRoot.createRelativePath(fromURL: url) + //TODO: remove + if doesPathContainParent(partialPath) { + DLOG("partialPath: \(partialPath)") + } self.md5Cache = md5 if size > 0 { self.sizeCache = size @@ -78,29 +83,174 @@ public extension PVFile { var url: URL? { get { + //TODO: if partialPath is NOT a partial path, ie it has the prefix OR contains the app sandbox container OR contains or has the prefix (cloudContainer/Documents), then remove either of those, this will be older db entries from older versions that erronously don't remove the prefix + let url2 = urlUpdate + DLOG("url2=\(url2)\tpartialPath=\(partialPath)") if partialPath.contains("iCloud") || partialPath.contains("private") { var pathComponents = (partialPath as NSString).pathComponents pathComponents.removeFirst() let path = pathComponents.joined(separator: "/") let isDocumentsDir = path.contains("Documents") - + if isDocumentsDir { let iCloudBase = URL.iCloudContainerDirectory let url = (iCloudBase ?? RelativeRoot.documentsDirectory).appendingPathComponent(path) - return url + DLOG("url:\(url)") + if doesPathContainParent(url.path) { + DLOG("invalid url:\(url)") + } +// return url + return url2 } else { if let iCloudBase = URL.iCloudDocumentsDirectory { - return iCloudBase.appendingPathComponent(path) + let appendedICloudBase = iCloudBase.appendingPathComponent(path) + DLOG("appendedICloudBase:\(appendedICloudBase))") + //TODO: new return url2 + if doesPathContainParent(appendedICloudBase.path) { + DLOG("invalid url:\(appendedICloudBase)") + } + return appendedICloudBase } else { - return RelativeRoot.documentsDirectory.appendingPathComponent(path) + let appendedRelativeRoot = RelativeRoot.documentsDirectory.appendingPathComponent(path) + DLOG("appendedRelativeRoot:\(appendedRelativeRoot)") + if doesPathContainParent(appendedRelativeRoot.path) { + DLOG("invalid url:\(appendedRelativeRoot)") + } +// return appendedRelativeRoot + return url2 } } } let root = relativeRoot let resolvedURL = root.appendingPath(partialPath) + DLOG("resolvedURL:\(resolvedURL)") +// return resolvedURL + return url2 + } + } + var urlUpdate:URL { + get { + DLOG("relativeRoot=\(relativeRoot)\tpartialPath=\(partialPath)") + //TODO: lazy load this so it's only done once + let pathSuffix: String + if let bundleIdentifier = Bundle.main.bundleIdentifier { + DLOG("Bundle Identifier: \(bundleIdentifier)") + let bundleComponents = bundleIdentifier.split(separator: ".") + DLOG("bundleComponents=\(bundleComponents)") + let joined = bundleComponents.joined(separator: "~") + DLOG("joined=\(joined)") + pathSuffix = joined + } else { + pathSuffix = "org~provenance-emu~provenance" + } + let privateDirectory = "private" + if partialPath.contains("/iCloud~\(pathSuffix)/") {//&& partialPath.hasPrefix("\(privateDirectory)/") { + let completePath: String + let filePrefix = "file:///" + if !partialPath.hasPrefix(filePrefix) { + completePath = "\(filePrefix)\(partialPath)" + } else { + completePath = partialPath + } + if let urlPath = URL(string: completePath) { + DLOG("urlPath=\(urlPath)") + return urlPath + } + + var pathComponents = (partialPath as NSString).pathComponents + DLOG("pathComponents=\(pathComponents)") + //["private", "var", "mobile", "Library", "Mobile Documents", "iCloud~\(pathSuffix)", "Documents"] + let mobileDocumentsEncoded = "Mobile%20Documents" + let mobileDocumentsDecoded = "Mobile Documents" + let directoryPath: String + + if let iCloudDocumentsDirectoryContainer = URL.iCloudDocumentsDirectory { + var tmp = "\(iCloudDocumentsDirectoryContainer)" + let filePrefix = "file://" + if tmp.hasPrefix(filePrefix) { + tmp = tmp.replacingOccurrences(of: filePrefix, with: "") + } + directoryPath = tmp + } else { + directoryPath = "\(privateDirectory)/var/mobile/Library/\(mobileDocumentsDecoded)/\(mobileDocumentsDecoded)/iCloud~\(pathSuffix)" + } + DLOG("directoryPath=\(directoryPath)") + var prefixes = directoryPath.split(separator: "/") + let mobileDocumentsEncodedSub = mobileDocumentsEncoded.prefix(mobileDocumentsEncoded.count) + //we also add an encoded one. + if !prefixes.contains(mobileDocumentsEncodedSub) { + prefixes.append(mobileDocumentsEncodedSub) + } + let mobileDocumentsDecodedSub = mobileDocumentsDecoded.prefix(mobileDocumentsDecoded.count) + //we also add a decoded one. + if !prefixes.contains(mobileDocumentsDecodedSub) { + prefixes.append(mobileDocumentsDecodedSub) + } + DLOG("prefixes=\(prefixes)") + while prefixes.contains(where: {String($0) == pathComponents.first}) { + /* + Action Button Pressed 1706495469592 Optional(1706495461875) + relativeRoot=documents partialPath=private/var/mobile/Library/Mobile Documents/iCloud~com~pqskapps~provenance/Documents/Save States/Gremlins (USA).a52/DC271E475B4766E80151F1DA5B764E52.728185244.058265.svs + pathComponents=["private", "var", "mobile", "Library", "Mobile Documents", "iCloud~com~pqskapps~provenance", "Documents", "Save States", "Gremlins (USA).a52", "DC271E475B4766E80151F1DA5B764E52.728185244.058265.svs"] + pathComponentslremoveFirst()=["var", "mobile", "Library", "Mobile Documents", "iCloud~com~pqskapps~provenance", "Documents", "Save States", "Gremlins (USA).a52", "DC271E475B4766E80151F1DA5B764E52.728185244.058265.svs"] + path=var/mobile/Library/Mobile Documents/iCloud~com~pqskapps~provenance/Documents/Save States/Gremlins (USA).a52/DC271E475B4766E80151F1DA5B764E52.728185244.058265.svs + PVEmulatorConfiguration.iCloudContainerDirectory=Optional(file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~pqskapps~provenance/) + PVEmulatorConfiguration.iCloudDocumentsDirectory=Optional(file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~pqskapps~provenance/Documents/) + iCloudBase=Optional(file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~pqskapps~provenance/) + url=file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~pqskapps~provenance/var/mobile/Library/Mobile%20Documents/iCloud~com~pqskapps~provenance/Documents/Save%20States/Gremlins%20(USA).a52/DC271E475B4766E80151F1DA5B764E52.728185244.058265.svs + */ + pathComponents.removeFirst() + DLOG("pathComponentslremoveFirst()=\(pathComponents)") + } + let path = pathComponents.joined(separator: "/") + DLOG("path=\(path)") + DLOG("PVEmulatorConfiguration.iCloudContainerDirectory=\(String(describing: URL.iCloudContainerDirectory))") + DLOG("PVEmulatorConfiguration.iCloudDocumentsDirectory=\(String(describing: URL.iCloudDocumentsDirectory))") + let iCloudBase = path.contains("Documents") ? URL.iCloudContainerDirectory : URL.iCloudDocumentsDirectory + DLOG("iCloudBase=\(String(describing: iCloudBase))") + let url = (iCloudBase ?? RelativeRoot.documentsDirectory).appendingPathComponent(path) + DLOG("url=\(url)") + return url + } + let root = relativeRoot + DLOG("root=\(root)") + if doesPathContainParent(partialPath) { + DLOG("invalid path: \(partialPath)") + } + var actualPartialPath = partialPath + if root == .iCloud && partialPath.starts(with: "var/mobile/Containers/Data/Application/") { + DLOG("iCloud path, but partialPath does NOT contain iCloud path, but instead local path") + var partialPathComponents = partialPath.components(separatedBy: "/") + let directoriesToRemove = partialPathComponents.count >= 7 ? 7 : partialPathComponents.count + partialPathComponents.removeFirst(directoriesToRemove) + actualPartialPath = partialPathComponents.joined(separator: "/") + } + DLOG("actualPartialPath=\(actualPartialPath)") + if partialPath.hasPrefix(privateDirectory) { + var tmp = partialPath.split(separator: "/") + tmp.removeFirst() + actualPartialPath = tmp.joined(separator: "/") + } + DLOG("actualPartialPath=\(actualPartialPath)") + let resolvedURL = root.appendingPath(actualPartialPath) + DLOG("resolvedURL=\(resolvedURL)") return resolvedURL + /* + relativeRoot=iCloud partialPath=var/mobile/Containers/Data/Application/B8153B85-9BB5-44B6-A189-FDE9D8ABC29C/Documents/PVCache/F62D5AA941BB70E1913B787A65CD7EFC + Bundle Identifier: com.pqskapps.provenance + bundleComponents=["com", "pqskapps", "provenance"] + joined=com~pqskapps~provenance + resolvedURL=file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~pqskapps~provenance/Documents/var/mobile/Containers/Data/Application/B8153B85-9BB5-44B6-A189-FDE9D8ABC29C/Documents/PVCache/F62D5AA941BB70E1913B787A65CD7EFC + */ } } + + func doesPathContainParent(_ path: String) -> Bool { + return path.starts(with: "private/var/mobile/Library/Mobile") + || path.starts(with: "var/mobile/Containers/Data/Application") + || path.starts(with: "var/mobile/Containers/") + || path.starts(with: "private/var/mobile") + } private func setURL(_ url: URL) { do { diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVImageFile.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVImageFile.swift index 54b5412f32..e7da7412b6 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVImageFile.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/Files/PVImageFile.swift @@ -41,6 +41,10 @@ public final class PVImageFile: PVFile { self.init() self.relativeRoot = relativeRoot let partialPath = relativeRoot.createRelativePath(fromURL: url) + //TODO: remove + if doesPathContainParent(partialPath) { + DLOG("invalid path: \(partialPath)") + } self.partialPath = partialPath calculateSizeData() @@ -54,6 +58,10 @@ public final class PVImageFile: PVFile { cgsize = .zero return } + //TODO: path is wrong when switching to iCloud + if doesPathContainParent(path) { + ELOG("invalid path: \(path)") + } // let size = await Task { () -> CGSize in #if canImport(UIKit) diff --git a/PVUI/Sources/PVSwiftUI/Resources/en.lproj/Localizable.strings b/PVUI/Sources/PVSwiftUI/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000000..7bb679d943 --- /dev/null +++ b/PVUI/Sources/PVSwiftUI/Resources/en.lproj/Localizable.strings @@ -0,0 +1,4 @@ +"DeleteGameTitle" = "Delete Game?"; +"DeleteGameBody" = "Are you sure you want to delete game \"%@\"?"; +"OK" = "OK"; +"Cancel" = "Cancel"; diff --git a/PVUI/Sources/PVSwiftUI/RootView/PVRootViewController+DelegateMethods.swift b/PVUI/Sources/PVSwiftUI/RootView/PVRootViewController+DelegateMethods.swift index 3687a37cc8..f5bab90f16 100644 --- a/PVUI/Sources/PVSwiftUI/RootView/PVRootViewController+DelegateMethods.swift +++ b/PVUI/Sources/PVSwiftUI/RootView/PVRootViewController+DelegateMethods.swift @@ -73,10 +73,14 @@ extension PVRootViewController: PVRootDelegate { } public func attemptToDelete(game: PVGame, deleteSaves: Bool) { - do { - try self.delete(game: game, deleteSaves: deleteSaves) - } catch { - self.presentError(error.localizedDescription, source: self.view) + //String(format: NSLocalizedString("DeleteGameBody", bundle: Bundle.module, comment: ""), game.title) + //NSLocalizedString("DeleteGameTitle", comment: "") + presentCancellableMessage("Are you sure you want to delete game \"\(game.title)\"?", title: "Delete Game?", source: view) { + do { + try self.delete(game: game, deleteSaves: deleteSaves) + } catch { + self.presentError(error.localizedDescription, source: self.view) + } } } diff --git a/PVUI/Sources/PVUIBase/UIViewController+Alerts.swift b/PVUI/Sources/PVUIBase/UIViewController+Alerts.swift index 1ea00015c6..f5829331db 100644 --- a/PVUI/Sources/PVUIBase/UIViewController+Alerts.swift +++ b/PVUI/Sources/PVUIBase/UIViewController+Alerts.swift @@ -13,7 +13,33 @@ import PVLogging public extension UIViewController { - func presentMessage(_ message: String, title: String, source: UIView, completion _: (() -> Swift.Void)? = nil) { + func presentMessage(_ message: String, title: String, source: UIView, completion: (() -> Swift.Void)? = nil) { + presentMessage(message, + title: title, + source: source, + completion: completion) + } + + func presentCancellableMessage(_ message: String, title: String, source: UIView, completion: (() -> Swift.Void)? = nil) { + presentMessage(message, title: title, + source: source, + secondaryActionTitle: NSLocalizedString("Cancel", comment: ""), + secondaryActionStyle: .cancel, + secondaryCompletion: nil, + defaultActionTitle: NSLocalizedString("Delete", comment: ""), + defaultActionStyle: .destructive, + completion: completion) + } + + func presentMessage(_ message: String, + title: String, + source: UIView, + secondaryActionTitle: String? = nil, + secondaryActionStyle: UIAlertAction.Style = .cancel, + secondaryCompletion: ((UIAlertAction) -> Void)? = nil, + defaultActionTitle: String = NSLocalizedString("OK", comment: ""), + defaultActionStyle: UIAlertAction.Style = .default, + completion: (() -> Swift.Void)? = nil) { NSLog("Title: %@ Message: %@", title, message); let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.preferredContentSize = CGSize(width: 300, height: 300) @@ -22,7 +48,12 @@ extension UIViewController { alert.popoverPresentationController?.sourceView = source alert.popoverPresentationController?.sourceRect = UIScreen.main.bounds - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: defaultActionTitle, style: defaultActionStyle) { _ in + completion?() + }) + if let actualSecondaryActionTitle = secondaryActionTitle { + alert.addAction(UIAlertAction(title: actualSecondaryActionTitle, style: secondaryActionStyle, handler: secondaryCompletion)) + } let presentingVC = presentedViewController ?? self diff --git a/Provenance.xcodeproj/project.pbxproj b/Provenance.xcodeproj/project.pbxproj index 732a3985dc..804b710a92 100644 --- a/Provenance.xcodeproj/project.pbxproj +++ b/Provenance.xcodeproj/project.pbxproj @@ -8847,7 +8847,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_ASSET_PATHS = "\"Provenance Mini Watch App/Preview Content\""; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -8864,7 +8864,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini.watchkitapp"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini.watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SKIP_INSTALL = YES; @@ -8899,7 +8899,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"Provenance Mini Watch App/Preview Content\""; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -8917,7 +8917,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini.watchkitapp"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini.watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SKIP_INSTALL = YES; @@ -8950,7 +8950,7 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"Provenance Mini Watch App/Preview Content\""; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -8968,7 +8968,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini.watchkitapp"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini.watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SKIP_INSTALL = YES; @@ -8996,7 +8996,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -9006,7 +9006,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; @@ -9033,7 +9033,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -9044,7 +9044,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; @@ -9069,7 +9069,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -9080,7 +9080,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; @@ -9105,7 +9105,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -9114,7 +9114,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini-Watch-AppTests"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini-Watch-AppTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; @@ -9146,7 +9146,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -9156,7 +9156,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini-Watch-AppTests"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini-Watch-AppTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SWIFT_EMIT_LOC_STRINGS = NO; @@ -9186,7 +9186,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -9196,7 +9196,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini-Watch-AppTests"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini-Watch-AppTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SWIFT_EMIT_LOC_STRINGS = NO; @@ -9224,7 +9224,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -9233,7 +9233,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini-Watch-AppUITests"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini-Watch-AppUITests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; @@ -9264,7 +9264,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -9274,7 +9274,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini-Watch-AppUITests"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini-Watch-AppUITests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SWIFT_EMIT_LOC_STRINGS = NO; @@ -9303,7 +9303,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -9313,7 +9313,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.Provenance-Mini-Watch-AppUITests"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_PREFIX).Provenance-Mini-Watch-AppUITests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SWIFT_EMIT_LOC_STRINGS = NO; @@ -9379,7 +9379,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -9454,7 +9454,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -9528,7 +9528,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -9627,7 +9627,7 @@ ); MTL_ENABLE_DEBUG_INFO = YES; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.lite"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE))"; PRODUCT_NAME = Provenance; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; @@ -9683,7 +9683,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.lite"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; PRODUCT_NAME = Provenance; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; @@ -9739,7 +9739,7 @@ ); MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.lite"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; PRODUCT_NAME = Provenance; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; @@ -9785,7 +9785,7 @@ ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.ThumbnailExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ThumbnailExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -9835,7 +9835,7 @@ ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.ThumbnailExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ThumbnailExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -9884,7 +9884,7 @@ ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.ThumbnailExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ThumbnailExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -9915,7 +9915,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -9933,7 +9933,7 @@ MACOSX_DEPLOYMENT_TARGET = 15.4; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.SpotlightImportExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).SpotlightImportExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -9967,7 +9967,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9986,7 +9986,7 @@ MACOSX_DEPLOYMENT_TARGET = 15.4; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.SpotlightImportExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).SpotlightImportExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -10019,7 +10019,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -10038,7 +10038,7 @@ MACOSX_DEPLOYMENT_TARGET = 15.4; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.SpotlightImportExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).SpotlightImportExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -10088,7 +10088,7 @@ ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.WidgetExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).WidgetExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -10140,7 +10140,7 @@ ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.WidgetExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).WidgetExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -10191,7 +10191,7 @@ ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.WidgetExtension"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).WidgetExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -10903,7 +10903,7 @@ CODE_SIGN_STYLE = Automatic; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -11003,7 +11003,7 @@ CODE_SIGN_STYLE = Automatic; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -11103,7 +11103,7 @@ CODE_SIGN_STYLE = Automatic; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -11226,7 +11226,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -11244,7 +11244,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.ROM-File-Provider"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ROM-File-Provider"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -11276,7 +11276,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -11295,7 +11295,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.ROM-File-Provider"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ROM-File-Provider"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -11325,7 +11325,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -11344,7 +11344,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.ROM-File-Provider"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ROM-File-Provider"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -11372,7 +11372,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -11390,7 +11390,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.ROM-File-ProviderUI"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ROM-File-ProviderUI"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -11421,7 +11421,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -11440,7 +11440,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.ROM-File-ProviderUI"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ROM-File-ProviderUI"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -11469,7 +11469,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = S32Z3HMYVQ; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -11488,7 +11488,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.ROM-File-ProviderUI"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ROM-File-ProviderUI"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -11529,7 +11529,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.stickers"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).stickers"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER_MAC = ""; SDKROOT = iphoneos; @@ -11572,7 +11572,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.stickers"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).stickers"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER_MAC = ""; SDKROOT = iphoneos; @@ -11615,7 +11615,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.provenance.stickers"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).stickers"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER_MAC = ""; SDKROOT = iphoneos; @@ -11761,7 +11761,7 @@ MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.ThumbnailExtensionMacOS"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_IDENTIFIER).ThumbnailExtensionMacOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; @@ -11808,7 +11808,7 @@ MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.ThumbnailExtensionMacOS"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_IDENTIFIER).ThumbnailExtensionMacOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; @@ -11854,7 +11854,7 @@ MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.ThumbnailExtensionMacOS"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ORG_IDENTIFIER).ThumbnailExtensionMacOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; @@ -11914,9 +11914,9 @@ MTL_FAST_MATH = YES; OTHER_CFLAGS = "-Wno-deprecated-declarations"; OTHER_LDFLAGS = "$(inherited)"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=appletvos*]" = "org.provenance-emu.provenance.lite"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "org.provenance-emu.provenance.lite"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.provenance-emu.provenance.lite"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=appletvos*]" = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; PRODUCT_MODULE_NAME = Provenance; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -11992,9 +11992,9 @@ "-Wno-deprecated-declarations", ); OTHER_LDFLAGS = "$(inherited)"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=appletvos*]" = "org.provenance-emu.provenance.lite"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "org.provenance-emu.provenance.lite"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.provenance-emu.provenance.lite"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=appletvos*]" = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; PRODUCT_MODULE_NAME = Provenance; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -12068,9 +12068,9 @@ "-Wno-deprecated-declarations", ); OTHER_LDFLAGS = "$(inherited)"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=appletvos*]" = "org.provenance-emu.provenance.lite"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "org.provenance-emu.provenance.lite"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.provenance-emu.provenance.lite"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=appletvos*]" = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "$(PRODUCT_BUNDLE_IDENTIFIER_LITE)"; PRODUCT_MODULE_NAME = Provenance; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Provenance/Main UI/PVAppDelegate.swift b/Provenance/Main UI/PVAppDelegate.swift index 8fb80c09ec..5010a64bab 100644 --- a/Provenance/Main UI/PVAppDelegate.swift +++ b/Provenance/Main UI/PVAppDelegate.swift @@ -354,15 +354,7 @@ final class PVAppDelegate: UIResponder, GameLaunchingAppDelegate, UIApplicationD func _initICloud() { PVEmulatorConfiguration.initICloud() - DispatchQueue.global(qos: .background).async { - let useiCloud = Defaults[.iCloudSync] && URL.supportsICloud - if useiCloud { - DispatchQueue.main.async { - iCloudSync.initICloudDocuments() - iCloudSync.importNewSaves() - } - } - } + iCloudSync.initICloudDocuments() } var currentThemeObservation: Any? // AnyCancellable? diff --git a/ProvenanceTV/ProvenanceTV-AppStore.entitlements b/ProvenanceTV/ProvenanceTV-AppStore.entitlements index 8fcc57498a..5b27db4542 100644 --- a/ProvenanceTV/ProvenanceTV-AppStore.entitlements +++ b/ProvenanceTV/ProvenanceTV-AppStore.entitlements @@ -26,7 +26,6 @@ com.apple.security.application-groups $(APP_GROUP_IDENTIFIER) - group.org.provenance-emu keychain-access-groups