292 lines
9.0 KiB
Swift
292 lines
9.0 KiB
Swift
//
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
var isLivePreview: Bool = true
|
|
|
|
private var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated)
|
|
private var copiedSaveFiles = [(originalURL: URL, copyURL: URL)]()
|
|
|
|
private lazy var temporaryDirectoryURL: URL = {
|
|
let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent("preview-" + UUID().uuidString)
|
|
try? FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
|
|
return directoryURL
|
|
}()
|
|
|
|
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)
|
|
|
|
let size = CGSize(width: emulatorCore.preferredRenderingSize.width * 2.0, height: emulatorCore.preferredRenderingSize.height * 2.0)
|
|
self.preferredContentSize = size
|
|
}
|
|
}
|
|
|
|
override var previewActionItems: [UIPreviewActionItem] {
|
|
guard let previewActionItems = self.overridePreviewActionItems else { return [] }
|
|
return previewActionItems
|
|
}
|
|
|
|
public required init()
|
|
{
|
|
super.init()
|
|
|
|
self.delegate = self
|
|
}
|
|
|
|
public required init?(coder aDecoder: NSCoder)
|
|
{
|
|
super.init(coder: aDecoder)
|
|
|
|
self.delegate = self
|
|
}
|
|
|
|
deinit
|
|
{
|
|
// Explicitly stop emulatorCore _before_ we remove ourselves as observer
|
|
// so we can wait until stopped before restoring save files (again).
|
|
self.emulatorCore?.stop()
|
|
|
|
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
|
|
}
|
|
}
|
|
|
|
//MARK: - UIViewController -
|
|
/// UIViewController
|
|
extension PreviewGameViewController
|
|
{
|
|
override func viewDidLoad()
|
|
{
|
|
super.viewDidLoad()
|
|
|
|
self.controllerView.isHidden = true
|
|
self.controllerView.controllerSkin = nil // Skip loading controller skin from disk, which may be slow.
|
|
|
|
// Temporarily prevent emulatorCore from updating gameView to prevent flicker of black, or other visual glitches
|
|
self.emulatorCore?.remove(self.gameView)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool)
|
|
{
|
|
super.viewWillAppear(animated)
|
|
|
|
self.copySaveFiles()
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool)
|
|
{
|
|
super.viewDidAppear(animated)
|
|
|
|
self.emulatorCoreQueue.async {
|
|
self.startEmulation()
|
|
}
|
|
}
|
|
|
|
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 viewDidDisappear(_ animated: Bool)
|
|
{
|
|
super.viewDidDisappear(animated)
|
|
|
|
// Already stopped = we've already restored save files and removed directory.
|
|
if self.emulatorCore?.state != .stopped
|
|
{
|
|
// Pre-emptively restore save files in case something goes wrong while stopping emulation.
|
|
// This also ensures if the core is never stopped (for some reason), saves are still restored.
|
|
self.restoreSaveFiles(removeCopyDirectory: false)
|
|
}
|
|
}
|
|
|
|
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: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
|
|
{
|
|
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 }
|
|
|
|
switch state
|
|
{
|
|
case .running where previousState == .stopped:
|
|
self.emulatorCoreQueue.async {
|
|
// Pause to prevent it from starting before visible (in case user peeked slowly)
|
|
self.emulatorCore?.pause()
|
|
self.preparePreview()
|
|
}
|
|
|
|
case .stopped:
|
|
// Emulation has stopped, so we can safely restore save files,
|
|
// and also remove the directory they were copied to.
|
|
self.restoreSaveFiles(removeCopyDirectory: true)
|
|
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
|
|
//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)
|
|
|
|
self.emulatorCore?.resume()
|
|
}
|
|
|
|
func copySaveFiles()
|
|
{
|
|
guard let game = self.game as? Game, let gameSave = game.gameSave else { return }
|
|
|
|
self.copiedSaveFiles.removeAll()
|
|
|
|
let fileURLs = gameSave.syncableFiles.lazy.map { $0.fileURL }
|
|
for fileURL in fileURLs
|
|
{
|
|
do
|
|
{
|
|
let destinationURL = self.temporaryDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
|
|
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
|
|
|
|
self.copiedSaveFiles.append((fileURL, destinationURL))
|
|
print("Copied save file:", fileURL.lastPathComponent)
|
|
}
|
|
catch
|
|
{
|
|
print("Failed to back up save file \(fileURL.lastPathComponent).", error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func restoreSaveFiles(removeCopyDirectory: Bool)
|
|
{
|
|
for (originalURL, copyURL) in self.copiedSaveFiles
|
|
{
|
|
do
|
|
{
|
|
try FileManager.default.copyItem(at: copyURL, to: originalURL, shouldReplace: true)
|
|
print("Restored save file:", originalURL.lastPathComponent)
|
|
}
|
|
catch
|
|
{
|
|
print("Failed to restore copied save file \(copyURL.lastPathComponent).", error)
|
|
}
|
|
}
|
|
|
|
if removeCopyDirectory
|
|
{
|
|
do
|
|
{
|
|
try FileManager.default.removeItem(at: self.temporaryDirectoryURL)
|
|
}
|
|
catch
|
|
{
|
|
print("Failed to remove preview temporary directory.", error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PreviewGameViewController: GameViewControllerDelegate
|
|
{
|
|
func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool
|
|
{
|
|
return self.isLivePreview
|
|
}
|
|
}
|