Adds support for creating and updating Auto Save States

This commit is contained in:
Riley Testut 2016-08-10 23:31:12 -05:00
parent 856c412a78
commit e2f229fc36
4 changed files with 160 additions and 24 deletions

View File

@ -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

View File

@ -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()
}
}

View File

@ -43,5 +43,6 @@ class LaunchViewController: UIViewController
guard segue.identifier == "embedGameViewController" else { return }
self.gameViewController = segue.destination as! GameViewController
self.gameViewController.updatesAutoSaveState = true
}
}

View File

@ -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