Refactors previewing GameViewController logic into new PreviewGameViewController

This commit is contained in:
Riley Testut 2016-08-12 01:09:52 -05:00
parent b017be6368
commit 3d16fed35a
7 changed files with 309 additions and 181 deletions

@ -1 +1 @@
Subproject commit d9b61b60c23286a4758bab06ed4a89de641f2628
Subproject commit d4895367427ffed6ef1fd04e60868cf799fb3371

View File

@ -13,6 +13,8 @@
BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */ = {isa = PBXBuildFile; fileRef = BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */; };
BF0CDDAD1C8155D200640168 /* LoadImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */; };
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF107EC31BF413F000E0C32C /* GamesViewController.swift */; };
BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */; };
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */; };
BF172AEB1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */; };
BF1FB1861C5EE643007E2494 /* SaveState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1FB1831C5EE643007E2494 /* SaveState.swift */; };
BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; };
@ -99,6 +101,8 @@
BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIDevice+Vibration.m"; sourceTree = "<group>"; };
BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoadImageOperation.swift; path = Components/LoadImageOperation.swift; sourceTree = "<group>"; };
BF107EC31BF413F000E0C32C /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = "<group>"; };
BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewGameViewController.swift; sourceTree = "<group>"; };
BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EmulatorCore+Cheats.swift"; sourceTree = "<group>"; };
BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = "<group>"; };
BF1FB1831C5EE643007E2494 /* SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveState.swift; sourceTree = "<group>"; };
BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = "<group>"; };
@ -189,6 +193,7 @@
BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */,
BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */,
BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */,
BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -412,6 +417,7 @@
isa = PBXGroup;
children = (
BF63BDE91D389EEB00FCB040 /* GameViewController.swift */,
BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */,
);
path = Emulation;
sourceTree = "<group>";
@ -573,6 +579,7 @@
BF7AE8241C2E984300B1B5BC /* GridCollectionViewLayout.swift in Sources */,
BF1FB1861C5EE643007E2494 /* SaveState.swift in Sources */,
BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */,
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */,
BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */,
BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */,
BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */,
@ -605,6 +612,7 @@
BF0CDDAD1C8155D200640168 /* LoadImageOperation.swift in Sources */,
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */,
BF172AEB1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */,
BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */,
BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */,
BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */,
BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */,

View File

