GBA002/Delta/Emulation/PreviewGameViewController.swift
2020-04-27 13:13:34 -07:00

267 lines
8.4 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()
}
}
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
}
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
// 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.emulatorCore?.start()
}
}
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)
}
}
}
}