GBA001/Delta/Emulation/GameViewController.swift
Riley Testut 77983e73dd Adds “Respect Silent Mode” setting to configure whether Delta plays game audio in Silent Mode
Delta will also now automatically mute game audio if another app is playing audio.

[Missed] Mutes game audio correctly
2023-01-31 14:34:32 -06:00

1373 lines
48 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 MelonDSDeltaCore
import Systems
import struct DSDeltaCore.DS
import Roxas
import AltKit
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.updateAudio()
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
private var presentedJITAlert = 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)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didEnableJIT(with:)), name: ServerManager.didEnableJITNotification, 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
}
else if self.emulatorCore?.deltaCore == MelonDS.core, ProcessInfo.processInfo.isJITAvailable
{
self.showJITEnabledAlert()
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
{
super.viewWillTransition(to: size, with: coordinator)
guard UIApplication.shared.applicationState != .background else { return }
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
if self.emulatorCore?.deltaCore == MelonDS.core, ProcessInfo.processInfo.isJITAvailable
{
self.transitionCoordinator?.animate(alongsideTransition: nil, completion: { (context) in
self.showJITEnabledAlert()
})
}
}
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)
if self.view.bounds.width > self.view.bounds.height
{
touchControllerSkin.screenLayoutAxis = .horizontal
}
else
{
touchControllerSkin.screenLayoutAxis = .vertical
}
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: - Audio -
/// Audio
private extension GameViewController
{
func updateAudio()
{
self.emulatorCore?.audioManager.respectsSilentMode = Settings.respectSilentMode
}
}
//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
} completion: { _ in
self.controllerView.becomeFirstResponder()
}
}
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.controllerView.resignFirstResponder()
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)
}
func showJITEnabledAlert()
{
guard !self.presentedJITAlert, self.presentedViewController == nil, self.game != nil else { return }
self.presentedJITAlert = true
func presentToastView()
{
let detailText: String?
let duration: TimeInterval
if UserDefaults.standard.jitEnabledAlertCount < 3
{
detailText = NSLocalizedString("You can now Fast Forward DS games up to 3x speed.", comment: "")
duration = 5.0
}
else
{
detailText = nil
duration = 2.0
}
let toastView = RSTToastView(text: NSLocalizedString("JIT Compilation Enabled", comment: ""), detailText: detailText)
toastView.edgeOffset.vertical = 8
self.show(toastView, duration: duration)
UserDefaults.standard.jitEnabledAlertCount += 1
}
DispatchQueue.main.async {
if let transitionCoordinator = self.transitionCoordinator
{
transitionCoordinator.animate(alongsideTransition: nil) { (context) in
presentToastView()
}
}
else
{
presentToastView()
}
}
}
}
//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 .respectSilentMode:
self.updateAudio()
case .syncingService, .isAltJITEnabled: 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 didEnableJIT(with notification: Notification)
{
DispatchQueue.main.async {
self.showJITEnabledAlert()
}
DispatchQueue.global(qos: .utility).async {
guard let emulatorCore = self.emulatorCore, let emulatorBridge = emulatorCore.deltaCore.emulatorBridge as? MelonDSEmulatorBridge, !emulatorBridge.isJITEnabled
else { return }
guard emulatorCore.state != .stopped else {
// Emulator core is not running, which means we can set
// isJITEnabled to true without resetting the core.
emulatorBridge.isJITEnabled = true
return
}
let isVideoEnabled = emulatorCore.videoManager.isEnabled
emulatorCore.videoManager.isEnabled = false
let isRunning = (emulatorCore.state == .running)
if isRunning
{
self.pauseEmulation()
}
let temporaryFileURL = FileManager.default.uniqueTemporaryURL()
let saveState = emulatorCore.saveSaveState(to: temporaryFileURL)
emulatorCore.stop()
emulatorBridge.isJITEnabled = true
emulatorCore.start()
emulatorCore.pause()
do
{
try emulatorCore.load(saveState)
}
catch
{
print("Failed to load save state after enabling JIT.", error)
}
if isRunning
{
self.resumeEmulation()
}
emulatorCore.videoManager.isEnabled = isVideoEnabled
}
}
@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
@NSManaged var jitEnabledAlertCount: Int
}