@ -39,19 +39,10 @@ class GameViewController: DeltaCore.GameViewController
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
}
didSet {
guard let emulatorCore = self.emulatorCore else { return }
self.preferredContentSize = emulatorCore.preferredRenderingSize
emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext)
self.emulatorCore?.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext)
}
}
// 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?
@ -86,26 +77,6 @@ class GameViewController: DeltaCore.GameViewController
private var sustainButtonsBlurView: UIVisualEffectView!
private var sustainButtonsBackgroundView: RSTBackgroundView!
override var previewActionItems: [UIPreviewActionItem]
{
if let previewActionItems = self.overridePreviewActionItems
{
return previewActionItems
}
guard let game = self.game as? Game else { return [] }
let presentingViewController = self.presentingViewController
let launchGameAction = UIPreviewAction(title: NSLocalizedString("Launch \(game.name)", comment: ""), style: .default) { (action, viewController) in
// Delaying until next run loop prevents self from being dismissed immediately
DispatchQueue.main.async {
presentingViewController?.present(viewController, animated: true, completion: nil)
}
}
return [launchGameAction]
}
required init()
{
self.reactivateSustainedInputsQueue = OperationQueue()
@ -349,7 +320,7 @@ extension GameViewController
if previousState == .stopped
{
self.updateCheats()
self.emulatorCore?.updateCheats()
}
}
}
@ -396,9 +367,7 @@ private extension GameViewController
extension GameViewController: SaveStatesViewControllerDelegate
{
private func updateAutoSaveState()
{
guard self.updatesAutoSaveState else { return }
{
// If pausedSaveState exists and has already been saved, don't update auto save state
// This prevents us from filling our auto save state slots with the same save state
let savedPausedSaveState = self.pausedSaveState?.isSaved ?? false
@ -566,7 +535,7 @@ extension GameViewController: SaveStatesViewControllerDelegate
print(error)
}
self.updateCheats()
self.emulatorCore?.updateCheats()
self.pauseViewController?.dismiss()
}
@ -578,67 +547,13 @@ extension GameViewController: CheatsViewControllerDelegate
{
func cheatsViewController(_ cheatsViewController: CheatsViewController, activateCheat cheat: Cheat)
{
self.activate(cheat)
self.emulatorCore?.activateCheatWithErrorLogging(cheat)
}
func cheatsViewController(_ cheatsViewController: CheatsViewController, deactivateCheat cheat: Cheat)
{
self.emulatorCore?.deactivate(cheat)
}
private func activate(_ cheat: Cheat)
{
do
{
try self.emulatorCore?.activate(cheat)
}
catch EmulatorCore.CheatError.invalid
{
print("Invalid cheat:", cheat.name, cheat.code)
}
catch let error as NSError
{
print("Unknown Cheat Error:", error, cheat.name, cheat.code)
}
}
private func updateCheats()
{
guard let game = self.game as? Game else { return }
let running = (self.emulatorCore?.state == .running)
if running
{
// Core MUST be paused when activating cheats, or else race conditions could crash the core
self.pauseEmulation()
}
let backgroundContext = DatabaseManager.shared.newBackgroundContext()
backgroundContext.performAndWait {
let predicate = NSPredicate(format: "%K == %@", Cheat.Attributes.game.rawValue, game)
let cheats = Cheat.instancesWithPredicate(predicate, inManagedObjectContext: backgroundContext, type: Cheat.self)
for cheat in cheats
{
if cheat.enabled
{
self.activate(cheat)
}
else
{
self.emulatorCore?.deactivate(cheat)
}
}
}
if running
{
self.resumeEmulation()
}
}
}
//MARK: - Sustain Buttons -

View File

@ -0,0 +1,176 @@
//
// PreviewGameViewController.swift
// Delta
//
// Created by Riley Testut on 8/11/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import UIKit
import DeltaCore
private var kvoContext = 0
class PreviewGameViewController: DeltaCore.GameViewController
{
// If non-nil, will override the default preview action items returned in previewActionItems()
var overridePreviewActionItems: [UIPreviewActionItem]?
// Save state to be loaded upon preview
var previewSaveState: SaveStateProtocol?
// Initial image to be shown while loading
var previewImage: UIImage? {
didSet {
self.updatePreviewImage()
}
}
private var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated)
override var game: GameProtocol? {
willSet {
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
}
didSet {
guard let emulatorCore = self.emulatorCore else {
self.preferredContentSize = CGSize.zero
return
}
emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext)
self.preferredContentSize = emulatorCore.preferredRenderingSize
}
}
override var previewActionItems: [UIPreviewActionItem] {
guard let previewActionItems = self.overridePreviewActionItems else { return [] }
return previewActionItems
}
deinit
{
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
}
}
//MARK: - UIViewController -
/// UIViewController
extension PreviewGameViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.controllerView.isHidden = true
// Temporarily prevent emulatorCore from updating gameView to prevent flicker of black, or other visual glitches
self.emulatorCore?.remove(self.gameView)
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
self.emulatorCoreQueue.async {
self.emulatorCore?.resume()
}
}
override func viewWillDisappear(_ animated: Bool)
{
super.viewWillDisappear(animated)
// Pause in viewWillDisappear and not viewDidDisappear like DeltaCore.GameViewController so the audio cuts off earlier if being dismissed
self.emulatorCore?.pause()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
// Need to update in viewDidLayoutSubviews() to ensure bounds of gameView are not CGRect.zero
self.updatePreviewImage()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func observeValue(forKeyPath keyPath: String?, of object: AnyObject?, change: [NSKeyValueChangeKey : AnyObject]?, context: UnsafeMutablePointer<Void>?)
{
guard context == &kvoContext else { return super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) }
guard
let rawValue = change?[.oldKey] as? Int,
let previousState = EmulatorCore.State(rawValue: rawValue),
let state = self.emulatorCore?.state
else { return }
if previousState == .stopped, state == .running
{
self.emulatorCoreQueue.sync {
if self.isAppearing
{
// Pause to prevent it from starting before visible (in case user peeked slowly)
self.emulatorCore?.pause()
}
self.preparePreview()
}
}
}
}
//MARK: - Private -
private extension PreviewGameViewController
{
func updatePreviewImage()
{
if let previewImage = self.previewImage
{
self.gameView?.inputImage = CIImage(image: previewImage)
}
else
{
self.gameView?.inputImage = nil
}
}
func preparePreview()
{
var previewSaveState = self.previewSaveState
if let saveState = self.previewSaveState as? SaveState
{
saveState.managedObjectContext?.performAndWait {
previewSaveState = DeltaCore.SaveState(fileURL: saveState.fileURL, gameType: saveState.gameType)
}
}
if let saveState = previewSaveState
{
do
{
try self.emulatorCore?.load(saveState)
}
catch EmulatorCore.SaveStateError.doesNotExist
{
print("Save State does not exist.")
}
catch
{
print(error)
}
}
self.emulatorCore?.updateCheats()
// Re-enable emulatorCore to update gameView again
self.emulatorCore?.add(self.gameView)
}
}

