diff --git a/Common/Database/Model/SaveState.swift b/Common/Database/Model/SaveState.swift index f7eaab8..f388794 100644 --- a/Common/Database/Model/SaveState.swift +++ b/Common/Database/Model/SaveState.swift @@ -26,27 +26,25 @@ extension SaveState case game case previewGame } - - @objc enum `Type`: Int16 - { - case auto - case general - case locked - } } - +@objc enum SaveStateType: Int16 +{ + case auto + case general + case locked +} @objc(SaveState) class SaveState: NSManagedObject, SaveStateProtocol { @NSManaged var name: String? + @NSManaged var creationDate: Date @NSManaged var modifiedDate: Date - @NSManaged var type: Type + @NSManaged var type: SaveStateType @NSManaged private(set) var filename: String @NSManaged private(set) var identifier: String - @NSManaged private(set) var creationDate: Date // Must be optional relationship to satisfy weird Core Data requirement // https://forums.developer.apple.com/thread/20535 diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index cf11b1a..891cec7 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -18,10 +18,18 @@ class GameViewController: DeltaCore.GameViewController { /// Assumed to be Delta.Game instance override var game: GameProtocol? { - willSet { + willSet + { self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext) } - didSet { + didSet + { + if self.game?.fileURL != oldValue?.fileURL + { + // Game changed, so we make sure auto save states are enabled again + self.ignoreAutoSaveStateUpdates = false + } + guard let emulatorCore = self.emulatorCore else { return } self.preferredContentSize = emulatorCore.preferredRenderingSize @@ -32,10 +40,16 @@ class GameViewController: DeltaCore.GameViewController // If non-nil, will override the default preview action items returned in previewActionItems() var overridePreviewActionItems: [UIPreviewActionItem]? + // Set to true to handle automatically updating auto save state + var updatesAutoSaveState = false + //MARK: - Private Properties - private var pauseViewController: PauseViewController? private var pausingGameController: GameController? + // Prevents the "same" save state from being saved multiple times + private var ignoreAutoSaveStateUpdates = false + private var context = CIContext(options: [kCIContextWorkingColorSpace: NSNull()]) // Sustain Buttons @@ -94,6 +108,7 @@ class GameViewController: DeltaCore.GameViewController NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalControllerDidConnect, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalControllerDidDisconnect, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didEnterBackground(with:)), name: .UIApplicationDidEnterBackground, object: UIApplication.shared) } deinit @@ -186,6 +201,8 @@ extension GameViewController let gamesViewController = (segue.destination as! UINavigationController).topViewController as! GamesViewController gamesViewController.theme = .dark + self.updateAutoSaveState() + case "pause": guard let gameController = sender as? GameController else { fatalError("sender for pauseSegue must be the game controller that pressed the Menu button") @@ -290,6 +307,11 @@ extension GameViewController { self.updateCheats() } + + if self.emulatorCore?.state == .running + { + self.ignoreAutoSaveStateUpdates = false + } } } @@ -334,25 +356,85 @@ private extension GameViewController /// Save States extension GameViewController: SaveStatesViewControllerDelegate { - func saveStatesViewController(_ saveStatesViewController: SaveStatesViewController, updateSaveState saveState: SaveState) + private func updateAutoSaveState() { - var updatingExistingSaveState = true + guard !self.ignoreAutoSaveStateUpdates else { return } + + // If not in view hierarchy, don't update auto save state + guard self.updatesAutoSaveState else { return } + + // Ignore future update auto save state requests until we resume emulation again + // This prevents us from filling our auto save state slots with the "same" save state + self.ignoreAutoSaveStateUpdates = true + + // Must be done synchronously + let backgroundContext = DatabaseManager.shared.newBackgroundContext() + backgroundContext.performAndWait { + + let game = backgroundContext.object(with: (self.game as! Game).objectID) as! Game + + let predicate = NSPredicate(format: "%K == %d AND %K == %@", #keyPath(SaveState.type), SaveStateType.auto.rawValue, #keyPath(SaveState.game), game) + + let fetchRequest = SaveState.fetchRequest() + fetchRequest.predicate = predicate + fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(SaveState.creationDate), ascending: true)] + + var saveStates: [SaveState]? = nil + + do + { + saveStates = try fetchRequest.execute() as? [SaveState] + } + catch + { + print(error) + } + + if let saveStates = saveStates, let saveState = saveStates.first, saveStates.count >= 2 + { + // If there are two or more auto save states, update the oldest one + self.update(saveState) + + // Tiny hack; SaveStatesViewController sorts save states by creation date, so we update the creation date too + // Simpler than deleting old save states ¯\_(ツ)_/¯ + saveState.creationDate = saveState.modifiedDate + } + else + { + // Otherwise, create a new one + let saveState = SaveState.insertIntoManagedObjectContext(backgroundContext) + saveState.type = .auto + saveState.game = game + + self.update(saveState) + } + + backgroundContext.saveWithErrorLogging() + } + } + + private func update(_ saveState: SaveState) + { + let isRunning = (self.emulatorCore?.state == .running) + + if isRunning + { + self.pauseEmulation() + } self.emulatorCore?.save { (temporarySaveState) in do { if FileManager.default.fileExists(atPath: saveState.fileURL.path) { - try FileManager.default.replaceItem(at: saveState.fileURL, withItemAt: temporarySaveState.fileURL, backupItemName: nil, options: [], resultingItemURL: nil) + try FileManager.default.replaceItem(at: saveState.fileURL, withItemAt: temporarySaveState.fileURL, backupItemName: nil, resultingItemURL: nil) } else { try FileManager.default.moveItem(at: temporarySaveState.fileURL, to: saveState.fileURL) - - updatingExistingSaveState = false } } - catch let error as NSError + catch { print(error) } @@ -367,7 +449,7 @@ extension GameViewController: SaveStatesViewControllerDelegate { try data.write(to: saveState.imageFileURL, options: [.atomicWrite]) } - catch let error as NSError + catch { print(error) } @@ -375,6 +457,20 @@ extension GameViewController: SaveStatesViewControllerDelegate saveState.modifiedDate = Date() + if isRunning + { + self.resumeEmulation() + } + } + + //MARK: - SaveStatesViewControllerDelegate + + func saveStatesViewController(_ saveStatesViewController: SaveStatesViewController, updateSaveState saveState: SaveState) + { + let updatingExistingSaveState = FileManager.default.fileExists(atPath: saveState.fileURL.path) + + self.update(saveState) + // Dismiss if updating an existing save state. // If creating a new one, don't dismiss. if updatingExistingSaveState @@ -385,9 +481,38 @@ extension GameViewController: SaveStatesViewControllerDelegate func saveStatesViewController(_ saveStatesViewController: SaveStatesViewController, loadSaveState saveState: SaveStateProtocol) { + // If we're loading the auto save state, we need to create a temporary copy of saveState. + // Then, we update the auto save state, but load our copy so everything works out. + var temporarySaveState: SaveStateProtocol? = nil + + if let autoSaveState = saveState as? SaveState, autoSaveState.type == .auto + { + let temporaryURL = FileManager.uniqueTemporaryURL() + + do + { + try FileManager.default.moveItem(at: saveState.fileURL, to: temporaryURL) + temporarySaveState = DeltaCore.SaveState(fileURL: temporaryURL, gameType: saveState.gameType) + } + catch + { + print(error) + } + } + + self.updateAutoSaveState() + do { - try self.emulatorCore?.load(saveState) + if let temporarySaveState = temporarySaveState + { + try self.emulatorCore?.load(temporarySaveState) + try FileManager.default.removeItem(at: temporarySaveState.fileURL) + } + else + { + try self.emulatorCore?.load(saveState) + } } catch EmulatorCore.SaveStateError.doesNotExist { @@ -614,7 +739,7 @@ extension GameViewController: GameViewControllerDelegate func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool { - return (self.presentedViewController == nil || self.presentedViewController?.isDisappearing == true) && !self.selectingSustainedButtons + return (self.presentedViewController == nil || self.presentedViewController?.isDisappearing == true) && !self.selectingSustainedButtons && self.view.window != nil } func gameViewControllerDidUpdate(_ gameViewController: DeltaCore.GameViewController) @@ -625,3 +750,12 @@ extension GameViewController: GameViewControllerDelegate } } } + +//MARK: - Notifications - +private extension GameViewController +{ + @objc func didEnterBackground(with notification: Notification) + { + self.updateAutoSaveState() + } +} diff --git a/Delta/Launch/LaunchViewController.swift b/Delta/Launch/LaunchViewController.swift index 1f88f54..7b89de7 100644 --- a/Delta/Launch/LaunchViewController.swift +++ b/Delta/Launch/LaunchViewController.swift @@ -43,5 +43,6 @@ class LaunchViewController: UIViewController guard segue.identifier == "embedGameViewController" else { return } self.gameViewController = segue.destination as! GameViewController + self.gameViewController.updatesAutoSaveState = true } } diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index aefbf23..2599974 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -258,9 +258,10 @@ private extension SaveStatesViewController let saveState = self.fetchedResultsController.object(at: indexPath) as! SaveState + guard let actions = self.actionsForSaveState(saveState)?.map({ $0.alertAction }) else { return } + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - let actions = self.actionsForSaveState(saveState).map { $0.alertAction } for action in actions { alertController.addAction(action) @@ -435,8 +436,10 @@ private extension SaveStatesViewController return section } - func actionsForSaveState(_ saveState: SaveState) -> [Action] + func actionsForSaveState(_ saveState: SaveState) -> [Action]? { + guard saveState.type != .auto else { return nil } + var actions = [Action]() if self.traitCollection.forceTouchCapability == .available @@ -606,7 +609,7 @@ extension SaveStatesViewController: UIViewControllerPreviewingDelegate, UIPrevie { try self.previewGameViewController.emulatorCore?.load(saveState) - let actions = self.actionsForSaveState(saveState).lazy.filter{ $0.style != .cancel }.map{ $0.previewAction } + let actions = self.actionsForSaveState(saveState)?.lazy.filter{ $0.style != .cancel }.map{ $0.previewAction } ?? [] self.previewGameViewController.overridePreviewActionItems = Array(actions) return self.previewGameViewController