GBA002/Delta/Emulation/PreviewGameViewController.swift
2024-05-30 10:09:40 +08:00

309 lines
10 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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()
// nilpreviewActionItems()
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).
// emulatorCore
//
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
// emulatorCore gameView
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
// viewWillDisappear DeltaCore.GameViewController viewDidDisappear
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
// viewDidLayoutSubviews() gameView 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
// emulatorCoregameView
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
}
}