View File

@ -0,0 +1,66 @@
//
// EmulatorCore+Cheats.swift
// Delta
//
// Created by Riley Testut on 8/11/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import DeltaCore
extension EmulatorCore
{
func activateCheatWithErrorLogging(_ cheat: Cheat)
{
do
{
try self.activate(cheat)
}
catch EmulatorCore.CheatError.invalid
{
print("Invalid cheat:", cheat.name, cheat.code)
}
catch
{
print("Unknown Cheat Error:", error, cheat.name, cheat.code)
}
}
func updateCheats()
{
guard let game = self.game as? Game else { return }
let running = (self.state == .running)
if running
{
// Core MUST be paused when activating cheats, or else race conditions could crash the core
self.pause()
}
let backgroundContext = DatabaseManager.shared.newBackgroundContext()
backgroundContext.performAndWait {
let predicate = NSPredicate(format: "%K == %@", Cheat.Attributes.game.rawValue, game)
let cheats = Cheat.instancesWithPredicate(predicate, inManagedObjectContext: backgroundContext, type: Cheat.self)
for cheat in cheats
{
if cheat.enabled
{
self.activateCheatWithErrorLogging(cheat)
}
else
{
self.deactivate(cheat)
}
}
}
if running
{
self.resume()
}
}
}

View File

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

View File

