// // 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.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: - 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) } 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 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, .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 }