GBA002/Delta/Emulation/GameViewController.swift
2021-02-10 12:29:40 -06:00

1235 lines
44 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 Systems
import struct DSDeltaCore.DS
import Roxas
private var kvoContext = 0
private extension DeltaCore.ControllerSkin
{
func hasTouchScreen(for traits: DeltaCore.ControllerSkin.Traits) -> Bool
{
let hasTouchScreen = self.items(for: traits)?.contains(where: { $0.kind == .touchScreen }) ?? false
return hasTouchScreen
}
}
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 DefaultInputMapping: 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)
{
return mappedInput
}
// Only intercept controller skin inputs.
guard controllerInput.type == .controller(.controllerSkin) else { return nil }
let actionInput = ActionInput(stringValue: controllerInput.stringValue)
return actionInput
}
}
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() }
if oldValue?.fileURL != game?.fileURL
{
self.shouldResetSustainedInputs = true
}
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 shouldResetSustainedInputs = false
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)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.emulationDidQuit(with:)), name: EmulatorCore.emulationDidQuitNotification, 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 let emulatorCore = self.emulatorCore, 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)
case .toggleFastForward:
let isFastForwarding = (emulatorCore.rate != emulatorCore.deltaCore.supportedRates.lowerBound)
self.performFastForwardAction(activate: !isFastForwarding)
}
}
}
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)
case .toggleFastForward: break
}
}
}
}
//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()
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: self.gameView)
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 Hold Down", 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: self.gameView.leadingAnchor).isActive = true
self.sustainButtonsContentView.trailingAnchor.constraint(equalTo: self.gameView.trailingAnchor).isActive = true
self.sustainButtonsContentView.topAnchor.constraint(equalTo: self.gameView.topAnchor).isActive = true
self.sustainButtonsContentView.bottomAnchor.constraint(equalTo: self.gameView.bottomAnchor).isActive = true
self.updateControllers()
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
if self.emulatorCore?.deltaCore == DS.core, UserDefaults.standard.desmumeDeprecatedAlertCount < 3
{
let toastView = RSTToastView(text: NSLocalizedString("DeSmuME Core Deprecated", comment: ""), detailText: NSLocalizedString("Switch to the melonDS core in Settings for latest improvements.", comment: ""))
self.show(toastView, duration: 5.0)
UserDefaults.standard.desmumeDeprecatedAlertCount += 1
}
}
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
if let emulatorCore = self.emulatorCore
{
gamesViewController.theme = .translucent
gamesViewController.activeEmulatorCore = emulatorCore
self.updateAutoSaveState()
}
else
{
gamesViewController.theme = .opaque
}
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
}
if self.emulatorCore?.deltaCore.supportedRates.upperBound == 1
{
pauseViewController.fastForwardItem = nil
}
switch self.game?.type
{
case .ds? where self.emulatorCore?.deltaCore == DS.core:
// Cheats are not supported by DeSmuME core.
pauseViewController.cheatCodesItem = nil
case .genesis?:
// GPGX core does not support cheats yet.
pauseViewController.cheatCodesItem = nil
default: break
}
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(where: { $0.playerIndex == index })
{
self.controllerView.playerIndex = index
self.controllerView.isHidden = false
}
else
{
if let game = self.game,
let traits = self.controllerView.controllerSkinTraits,
let controllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: game.type),
controllerSkin.hasTouchScreen(for: traits)
{
self.controllerView.isHidden = false
self.controllerView.playerIndex = 0
}
else
{
self.controllerView.isHidden = true
self.controllerView.playerIndex = nil
}
Settings.localControllerPlayerIndex = nil
}
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
// 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)
if let emulatorCore = self.emulatorCore, let game = self.game
{
for gameController in controllers
{
if gameController.playerIndex != nil
{
let inputMapping: GameControllerInputMappingProtocol
if let mapping = GameControllerInputMapping.inputMapping(for: gameController, gameType: game.type, in: DatabaseManager.shared.viewContext)
{
inputMapping = mapping
}
else
{
inputMapping = DefaultInputMapping(gameController: gameController)
}
gameController.addReceiver(self, inputMapping: inputMapping)
gameController.addReceiver(emulatorCore, inputMapping: inputMapping)
}
else
{
gameController.removeReceiver(self)
gameController.removeReceiver(emulatorCore)
}
}
}
if self.shouldResetSustainedInputs
{
for controller in controllers
{
for input in controller.sustainedInputs.keys
{
controller.unsustain(input)
}
}
self.shouldResetSustainedInputs = false
}
self.controllerView.isButtonHapticFeedbackEnabled = Settings.isButtonHapticFeedbackEnabled
self.controllerView.isThumbstickHapticFeedbackEnabled = Settings.isThumbstickHapticFeedbackEnabled
self.updateControllerSkin()
}
func updateControllerSkin()
{
guard let game = self.game as? Game, let window = self.view.window else { return }
let traits = DeltaCore.ControllerSkin.Traits.defaults(for: window)
if Settings.localControllerPlayerIndex != nil
{
let controllerSkin = Settings.preferredControllerSkin(for: game, traits: traits)
self.controllerView.controllerSkin = controllerSkin
}
else if let controllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: game.type), controllerSkin.hasTouchScreen(for: traits)
{
var touchControllerSkin = TouchControllerSkin(controllerSkin: controllerSkin)
touchControllerSkin.layoutGuide = self.view.safeAreaLayoutGuide
switch traits.orientation
{
case .portrait: touchControllerSkin.screenLayoutAxis = .vertical
case .landscape: touchControllerSkin.screenLayoutAxis = .horizontal
}
self.controllerView.controllerSkin = touchControllerSkin
}
self.view.setNeedsLayout()
}
}
//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 }
guard let emulatorCore = self.emulatorCore, emulatorCore.state != .stopped 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.emulatorCore?.videoManager.snapshot(), let data = snapshot.pngData()
{
do
{
try data.write(to: saveState.imageFileURL, options: [.atomicWrite])
}
catch
{
print(error)
}
}
saveState.modifiedDate = Date()
saveState.coreIdentifier = self.emulatorCore?.deltaCore.identifier
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
}
}
private extension GameViewController
{
func show(_ toastView: RSTToastView, duration: TimeInterval = 3.0)
{
toastView.textLabel.textAlignment = .center
toastView.presentationEdge = .top
toastView.show(in: self.view, duration: duration)
}
}
//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, .isButtonHapticFeedbackEnabled, .isThumbstickHapticFeedbackEnabled:
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 }
let previousGame = self.game
self.game = game
if let pausedSaveState = self.pausedSaveState, game == (previousGame 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: ""))
self.show(toastView)
}
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
}
@objc func emulationDidQuit(with notification: Notification)
{
DispatchQueue.main.async {
guard self.presentedViewController == nil else { return }
// Wait for emulation to stop completely before performing segue.
var token: NSKeyValueObservation?
token = self.emulatorCore?.observe(\.state, options: [.initial]) { (emulatorCore, change) in
guard emulatorCore.state == .stopped else { return }
DispatchQueue.main.async {
self.game = nil
self.performSegue(withIdentifier: "showGamesViewController", sender: nil)
}
token?.invalidate()
}
}
}
}
private extension UserDefaults
{
@NSManaged var desmumeDeprecatedAlertCount: Int
}