@ -65,8 +65,6 @@ class SaveStatesViewController: UICollectionViewController
private let dateFormatter: DateFormatter
private let previewGameViewController = GameViewController()
required init?(coder aDecoder: NSCoder)
{
self.dateFormatter = DateFormatter()
@ -122,9 +120,8 @@ extension SaveStatesViewController
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(SaveStatesViewController.handleLongPressGesture(_:)))
self.collectionView?.addGestureRecognizer(longPressGestureRecognizer)
// Pre-initialize previewGameViewController with game and start/pause emulation to ensure previewingContext(_:viewControllerForLocation:) callback doesn't take too long + break 3D Touch animation
self.preparePreviewGameViewController()
self.prepareEmulatorCoreSaveState()
self.registerForPreviewing(with: self, sourceView: self.collectionView!)
@ -286,23 +283,13 @@ private extension SaveStatesViewController
func updateSaveState(_ saveState: SaveState)
{
// Stop previewGameViewController.emulatorCore, and switch to self.emulatorCore
// Switch back to self.emulatorCore
self.prepareEmulatorCore()
saveState.managedObjectContext?.performAndWait {
self.delegate?.saveStatesViewController(self, updateSaveState: saveState)
saveState.managedObjectContext?.saveWithErrorLogging()
}
DispatchQueue.main.async {
// Only restart self.previewGameViewController.emulatorCore if we're not being dismissed
if !self.isDisappearing
{
self.emulatorCore?.stop()
self.previewGameViewController.emulatorCore?.start()
self.previewGameViewController.emulatorCore?.pause()
}
}
}
func loadSaveState(_ saveState: SaveStateProtocol)
@ -492,9 +479,14 @@ private extension SaveStatesViewController
func resetEmulatorCoreIfNeeded()
{
guard let saveState = self.emulatorCoreSaveState else { return }
// Kinda hacky, but isMovingFromParentViewController only returns yes when popping off navigation controller, and not being dismissed modally
// Because of this, this is only run when the user returns to PauseMenuViewController, and not when they choose a save state to load
if self.isMovingFromParentViewController
{
self.prepareEmulatorCore()
}
defer
if let saveState = self.emulatorCoreSaveState
{
// Remove temporary save state file
do
@ -506,76 +498,57 @@ private extension SaveStatesViewController
print(error)
}
}
self.previewGameViewController.emulatorCore?.stop()
// Kinda hacky, but isMovingFromParentViewController only returns yes when popping off navigation controller, and not being dismissed modally
// Because of this, this is only run when the user returns to PauseMenuViewController, and not when they choose a save state to load
if self.isMovingFromParentViewController
{
self.prepareEmulatorCore()
}
}
func prepareEmulatorCore()
{
self.previewGameViewController.emulatorCore?.stop()
// We stopped emulation for 3D Touch, so now we must resume emulation and load the save state we made to make it seem like it was never stopped
if let emulatorCore = self.emulatorCore
guard let emulatorCore = self.emulatorCore else { return }
// Temporarily disable video rendering to prevent flickers
emulatorCore.videoManager.isEnabled = false
// Load the save state we stored a reference to
emulatorCore.start()
emulatorCore.pause()
if let saveState = self.emulatorCoreSaveState
{
// Temporarily disable video rendering to prevent flickers
emulatorCore.videoManager.isEnabled = false
// Load the save state we stored a reference to
emulatorCore.start()
emulatorCore.pause()
if let saveState = self.emulatorCoreSaveState
do
{
do
{
try emulatorCore.load(saveState)
}
catch EmulatorCore.SaveStateError.doesNotExist
{
print("Save State does not exist.")
}
catch let error as NSError
{
print(error)
}
try emulatorCore.load(saveState)
}
catch EmulatorCore.SaveStateError.doesNotExist
{
print("Save State does not exist.")
}
catch let error as NSError
{
print(error)
}
// Re-enable video rendering
emulatorCore.videoManager.isEnabled = true
}
// Re-enable video rendering
emulatorCore.videoManager.isEnabled = true
}
}
//MARK: - 3D Touch -
extension SaveStatesViewController: UIViewControllerPreviewingDelegate
{
private func preparePreviewGameViewController()
private func prepareEmulatorCoreSaveState()
{
if let emulatorCore = self.emulatorCore
guard let emulatorCore = self.emulatorCore else { return }
// Store reference to current game state before we stop emulation so we can resume it if user decides to not load a save state
let fileURL = FileManager.uniqueTemporaryURL()
self.emulatorCoreSaveState = emulatorCore.saveSaveState(to: fileURL)
if self.emulatorCoreSaveState != nil
{
// Store reference to current game state before we stop emulation so we can resume it if user decides to not load a save state
let fileURL = FileManager.uniqueTemporaryURL()
self.emulatorCoreSaveState = emulatorCore.saveSaveState(to: fileURL)
guard self.emulatorCoreSaveState != nil else { return }
emulatorCore.stop()
}
self.previewGameViewController.loadViewIfNeeded()
self.previewGameViewController.controllerView.isHidden = true
self.previewGameViewController.game = self.game
self.previewGameViewController.emulatorCore?.start()
self.previewGameViewController.emulatorCore?.pause()
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
@ -589,37 +562,28 @@ extension SaveStatesViewController: UIViewControllerPreviewingDelegate
previewingContext.sourceRect = layoutAttributes.frame
let saveState = self.fetchedResultsController.object(at: indexPath) as! SaveState
let actions = self.actionsForSaveState(saveState)?.lazy.filter{ $0.style != .cancel }.map{ $0.previewAction } ?? []
let previewImage = self.imageCache.object(forKey: saveState.imageFileURL) ?? UIImage(contentsOfFile: saveState.imageFileURL.path)
do
{
try self.previewGameViewController.emulatorCore?.load(saveState)
let actions = self.actionsForSaveState(saveState)?.lazy.filter{ $0.style != .cancel }.map{ $0.previewAction } ?? []
self.previewGameViewController.overridePreviewActionItems = Array(actions)
return self.previewGameViewController
}
catch EmulatorCore.SaveStateError.doesNotExist
{
print("Save State \(saveState.name) does not exist.")
}
catch let error as NSError
{
print(error)
}
let previewGameViewController = PreviewGameViewController()
previewGameViewController.game = self.game
previewGameViewController.overridePreviewActionItems = actions
previewGameViewController.previewSaveState = saveState
previewGameViewController.previewImage = previewImage
return nil
return previewGameViewController
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
{
let gameViewController = viewControllerToCommit as! GameViewController
let gameViewController = viewControllerToCommit as! PreviewGameViewController
gameViewController.emulatorCore?.pause()
let fileURL = FileManager.uniqueTemporaryURL()
if let saveState = gameViewController.emulatorCore?.saveSaveState(to: fileURL)
{
gameViewController.emulatorCore?.stop()
self.loadSaveState(saveState)
do