GBA001/Delta/Emulation/GameViewController.swift
Riley Testut cb2caa7ef1 Replaces screen edge gesture hack with preferredScreenEdgesDeferringSystemGestures
We want priority over system gestures when tapping near edges of screen. Previously, we needed to access the private screen edge gesture recognizer, but now we can use preferredScreenEdgesDeferringSystemGestures.
2019-08-05 22:58:59 -07:00

1072 lines
38 KiB
Swift

//
// GameViewController.swift
// Delta
//
// Created by Riley Testut on 5/5/15.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import UIKit
import DeltaCore
import GBADeltaCore
import Roxas
private var kvoContext = 0
private extension GameViewController
{
struct PausedSaveState: SaveStateProtocol
{
var fileURL: URL
var gameType: GameType
var isSaved = false
init(fileURL: URL, gameType: GameType)
{
self.fileURL = fileURL
self.gameType = gameType
}
}
struct SustainInputsMapping: GameControllerInputMappingProtocol
{
let gameController: GameController
var gameControllerInputType: GameControllerInputType {
return self.gameController.inputType
}
func input(forControllerInput controllerInput: Input) -> Input?
{
if let mappedInput = self.gameController.defaultInputMapping?.input(forControllerInput: controllerInput), mappedInput == StandardGameControllerInput.menu
{
return mappedInput
}
return controllerInput
}
}
}
class GameViewController: DeltaCore.GameViewController
{
/// Assumed to be Delta.Game instance
override var game: GameProtocol? {
willSet {
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
let game = self.game as? Game
NotificationCenter.default.removeObserver(self, name: .NSManagedObjectContextDidSave, object: game?.managedObjectContext)
}
didSet {
self.emulatorCore?.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext)
let game = self.game as? Game
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.managedObjectContextDidChange(with:)), name: .NSManagedObjectContextObjectsDidChange, object: game?.managedObjectContext)
self.emulatorCore?.saveHandler = { [weak self] _ in self?.updateGameSave() }
self.updateControllerSkin()
self.updateControllers()
self.presentedGyroAlert = false
}
}
//MARK: - Private Properties -
private var pauseViewController: PauseViewController?
private var pausingGameController: GameController?
// Prevents the same save state from being saved multiple times
private var pausedSaveState: PausedSaveState? {
didSet
{
if let saveState = oldValue, self.pausedSaveState == nil
{
do
{
try FileManager.default.removeItem(at: saveState.fileURL)
}
catch
{
print(error)
}
}
}
}
private var _deepLinkResumingSaveState: SaveStateProtocol? {
didSet {
guard let saveState = oldValue, _deepLinkResumingSaveState == nil else { return }
do
{
try FileManager.default.removeItem(at: saveState.fileURL)
}
catch
{
print(error)
}
}
}
private var _isLoadingSaveState = false
// Sustain Buttons
private var isSelectingSustainedButtons = false
private var sustainInputsMapping: SustainInputsMapping?
private var sustainButtonsContentView: UIView!
private var sustainButtonsBlurView: UIVisualEffectView!
private var sustainButtonsBackgroundView: RSTPlaceholderView!
private var inputsToSustain = [AnyInput: Double]()
private var isGyroActive = false
private var presentedGyroAlert = false
override var shouldAutorotate: Bool {
return !self.isGyroActive
}
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
return .all
}
required init()
{
super.init()
self.initialize()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initialize()
}
private func initialize()
{
self.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalGameControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalGameControllerDidDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didEnterBackground(with:)), name: UIApplication.didEnterBackgroundNotification, object: UIApplication.shared)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.settingsDidChange(with:)), name: .settingsDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.deepLinkControllerLaunchGame(with:)), name: .deepLinkControllerLaunchGame, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didActivateGyro(with:)), name: GBA.didActivateGyroNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didDeactivateGyro(with:)), name: GBA.didDeactivateGyroNotification, object: nil)
}
deinit
{
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
}
// MARK: - GameControllerReceiver -
override func gameController(_ gameController: GameController, didActivate input: Input, value: Double)
{
super.gameController(gameController, didActivate: input, value: value)
if self.isSelectingSustainedButtons
{
guard let pausingGameController = self.pausingGameController, gameController == pausingGameController else { return }
if input != StandardGameControllerInput.menu
{
self.inputsToSustain[AnyInput(input)] = value
}
}
else if self.emulatorCore?.state == .running
{
guard let actionInput = ActionInput(input: input) else { return }
switch actionInput
{
case .quickSave: self.performQuickSaveAction()
case .quickLoad: self.performQuickLoadAction()
case .fastForward: self.performFastForwardAction(activate: true)
}
}
}
override func gameController(_ gameController: GameController, didDeactivate input: Input)
{
super.gameController(gameController, didDeactivate: input)
if self.isSelectingSustainedButtons
{
if input.isContinuous
{
self.inputsToSustain[AnyInput(input)] = nil
}
}
else
{
guard let actionInput = ActionInput(input: input) else { return }
switch actionInput
{
case .quickSave: break
case .quickLoad: break
case .fastForward: self.performFastForwardAction(activate: false)
}
}
}
}
//MARK: - UIViewController -
/// UIViewController
extension GameViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
// Lays out self.gameView, so we can pin self.sustainButtonsContentView to it without resulting in a temporary "cannot satisfy constraints".
self.view.layoutIfNeeded()
let gameViewContainerView = self.gameView.superview!
self.controllerView.translucentControllerSkinOpacity = Settings.translucentControllerSkinOpacity
self.sustainButtonsContentView = UIView(frame: CGRect(x: 0, y: 0, width: self.gameView.bounds.width, height: self.gameView.bounds.height))
self.sustainButtonsContentView.translatesAutoresizingMaskIntoConstraints = false
self.sustainButtonsContentView.isHidden = true
self.view.insertSubview(self.sustainButtonsContentView, aboveSubview: gameViewContainerView)
let blurEffect = UIBlurEffect(style: .dark)
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
self.sustainButtonsBlurView = UIVisualEffectView(effect: blurEffect)
self.sustainButtonsBlurView.frame = CGRect(x: 0, y: 0, width: self.sustainButtonsContentView.bounds.width, height: self.sustainButtonsContentView.bounds.height)
self.sustainButtonsBlurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.sustainButtonsContentView.addSubview(self.sustainButtonsBlurView)
let vibrancyView = UIVisualEffectView(effect: vibrancyEffect)
vibrancyView.frame = CGRect(x: 0, y: 0, width: self.sustainButtonsBlurView.contentView.bounds.width, height: self.sustainButtonsBlurView.contentView.bounds.height)
vibrancyView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.sustainButtonsBlurView.contentView.addSubview(vibrancyView)
self.sustainButtonsBackgroundView = RSTPlaceholderView(frame: CGRect(x: 0, y: 0, width: vibrancyView.contentView.bounds.width, height: vibrancyView.contentView.bounds.height))
self.sustainButtonsBackgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.sustainButtonsBackgroundView.textLabel.text = NSLocalizedString("Select Buttons to Sustain", comment: "")
self.sustainButtonsBackgroundView.textLabel.numberOfLines = 1
self.sustainButtonsBackgroundView.textLabel.minimumScaleFactor = 0.5
self.sustainButtonsBackgroundView.textLabel.adjustsFontSizeToFitWidth = true
self.sustainButtonsBackgroundView.detailTextLabel.text = NSLocalizedString("Press the Menu button when finished.", comment: "")
self.sustainButtonsBackgroundView.alpha = 0.0
vibrancyView.contentView.addSubview(self.sustainButtonsBackgroundView)
// Auto Layout
self.sustainButtonsContentView.leadingAnchor.constraint(equalTo: gameViewContainerView.leadingAnchor).isActive = true
self.sustainButtonsContentView.trailingAnchor.constraint(equalTo: gameViewContainerView.trailingAnchor).isActive = true
self.sustainButtonsContentView.topAnchor.constraint(equalTo: gameViewContainerView.topAnchor).isActive = true
self.sustainButtonsContentView.bottomAnchor.constraint(equalTo: gameViewContainerView.bottomAnchor).isActive = true
self.updateControllerSkin()
self.updateControllers()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
{
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (context) in
self.updateControllerSkin()
}, completion: nil)
}
// MARK: - Segues
/// KVO
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard let identifier = segue.identifier else { return }
switch identifier
{
case "showGamesViewController":
let gamesViewController = (segue.destination as! UINavigationController).topViewController as! GamesViewController
gamesViewController.theme = .translucent
gamesViewController.activeEmulatorCore = self.emulatorCore
self.updateAutoSaveState()
case "pause":
if let game = self.game
{
let fileURL = FileManager.default.uniqueTemporaryURL()
self.pausedSaveState = PausedSaveState(fileURL: fileURL, gameType: game.type)
self.emulatorCore?.saveSaveState(to: fileURL)
}
guard let gameController = sender as? GameController else {
fatalError("sender for pauseSegue must be the game controller that pressed the Menu button")
}
self.pausingGameController = gameController
let pauseViewController = segue.destination as! PauseViewController
pauseViewController.pauseText = (self.game as? Game)?.name ?? NSLocalizedString("Delta", comment: "")
pauseViewController.emulatorCore = self.emulatorCore
pauseViewController.saveStatesViewControllerDelegate = self
pauseViewController.cheatsViewControllerDelegate = self
pauseViewController.fastForwardItem?.isSelected = (self.emulatorCore?.rate != self.emulatorCore?.deltaCore.supportedRates.lowerBound)
pauseViewController.fastForwardItem?.action = { [unowned self] item in
self.performFastForwardAction(activate: item.isSelected)
}
pauseViewController.sustainButtonsItem?.isSelected = gameController.sustainedInputs.count > 0
pauseViewController.sustainButtonsItem?.action = { [unowned self, unowned pauseViewController] item in
for input in gameController.sustainedInputs.keys
{
gameController.unsustain(input)
}
if item.isSelected
{
self.showSustainButtonView()
pauseViewController.dismiss()
}
// Re-set gameController as pausingGameController.
self.pausingGameController = gameController
}
self.pauseViewController = pauseViewController
default: break
}
}
@IBAction private func unwindFromPauseViewController(_ segue: UIStoryboardSegue)
{
self.pauseViewController = nil
self.pausingGameController = nil
guard let identifier = segue.identifier else { return }
switch identifier
{
case "unwindFromPauseMenu":
self.pausedSaveState = nil
DispatchQueue.main.async {
if self._isLoadingSaveState
{
// If loading save state, resume emulation immediately (since the game view needs to be updated ASAP)
if self.resumeEmulation()
{
// Temporarily disable audioManager to prevent delayed audio bug when using 3D Touch Peek & Pop
self.emulatorCore?.audioManager.isEnabled = false
// Re-enable after delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.emulatorCore?.audioManager.isEnabled = true
}
}
}
else
{
// Otherwise, wait for the transition to complete before resuming emulation
self.transitionCoordinator?.animate(alongsideTransition: nil, completion: { (context) in
self.resumeEmulation()
})
}
self._isLoadingSaveState = false
}
case "unwindToGames":
DispatchQueue.main.async {
self.transitionCoordinator?.animate(alongsideTransition: nil, completion: { (context) in
self.performSegue(withIdentifier: "showGamesViewController", sender: nil)
})
}
default: break
}
}
@IBAction private func unwindFromGamesViewController(with segue: UIStoryboardSegue)
{
self.pausedSaveState = nil
if let emulatorCore = self.emulatorCore, emulatorCore.state == .paused
{
emulatorCore.resume()
}
}
// MARK: - KVO
/// KVO
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) else { return }
if let saveState = _deepLinkResumingSaveState, let emulatorCore = self.emulatorCore, emulatorCore.state == .running
{
emulatorCore.pause()
do
{
try emulatorCore.load(saveState)
}
catch
{
print(error)
}
_deepLinkResumingSaveState = nil
emulatorCore.resume()
}
if previousState == .stopped
{
self.emulatorCore?.updateCheats()
}
if self.emulatorCore?.state == .running
{
DatabaseManager.shared.performBackgroundTask { (context) in
guard let game = self.game as? Game else { return }
let backgroundGame = context.object(with: game.objectID) as! Game
backgroundGame.playedDate = Date()
context.saveWithErrorLogging()
}
}
}
}
//MARK: - Controllers -
private extension GameViewController
{
@objc func updateControllers()
{
let isExternalGameControllerConnected = ExternalGameControllerManager.shared.connectedControllers.contains(where: { $0.playerIndex != nil })
if !isExternalGameControllerConnected && Settings.localControllerPlayerIndex == nil
{
Settings.localControllerPlayerIndex = 0
}
// If Settings.localControllerPlayerIndex is non-nil, and there isn't a connected controller with same playerIndex, show controller view.
if let index = Settings.localControllerPlayerIndex, !ExternalGameControllerManager.shared.connectedControllers.contains { $0.playerIndex == index }
{
self.controllerView.playerIndex = index
self.controllerView.isHidden = false
}
else
{
self.controllerView.playerIndex = nil
self.controllerView.isHidden = true
Settings.localControllerPlayerIndex = nil
}
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
if let emulatorCore = self.emulatorCore, let game = self.game
{
// Roundabout way of combining arrays to prevent rare runtime crash in + operator :(
var controllers = [GameController]()
controllers.append(self.controllerView)
controllers.append(contentsOf: ExternalGameControllerManager.shared.connectedControllers)
for gameController in controllers
{
if gameController.playerIndex != nil
{
if let inputMapping = GameControllerInputMapping.inputMapping(for: gameController, gameType: game.type, in: DatabaseManager.shared.viewContext)
{
gameController.addReceiver(self, inputMapping: inputMapping)
gameController.addReceiver(emulatorCore, inputMapping: inputMapping)
}
else
{
gameController.addReceiver(self)
gameController.addReceiver(emulatorCore)
}
}
else
{
gameController.removeReceiver(self)
gameController.removeReceiver(emulatorCore)
}
}
}
}
func updateControllerSkin()
{
guard let game = self.game, let system = System(gameType: game.type), let window = self.view.window else { return }
let traits = DeltaCore.ControllerSkin.Traits.defaults(for: window)
let controllerSkin = Settings.preferredControllerSkin(for: system, traits: traits)
self.controllerView.controllerSkin = controllerSkin
}
}
//MARK: - Game Saves -
/// Game Saves
private extension GameViewController
{
func updateGameSave()
{
guard let game = self.game as? Game else { return }
DatabaseManager.shared.performBackgroundTask { (context) in
do
{
let game = context.object(with: game.objectID) as! Game
let hash = try RSTHasher.sha1HashOfFile(at: game.gameSaveURL)
let previousHash = game.gameSaveURL.extendedAttribute(name: "com.rileytestut.delta.sha1Hash")
guard hash != previousHash else { return }
if let gameSave = game.gameSave
{
gameSave.modifiedDate = Date()
}
else
{
let gameSave = GameSave(context: context)
gameSave.identifier = game.identifier
game.gameSave = gameSave
}
try context.save()
try game.gameSaveURL.setExtendedAttribute(name: "com.rileytestut.delta.sha1Hash", value: hash)
}
catch CocoaError.fileNoSuchFile
{
// Ignore
}
catch
{
print("Error updating game save.", error)
}
}
}
}
//MARK: - Save States -
/// Save States
extension GameViewController: SaveStatesViewControllerDelegate
{
private func updateAutoSaveState()
{
// Ensures game is non-nil and also a Game subclass
guard let game = self.game as? Game 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
guard !savedPausedSaveState else { return }
self.pausedSaveState?.isSaved = true
// Must be done synchronously
let backgroundContext = DatabaseManager.shared.newBackgroundContext()
backgroundContext.performAndWait {
let game = backgroundContext.object(with: game.objectID) as! Game
let fetchRequest = SaveState.fetchRequest(for: game, type: .auto)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(SaveState.creationDate), ascending: true)]
do
{
let saveStates = try fetchRequest.execute()
if let saveState = saveStates.first, saveStates.count >= 2
{
// If there are two or more auto save states, update the oldest one
self.update(saveState, with: self.pausedSaveState)
// 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, with: self.pausedSaveState)
}
}
catch
{
print(error)
}
backgroundContext.saveWithErrorLogging()
}
}
private func update(_ saveState: SaveState, with replacementSaveState: SaveStateProtocol? = nil)
{
let isRunning = (self.emulatorCore?.state == .running)
if isRunning
{
self.pauseEmulation()
}
if let replacementSaveState = replacementSaveState
{
do
{
if FileManager.default.fileExists(atPath: saveState.fileURL.path)
{
// Don't use replaceItem(), since that removes the original file as well
try FileManager.default.removeItem(at: saveState.fileURL)
}
try FileManager.default.copyItem(at: replacementSaveState.fileURL, to: saveState.fileURL)
}
catch
{
print(error)
}
}
else
{
self.emulatorCore?.saveSaveState(to: saveState.fileURL)
}
if let snapshot = self.gameView.snapshot(), let data = snapshot.pngData()
{
do
{
try data.write(to: saveState.imageFileURL, options: [.atomicWrite])
}
catch
{
print(error)
}
}
saveState.modifiedDate = Date()
if isRunning
{
self.resumeEmulation()
}
}
private func load(_ saveState: SaveStateProtocol)
{
let isRunning = (self.emulatorCore?.state == .running)
if isRunning
{
self.pauseEmulation()
}
// 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.default.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
{
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
{
print("Save State does not exist.")
}
catch let error as NSError
{
print(error)
}
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
{
self.pauseViewController?.dismiss()
}
}
func saveStatesViewController(_ saveStatesViewController: SaveStatesViewController, loadSaveState saveState: SaveStateProtocol)
{
self._isLoadingSaveState = true
self.load(saveState)
self.pauseViewController?.dismiss()
}
}
//MARK: - Cheats -
/// Cheats
extension GameViewController: CheatsViewControllerDelegate
{
func cheatsViewController(_ cheatsViewController: CheatsViewController, activateCheat cheat: Cheat)
{
self.emulatorCore?.activateCheatWithErrorLogging(cheat)
}
func cheatsViewController(_ cheatsViewController: CheatsViewController, deactivateCheat cheat: Cheat)
{
self.emulatorCore?.deactivate(cheat)
}
}
//MARK: - Sustain Buttons -
private extension GameViewController
{
func showSustainButtonView()
{
guard let gameController = self.pausingGameController else { return }
self.isSelectingSustainedButtons = true
let sustainInputsMapping = SustainInputsMapping(gameController: gameController)
gameController.addReceiver(self, inputMapping: sustainInputsMapping)
let blurEffect = self.sustainButtonsBlurView.effect
self.sustainButtonsBlurView.effect = nil
self.sustainButtonsContentView.isHidden = false
UIView.animate(withDuration: 0.4) {
self.sustainButtonsBlurView.effect = blurEffect
self.sustainButtonsBackgroundView.alpha = 1.0
}
}
func hideSustainButtonView()
{
guard let gameController = self.pausingGameController else { return }
self.isSelectingSustainedButtons = false
self.updateControllers()
self.sustainInputsMapping = nil
// Activate all sustained inputs, since they will now be mapped to game inputs.
for (input, value) in self.inputsToSustain
{
gameController.sustain(input, value: value)
}
let blurEffect = self.sustainButtonsBlurView.effect
UIView.animate(withDuration: 0.4, animations: {
self.sustainButtonsBlurView.effect = nil
self.sustainButtonsBackgroundView.alpha = 0.0
}) { (finished) in
self.sustainButtonsContentView.isHidden = true
self.sustainButtonsBlurView.effect = blurEffect
}
self.inputsToSustain = [:]
}
}
//MARK: - Action Inputs -
/// Action Inputs
extension GameViewController
{
func performQuickSaveAction()
{
guard let game = self.game as? Game else { return }
let backgroundContext = DatabaseManager.shared.newBackgroundContext()
backgroundContext.performAndWait {
let game = backgroundContext.object(with: game.objectID) as! Game
let fetchRequest = SaveState.fetchRequest(for: game, type: .quick)
do
{
if let quickSaveState = try fetchRequest.execute().first
{
self.update(quickSaveState)
}
else
{
let saveState = SaveState(context: backgroundContext)
saveState.type = .quick
saveState.game = game
self.update(saveState)
}
}
catch
{
print(error)
}
backgroundContext.saveWithErrorLogging()
}
}
func performQuickLoadAction()
{
guard let game = self.game as? Game else { return }
let fetchRequest = SaveState.fetchRequest(for: game, type: .quick)
do
{
if let quickSaveState = try DatabaseManager.shared.viewContext.fetch(fetchRequest).first
{
self.load(quickSaveState)
}
}
catch
{
print(error)
}
}
func performFastForwardAction(activate: Bool)
{
guard let emulatorCore = self.emulatorCore else { return }
if activate
{
emulatorCore.rate = emulatorCore.deltaCore.supportedRates.upperBound
}
else
{
emulatorCore.rate = emulatorCore.deltaCore.supportedRates.lowerBound
}
}
}
//MARK: - GameViewControllerDelegate -
/// GameViewControllerDelegate
extension GameViewController: GameViewControllerDelegate
{
func gameViewController(_ gameViewController: DeltaCore.GameViewController, handleMenuInputFrom gameController: GameController)
{
if let pausingGameController = self.pausingGameController
{
guard pausingGameController == gameController else { return }
}
if self.isSelectingSustainedButtons
{
self.hideSustainButtonView()
}
if let pauseViewController = self.pauseViewController, !self.isSelectingSustainedButtons
{
pauseViewController.dismiss()
}
else if self.presentedViewController == nil
{
self.pauseEmulation()
self.performSegue(withIdentifier: "pause", sender: gameController)
}
}
func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool
{
var result = false
rst_dispatch_sync_on_main_thread {
result = (self.presentedViewController == nil || self.presentedViewController?.isDisappearing == true) && !self.isSelectingSustainedButtons && self.view.window != nil
}
return result
}
}
//MARK: - Notifications -
private extension GameViewController
{
@objc func didEnterBackground(with notification: Notification)
{
self.updateAutoSaveState()
}
@objc func managedObjectContextDidChange(with notification: Notification)
{
guard let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> else { return }
guard let game = self.game as? Game else { return }
if deletedObjects.contains(game)
{
self.emulatorCore?.gameViews.forEach { $0.inputImage = nil }
self.game = nil
}
}
@objc func settingsDidChange(with notification: Notification)
{
guard let settingsName = notification.userInfo?[Settings.NotificationUserInfoKey.name] as? Settings.Name else { return }
switch settingsName
{
case .localControllerPlayerIndex: self.updateControllers()
case .preferredControllerSkin:
guard
let system = notification.userInfo?[Settings.NotificationUserInfoKey.system] as? System,
let traits = notification.userInfo?[Settings.NotificationUserInfoKey.traits] as? DeltaCore.ControllerSkin.Traits
else { return }
if system.gameType == self.game?.type && traits.orientation == self.controllerView.controllerSkinTraits?.orientation
{
self.updateControllerSkin()
}
case .translucentControllerSkinOpacity: self.controllerView.translucentControllerSkinOpacity = Settings.translucentControllerSkinOpacity
case .syncingService: break
}
}
@objc func deepLinkControllerLaunchGame(with notification: Notification)
{
guard let game = notification.userInfo?[DeepLink.Key.game] as? Game else { return }
self.game = game
if let pausedSaveState = self.pausedSaveState, game == (self.game as? Game)
{
// Launching current game via deep link, so we store a copy of the paused save state to resume when emulator core is started.
do
{
let temporaryURL = FileManager.default.uniqueTemporaryURL()
try FileManager.default.copyItem(at: pausedSaveState.fileURL, to: temporaryURL)
_deepLinkResumingSaveState = DeltaCore.SaveState(fileURL: temporaryURL, gameType: game.type)
}
catch
{
print(error)
}
}
if let pauseViewController = self.pauseViewController
{
let segue = UIStoryboardSegue(identifier: "unwindFromPauseMenu", source: pauseViewController, destination: self)
self.unwindFromPauseViewController(segue)
}
else if
let navigationController = self.presentedViewController as? UINavigationController,
let pageViewController = navigationController.topViewController?.children.first as? UIPageViewController,
let gameCollectionViewController = pageViewController.viewControllers?.first as? GameCollectionViewController
{
let segue = UIStoryboardSegue(identifier: "unwindFromGames", source: gameCollectionViewController, destination: self)
self.unwindFromGamesViewController(with: segue)
}
self.dismiss(animated: true, completion: nil)
}
@objc func didActivateGyro(with notification: Notification)
{
self.isGyroActive = true
guard !self.presentedGyroAlert else { return }
self.presentedGyroAlert = true
func presentToastView()
{
let toastView = RSTToastView(text: NSLocalizedString("Autorotation Disabled", comment: ""), detailText: NSLocalizedString("Pause game to change orientation.", comment: ""))
toastView.textLabel.textAlignment = .center
toastView.presentationEdge = .bottom
if let traits = self.controllerView.controllerSkinTraits, traits.orientation == .landscape, self.controllerView?.controllerSkin?.gameScreenFrame(for: traits) == nil
{
// Only change landscape vertical offset if there is no custom game screen frame for the current controller skin.
toastView.edgeOffset.vertical = 30
}
toastView.show(in: self.gameView, duration: 3.0)
}
DispatchQueue.main.async {
if let transitionCoordinator = self.transitionCoordinator
{
transitionCoordinator.animate(alongsideTransition: nil) { (context) in
presentToastView()
}
}
else
{
presentToastView()
}
}
}
@objc func didDeactivateGyro(with notification: Notification)
{
self.isGyroActive = false
}
}