From 60bc9dcfbcdc627ba785ab492c301d4b5d4b6a60 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Sun, 17 Jul 2016 16:41:57 -0500 Subject: [PATCH] Adds basic implementation of GameViewController, replacement for EmulationViewController --- Cores/DeltaCore | 2 +- Delta.xcodeproj/project.pbxproj | 12 +- Delta/Base.lproj/Main.storyboard | 153 +--- Delta/Emulation/EmulationViewController.swift | 681 ------------------ Delta/Emulation/GameViewController.swift | 153 ++++ .../Game Selection/GamesViewController.swift | 33 +- 6 files changed, 185 insertions(+), 849 deletions(-) delete mode 100644 Delta/Emulation/EmulationViewController.swift create mode 100644 Delta/Emulation/GameViewController.swift diff --git a/Cores/DeltaCore b/Cores/DeltaCore index 9a587be..d819614 160000 --- a/Cores/DeltaCore +++ b/Cores/DeltaCore @@ -1 +1 @@ -Subproject commit 9a587be47fc89154d7917267e8d423e8a5af0951 +Subproject commit d819614e5fe422aa4975f70e3d4f1d2dc97b9c24 diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index b5e08fd..f936360 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FFD1C5DA3C500C1184C /* PausePresentationController.swift */; }; BF3540001C5DA3C500C1184C /* PausePresentationControllerContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */; }; BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */; }; - BF3540051C5DA70400C1184C /* SaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */; }; BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540071C5DAFAD00C1184C /* PauseTransitionCoordinator.swift */; }; BF4566E81BC090B6007BFA1A /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BF4566E61BC090B6007BFA1A /* Model.xcdatamodeld */; }; BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */; }; @@ -46,12 +45,14 @@ BF99C6951D0A9AA600BA92BC /* SNESDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF9F4FCF1AAD7B87004C9500 /* DeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; }; BF9F4FD01AAD7B87004C9500 /* DeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63BDE91D389EEB00FCB040 /* GameViewController.swift */; }; BFA2315C1CED10BE0011E35A /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA2315B1CED10BE0011E35A /* Action.swift */; }; BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAA1FEC1B8AA4FA00495943 /* Settings.swift */; }; BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAA1FF31B8AD7F900495943 /* ControllersSettingsViewController.swift */; }; BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB141171BE46934004FBF46 /* GameCollectionViewDataSource.swift */; }; BFC2731A1BE6152200D22B05 /* GameCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC273171BE6152200D22B05 /* GameCollection.swift */; }; BFC9B7391CEFCD34008629BB /* CheatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */; }; + BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */; }; BFDB28451BC9DA7B001D0C83 /* GamePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB28441BC9DA7B001D0C83 /* GamePickerController.swift */; }; BFDE393C1BC0CEDF003F72E8 /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDE39391BC0CEDF003F72E8 /* Game.swift */; }; BFE704F51CEA426E0058BAC8 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; }; @@ -61,7 +62,6 @@ BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */; }; BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFFA71E01AAC406100EE9DD1 /* Main.storyboard */; }; BFFA71E71AAC406100EE9DD1 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFFA71E51AAC406100EE9DD1 /* LaunchScreen.xib */; }; - BFFB709F1AF99B1700DE56FE /* EmulationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFB709E1AF99B1700DE56FE /* EmulationViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -115,6 +115,7 @@ BF4566E71BC090B6007BFA1A /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; BF5E7F451B9A652600AE44F8 /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = ""; }; + BF63BDE91D389EEB00FCB040 /* GameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = ""; }; BF65E8621CEE5C6A00CD3247 /* Cheat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cheat.swift; sourceTree = ""; }; BF6BB2451BB73FE800CCF94A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; BF70798B1B6B464B0019077C /* ZipZap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ZipZap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -142,7 +143,6 @@ BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; BFFA71E11AAC406100EE9DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; BFFA71E61AAC406100EE9DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; - BFFB709E1AF99B1700DE56FE /* EmulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmulationViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -399,7 +399,7 @@ BFFB709D1AF99ACA00DE56FE /* Emulation */ = { isa = PBXGroup; children = ( - BFFB709E1AF99B1700DE56FE /* EmulationViewController.swift */, + BF63BDE91D389EEB00FCB040 /* GameViewController.swift */, ); path = Emulation; sourceTree = ""; @@ -541,9 +541,9 @@ BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */, BF7AE8241C2E984300B1B5BC /* GridCollectionViewLayout.swift in Sources */, BF1FB1861C5EE643007E2494 /* SaveState.swift in Sources */, + BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */, BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */, BFA2315C1CED10BE0011E35A /* Action.swift in Sources */, - BFFB709F1AF99B1700DE56FE /* EmulationViewController.swift in Sources */, BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */, BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */, BF27CC971BCC890700A20D89 /* GamesCollectionViewController.swift in Sources */, @@ -565,6 +565,7 @@ BF762E9E1BC19D31002C8866 /* DatabaseManager.swift in Sources */, BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */, BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */, + BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */, BF65E8631CEE5C6A00CD3247 /* Cheat.swift in Sources */, BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */, BF0CDDAD1C8155D200640168 /* LoadImageOperation.swift in Sources */, @@ -574,7 +575,6 @@ BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */, BFDB28451BC9DA7B001D0C83 /* GamePickerController.swift in Sources */, BF7AE8081C2E858400B1B5BC /* PauseMenuViewController.swift in Sources */, - BF3540051C5DA70400C1184C /* SaveStatesViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Delta/Base.lproj/Main.storyboard b/Delta/Base.lproj/Main.storyboard index 2448a78..8512ccb 100644 --- a/Delta/Base.lproj/Main.storyboard +++ b/Delta/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - + - + + + @@ -14,17 +16,16 @@ - + - - + @@ -54,9 +55,9 @@ - + - + @@ -65,17 +66,16 @@ - + - - - - + + + @@ -88,130 +88,25 @@ - + - - + + - + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -258,13 +153,13 @@ - + - + diff --git a/Delta/Emulation/EmulationViewController.swift b/Delta/Emulation/EmulationViewController.swift deleted file mode 100644 index 8fc1ca3..0000000 --- a/Delta/Emulation/EmulationViewController.swift +++ /dev/null @@ -1,681 +0,0 @@ -// -// EmulationViewController.swift -// Delta -// -// Created by Riley Testut on 5/5/15. -// Copyright (c) 2015 Riley Testut. All rights reserved. -// - -import UIKit - -import DeltaCore -import Roxas - -// Temporary wrapper around dispatch_semaphore_t until Swift 3 + modernized libdispatch -private struct DispatchSemaphore: Hashable -{ - let semaphore: Dispatch.DispatchSemaphore - - var hashValue: Int { - return semaphore.hash - } - - init(value: Int) - { - self.semaphore = Dispatch.DispatchSemaphore(value: value) - } -} - -private func ==(lhs: DispatchSemaphore, rhs: DispatchSemaphore) -> Bool -{ - return lhs.semaphore.isEqual(rhs.semaphore) -} - -class EmulationViewController: UIViewController -{ - //MARK: - Properties - - /** Properties **/ - - /// Should only be set when preparing for segue. Otherwise, should be considered immutable - var game: Game! { - didSet - { - guard oldValue != game else { return } - - self.emulatorCore = EmulatorCore(game: game) - - } - } - private(set) var emulatorCore: EmulatorCore! { - didSet - { - // Cannot set directly, or else we're left with a strong reference cycle - //self.emulatorCore.updateHandler = emulatorCoreDidUpdate - - self.emulatorCore.updateHandler = { [weak self] core in - self?.emulatorCoreDidUpdate(core) - } - - self.preferredContentSize = self.emulatorCore.preferredRenderingSize - } - } - - // If non-nil, will override the default preview action items returned in previewActionItems() - var overridePreviewActionItems: [UIPreviewActionItem]? - - // Annoying iOS gotcha: if the previewingContext(_:viewControllerForLocation:) callback takes too long, the peek/preview starts, but fails to actually present the view controller - // To workaround, we have this closure to defer work for Peeking/Popping until the view controller appears - // Hacky, but works - var deferredPreparationHandler: ((Void) -> Void)? - - //MARK: - Private Properties - private var pauseViewController: PauseViewController? - private var pausingGameController: GameController? - - private var context = CIContext(options: [kCIContextWorkingColorSpace: NSNull()]) - - private var updateSemaphores = Set() - - private var sustainedInputs = [ObjectIdentifier: [Input]]() - private var reactivateSustainInputsQueue: OperationQueue - private var choosingSustainedButtons = false - - @IBOutlet private var controllerView: ControllerView! - @IBOutlet private var gameView: GameView! - @IBOutlet private var sustainButtonContentView: UIView! - @IBOutlet private var backgroundView: RSTBackgroundView! - - @IBOutlet private var controllerViewHeightConstraint: NSLayoutConstraint! - - - //MARK: - Initializers - - /** Initializers **/ - required init?(coder aDecoder: NSCoder) - { - self.reactivateSustainInputsQueue = OperationQueue() - self.reactivateSustainInputsQueue.maxConcurrentOperationCount = 1 - - super.init(coder: aDecoder) - - NotificationCenter.default.addObserver(self, selector: #selector(EmulationViewController.updateControllers), name: .externalControllerDidConnect, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(EmulationViewController.updateControllers), name: .externalControllerDidDisconnect, object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(EmulationViewController.willResignActive(_:)), name: NSNotification.Name.UIApplicationWillResignActive, object: UIApplication.shared()) - NotificationCenter.default.addObserver(self, selector: #selector(EmulationViewController.didBecomeActive(_:)), name: NSNotification.Name.UIApplicationDidBecomeActive, object: UIApplication.shared()) - } - - deinit - { - // To ensure the emulation stops when cancelling a peek/preview gesture - self.emulatorCore.stop() - } - - //MARK: - Overrides - /** Overrides **/ - - //MARK: - UIViewController - /// UIViewController - override func viewDidLoad() - { - super.viewDidLoad() - - // Set this to 0 now and update it in viewDidLayoutSubviews to ensure there are never conflicting constraints - // (such as when peeking and popping) - self.controllerViewHeightConstraint.constant = 0 - - self.gameView.backgroundColor = UIColor.clear() - self.emulatorCore.add(self.gameView) - - self.backgroundView.textLabel.text = NSLocalizedString("Select Buttons to Sustain", comment: "") - self.backgroundView.detailTextLabel.text = NSLocalizedString("Press the Menu button when finished.", comment: "") - - let controllerSkin = ControllerSkin.standardControllerSkin(for: self.game.type) - - self.controllerView.controllerSkin = controllerSkin - - self.updateControllers() - } - - override func viewDidAppear(_ animated: Bool) - { - super.viewDidAppear(animated) - - self.deferredPreparationHandler?() - self.deferredPreparationHandler = nil - - // Yes, order DOES matter here, in order to prevent audio from being slightly delayed after peeking with 3D Touch (ugh so tired of that issue) - switch self.emulatorCore.state - { - case .stopped: - self.emulatorCore.start() - self.updateCheats() - - case .running: break - case .paused: - self.updateCheats() - self.resumeEmulation() - } - - // Toggle audioManager.enabled to reset the audio buffer and ensure the audio isn't delayed from the beginning - // This is especially noticeable when peeking a game - self.emulatorCore.audioManager.enabled = false - self.emulatorCore.audioManager.enabled = true - } - - override func viewDidLayoutSubviews() - { - super.viewDidLayoutSubviews() - - if Settings.localControllerPlayerIndex != nil && self.controllerView.intrinsicContentSize() != CGSize(width: UIViewNoIntrinsicMetric, height: UIViewNoIntrinsicMetric) && !self.isPreviewing - { - let scale = self.view.bounds.width / self.controllerView.intrinsicContentSize().width - self.controllerViewHeightConstraint.constant = self.controllerView.intrinsicContentSize().height * scale - } - else - { - self.controllerViewHeightConstraint.constant = 0 - } - - self.controllerView.isHidden = self.isPreviewing - } - - override func prefersStatusBarHidden() -> Bool - { - return true - } - - /// - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) - { - super.viewWillTransition(to: size, with: coordinator) - - self.controllerView.beginAnimatingUpdateControllerSkin() - - coordinator.animate(alongsideTransition: { _ in - - if self.emulatorCore.state == .paused - { - // We need to manually "refresh" the game screen, otherwise the system tries to cache the rendered image, but skews it incorrectly when rotating b/c of UIVisualEffectView - self.gameView.inputImage = self.gameView.outputImage - } - - }, completion: { _ in - self.controllerView.finishAnimatingUpdateControllerSkin() - }) - } - - // MARK: - Navigation - - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) - { - self.pauseEmulation() - - if segue.identifier == "pauseSegue" - { - 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.destinationViewController as! PauseViewController - pauseViewController.pauseText = self.game.name - - // Swift has a bug where using unowned references can lead to swift_abortRetainUnowned errors. - // Specifically, if you pause a game, open the save states menu, go back, return to menu, select a new game, then try to pause it, it will crash - // As a dirty workaround, we just use a weak reference, and force unwrap it if needed - - let saveStateItem = PauseItem(image: UIImage(named: "SaveSaveState")!, text: NSLocalizedString("Save State", comment: ""), action: { [unowned self] _ in - pauseViewController.presentSaveStateViewControllerWithMode(.saving, delegate: self) - }) - - let loadStateItem = PauseItem(image: UIImage(named: "LoadSaveState")!, text: NSLocalizedString("Load State", comment: ""), action: { [unowned self] _ in - pauseViewController.presentSaveStateViewControllerWithMode(.loading, delegate: self) - }) - - let cheatCodesItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Cheat Codes", comment: ""), action: { [unowned self] _ in - pauseViewController.presentCheatsViewController(delegate: self) - }) - - var sustainButtonsItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Sustain Buttons", comment: ""), action: { [unowned self] item in - - self.resetSustainedInputs(forGameController: gameController) - - if item.selected - { - self.showSustainButtonView() - pauseViewController.dismiss() - } - }) - sustainButtonsItem.selected = self.sustainedInputs[ObjectIdentifier(gameController)]?.count > 0 - - var fastForwardItem = PauseItem(image: UIImage(named: "FastForward")!, text: NSLocalizedString("Fast Forward", comment: ""), action: { [unowned self] item in - self.emulatorCore.rate = item.selected ? self.emulatorCore.configuration.supportedRates.upperBound : self.emulatorCore.configuration.supportedRates.lowerBound - }) - fastForwardItem.selected = self.emulatorCore.rate == self.emulatorCore.configuration.supportedRates.lowerBound ? false : true - - pauseViewController.items = [saveStateItem, loadStateItem, cheatCodesItem, fastForwardItem, sustainButtonsItem] - - self.pauseViewController = pauseViewController - } - } - - @IBAction func unwindFromPauseViewController(_ segue: UIStoryboardSegue) - { - self.pauseViewController = nil - self.pausingGameController = nil - - if self.resumeEmulation() - { - // Temporarily disable audioManager to prevent delayed audio bug when using 3D Touch Peek & Pop - self.emulatorCore.audioManager.enabled = false - - // Re-enable after delay - DispatchQueue.main.after(when: DispatchTime.now() + Double(Int64(0.1 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { - self.emulatorCore.audioManager.enabled = true - } - } - } - - //MARK: - 3D Touch - - /// 3D Touch - override func previewActionItems() -> [UIPreviewActionItem] - { - if let previewActionItems = self.overridePreviewActionItems - { - return previewActionItems - } - - let presentingViewController = self.presentingViewController - - let launchGameAction = UIPreviewAction(title: NSLocalizedString("Launch \(self.game.name)", comment: ""), style: .default) { (action, viewController) in - // Delaying until next run loop prevents self from being dismissed immediately - DispatchQueue.main.async { - presentingViewController?.present(viewController, animated: true, completion: nil) - } - } - return [launchGameAction] - } -} - -//MARK: - Emulation - -/// Emulation -private extension EmulationViewController -{ - func pause(sender: AnyObject?) - { - self.performSegue(withIdentifier: "pauseSegue", sender: sender) - } - - @discardableResult func pauseEmulation() -> Bool - { - return self.emulatorCore.pause() - } - - @discardableResult func resumeEmulation() -> Bool - { - guard !self.choosingSustainedButtons && self.pauseViewController == nil else { return false } - - return self.emulatorCore.resume() - } - - func emulatorCoreDidUpdate(_ emulatorCore: EmulatorCore) - { - for semaphore in self.updateSemaphores - { - semaphore.semaphore.signal() - } - } -} - -//MARK: - Controllers - -/// Controllers -private extension EmulationViewController -{ - @objc func updateControllers() - { - self.emulatorCore.removeAllGameControllers() - - if let index = Settings.localControllerPlayerIndex - { - self.controllerView.playerIndex = index - } - - var controllers = [GameController]() - controllers.append(self.controllerView) - - // We need to map each item as a GameControllerProtocol due to a Swift bug - controllers.append(contentsOf: ExternalControllerManager.shared.connectedControllers.map { $0 as GameController }) - - for controller in controllers - { - if let index = controller.playerIndex - { - self.emulatorCore.setGameController(controller, at: index) - controller.addReceiver(self) - } - else - { - controller.removeReceiver(self) - } - } - - self.view.setNeedsLayout() - } -} - -//MARK: - Sustain Button - -private extension EmulationViewController -{ - func showSustainButtonView() - { - self.choosingSustainedButtons = true - self.sustainButtonContentView.isHidden = false - } - - func hideSustainButtonView() - { - self.choosingSustainedButtons = false - - UIView.animate(withDuration: 0.4, animations: { - self.sustainButtonContentView.alpha = 0.0 - }) { (finished) in - self.sustainButtonContentView.isHidden = true - self.sustainButtonContentView.alpha = 1.0 - } - } - - func resetSustainedInputs(forGameController gameController: GameController) - { - if let previousInputs = self.sustainedInputs[ObjectIdentifier(gameController)] - { - let receivers = gameController.receivers - receivers.forEach { gameController.removeReceiver($0) } - - // Activate previousInputs without notifying anyone so we can then deactivate them - // We do this because deactivating an already deactivated input has no effect - previousInputs.forEach { gameController.activate($0) } - - receivers.forEach { gameController.addReceiver($0) } - - // Deactivate previously sustained inputs - previousInputs.forEach { gameController.deactivate($0) } - } - - self.sustainedInputs[ObjectIdentifier(gameController)] = [] - } - - func addSustainedInput(_ input: Input, gameController: GameController) - { - var inputs = self.sustainedInputs[ObjectIdentifier(gameController)] ?? [] - - guard !inputs.contains({ $0.isEqual(input) }) else { return } - - inputs.append(input) - self.sustainedInputs[ObjectIdentifier(gameController)] = inputs - - let receivers = gameController.receivers - receivers.forEach { gameController.removeReceiver($0) } - - // Causes input to be considered deactivated, so gameController won't send a subsequent message to observers when user actually deactivates - // However, at this point the core still thinks it is activated, and is temporarily not a receiver, thus sustaining it - gameController.deactivate(input) - - receivers.forEach { gameController.addReceiver($0) } - } - - func reactivateSustainedInput(_ input: Input, gameController: GameController) - { - // These MUST be performed serially, or else Bad Things Happen™ if multiple inputs are reactivated at once - self.reactivateSustainInputsQueue.addOperation { - - // The manual activations/deactivations here are hidden implementation details, so we won't notify ourselves about them - gameController.removeReceiver(self) - - // Must deactivate first so core recognizes a secondary activation - gameController.deactivate(input) - - let dispatchQueue = DispatchQueue(label: "com.rileytestut.Delta.sustainButtonsQueue", attributes: DispatchQueueAttributes.serial) - dispatchQueue.async { - - let semaphore = DispatchSemaphore(value: 0) - self.updateSemaphores.insert(semaphore) - - // To ensure the emulator core recognizes us activating the input again, we need to wait at least two frames - // Unfortunately we cannot init DispatchSemaphore with value less than 0 - // To compensate, we simply wait twice; once the first wait returns, we wait again - semaphore.semaphore.wait() - semaphore.semaphore.wait() - - // These MUST be performed serially, or else Bad Things Happen™ if multiple inputs are reactivated at once - self.reactivateSustainInputsQueue.addOperation { - - self.updateSemaphores.remove(semaphore) - - // Ensure we still are not a receiver (to prevent rare race conditions) - gameController.removeReceiver(self) - - gameController.activate(input) - - let receivers = gameController.receivers - receivers.forEach { gameController.removeReceiver($0) } - - // Causes input to be considered deactivated, so gameController won't send a subsequent message to observers when user actually deactivates - // However, at this point the core still thinks it is activated, and is temporarily not a receiver, thus sustaining it - gameController.deactivate(input) - - receivers.forEach { gameController.addReceiver($0) } - } - - // More Bad Things Happen™ if we add self as observer before ALL reactivations have occurred (notable, infinite loops) - self.reactivateSustainInputsQueue.waitUntilAllOperationsAreFinished() - - gameController.addReceiver(self) - - } - } - } -} - -//MARK: - Save States -/// Save States -extension EmulationViewController: SaveStatesViewControllerDelegate -{ - func saveStatesViewControllerActiveEmulatorCore(_ saveStatesViewController: SaveStatesViewController) -> EmulatorCore - { - return self.emulatorCore - } - - func saveStatesViewController(_ saveStatesViewController: SaveStatesViewController, updateSaveState saveState: SaveState) - { - guard let filepath = saveState.fileURL.path else { return } - - var updatingExistingSaveState = true - - self.emulatorCore.save { (temporarySaveState) in - do - { - if FileManager.default.fileExists(atPath: filepath) - { - try FileManager.default.replaceItem(at: saveState.fileURL, withItemAt: temporarySaveState.fileURL, backupItemName: nil, options: [], resultingItemURL: nil) - } - else - { - try FileManager.default.moveItem(at: temporarySaveState.fileURL, to: saveState.fileURL) - - updatingExistingSaveState = false - } - } - catch let error as NSError - { - print(error) - } - } - - if let outputImage = self.gameView.outputImage, let quartzImage = self.context.createCGImage(outputImage, from: outputImage.extent) - { - let image = UIImage(cgImage: quartzImage) - try! UIImagePNGRepresentation(image)?.write(to: saveState.imageFileURL, options: [.atomicWrite]) - } - - saveState.modifiedDate = Date() - - // 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) - { - do - { - try self.emulatorCore.load(saveState) - } - catch EmulatorCore.SaveStateError.doesNotExist - { - print("Save State does not exist.") - } - catch let error as NSError - { - print(error) - } - - self.updateCheats() - - self.pauseViewController?.dismiss() - } -} - -//MARK: - Cheats -/// Cheats -extension EmulationViewController: CheatsViewControllerDelegate -{ - func cheatsViewControllerActiveEmulatorCore(_ saveStatesViewController: CheatsViewController) -> EmulatorCore - { - return self.emulatorCore - } - - func cheatsViewController(_ cheatsViewController: CheatsViewController, didActivateCheat cheat: Cheat) throws - { - try self.emulatorCore.activate(cheat) - } - - func cheatsViewController(_ cheatsViewController: CheatsViewController, didDeactivateCheat cheat: Cheat) - { - self.emulatorCore.deactivate(cheat) - } - - private func updateCheats() - { - let backgroundContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() - backgroundContext.performAndWait { - - let running = (self.emulatorCore.state == .running) - - if running - { - // Core MUST be paused when activating cheats, or else race conditions could crash the core - self.pauseEmulation() - } - - let predicate = Predicate(format: "%K == %@", Cheat.Attributes.game.rawValue, self.emulatorCore.game as! Game) - - let cheats = Cheat.instancesWithPredicate(predicate, inManagedObjectContext: backgroundContext, type: Cheat.self) - for cheat in cheats - { - if cheat.enabled - { - do - { - try self.emulatorCore.activate(cheat) - } - catch EmulatorCore.CheatError.invalid - { - print("Invalid cheat:", cheat.name, cheat.code) - } - catch let error as NSError - { - print("Unknown Cheat Error:", error, cheat.name, cheat.code) - } - - } - else - { - self.emulatorCore.deactivate(cheat) - } - } - - if running - { - self.resumeEmulation() - } - - } - - } -} - -//MARK: - App Lifecycle - -private extension EmulationViewController -{ - @objc func willResignActive(_ notification: Notification) - { - self.pauseEmulation() - } - - @objc func didBecomeActive(_ notification: Notification) - { - self.resumeEmulation() - } -} - -//MARK: - - -/// -extension EmulationViewController: GameControllerReceiver -{ - func gameController(_ gameController: GameController, didActivate input: Input) - { - if gameController is ControllerView && UIDevice.current().isVibrationSupported - { - UIDevice.current().vibrate() - } - - if let input = input as? ControllerInput - { - switch input - { - case ControllerInput.menu: - if self.choosingSustainedButtons { self.hideSustainButtonView() } - self.pause(sender: gameController) - - // Return now, because Menu cannot be sustained - return - } - } - - if self.choosingSustainedButtons - { - self.addSustainedInput(input, gameController: gameController) - return - } - - if let sustainedInputs = self.sustainedInputs[ObjectIdentifier(gameController)] where sustainedInputs.contains({ $0.isEqual(input) }) - { - // Perform on next run loop - DispatchQueue.main.async { - self.reactivateSustainedInput(input, gameController: gameController) - } - - return - } - } - - func gameController(_ gameController: GameController, didDeactivate input: Input) - { - guard let input = input as? ControllerInput else { return } - - print("Deactivated \(input)") - } -} diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift new file mode 100644 index 0000000..32b5ca4 --- /dev/null +++ b/Delta/Emulation/GameViewController.swift @@ -0,0 +1,153 @@ +// +// GameViewController.swift +// Delta +// +// Created by Riley Testut on 5/5/15. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import UIKit + +import DeltaCore + +class GameViewController: DeltaCore.GameViewController +{ + override var game: GameProtocol? { + didSet { + guard let emulatorCore = self.emulatorCore else { return } + self.preferredContentSize = emulatorCore.preferredRenderingSize + } + } + + // If non-nil, will override the default preview action items returned in previewActionItems() + var overridePreviewActionItems: [UIPreviewActionItem]? + + 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: .externalControllerDidConnect, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalControllerDidDisconnect, object: nil) + } + + // MARK: GameControllerReceiver - + override func gameController(_ gameController: GameController, didActivate input: Input) + { + super.gameController(gameController, didActivate: input) + + if gameController is ControllerView && UIDevice.current().isVibrationSupported + { + UIDevice.current().vibrate() + } + } +} + + +//MARK: UIViewController - +/// UIViewController +extension GameViewController +{ + override func viewDidLoad() + { + super.viewDidLoad() + + self.updateControllers() + } + + override func viewDidLayoutSubviews() + { + super.viewDidLayoutSubviews() + + self.controllerView.isHidden = self.isPreviewing + } + + override func previewActionItems() -> [UIPreviewActionItem] + { + if let previewActionItems = self.overridePreviewActionItems + { + return previewActionItems + } + + guard let game = self.game as? Game else { return [] } + + let presentingViewController = self.presentingViewController + + let launchGameAction = UIPreviewAction(title: NSLocalizedString("Launch \(game.name)", comment: ""), style: .default) { (action, viewController) in + // Delaying until next run loop prevents self from being dismissed immediately + DispatchQueue.main.async { + presentingViewController?.present(viewController, animated: true, completion: nil) + } + } + return [launchGameAction] + } +} + +//MARK: Controllers - +private extension GameViewController +{ + @objc func updateControllers() + { + self.emulatorCore?.removeAllGameControllers() + + if let index = Settings.localControllerPlayerIndex + { + self.controllerView.playerIndex = index + } + + var controllers = [GameController]() + controllers.append(self.controllerView) + + // We need to map each item as a GameControllerProtocol due to a Swift bug + controllers.append(contentsOf: ExternalControllerManager.shared.connectedControllers.map { $0 as GameController }) + + for controller in controllers + { + if let index = controller.playerIndex + { + // We need to place the underscore here to silence erroneous unused result warning despite annotating function with @discardableResult + // Hopefully this bug won't be around for too long... + _ = self.emulatorCore?.setGameController(controller, at: index) + controller.addReceiver(self) + } + else + { + controller.removeReceiver(self) + } + } + + self.view.setNeedsLayout() + } +} + +//MARK: GameViewControllerDelegate - +/// GameViewControllerDelegate +extension GameViewController: GameViewControllerDelegate +{ + func gameViewController(gameViewController: DeltaCore.GameViewController, handleMenuInputFrom gameController: GameController) + { + self.pauseEmulation() + + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { (action) in + self.resumeEmulation() + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Stop Emulation", comment: ""), style: .destructive, handler: { (action) in + self.dismiss(animated: true) + })) + self.present(alertController, animated: true) + } +} diff --git a/Delta/Game Selection/GamesViewController.swift b/Delta/Game Selection/GamesViewController.swift index 3ed0b86..f91a382 100644 --- a/Delta/Game Selection/GamesViewController.swift +++ b/Delta/Game Selection/GamesViewController.swift @@ -118,50 +118,19 @@ class GamesViewController: UIViewController override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) { guard let sourceViewController = segue.sourceViewController as? GamesCollectionViewController else { return } - guard let destinationViewController = segue.destinationViewController as? EmulationViewController else { return } + guard let destinationViewController = segue.destinationViewController as? GameViewController else { return } guard let cell = sender as? UICollectionViewCell else { return } let indexPath = sourceViewController.collectionView?.indexPath(for: cell) let game = sourceViewController.dataSource.fetchedResultsController.object(at: indexPath!) as! Game destinationViewController.game = game - - if segue.identifier == "peekEmulationViewController" - { - destinationViewController.deferredPreparationHandler = { [unowned destinationViewController] in - - if let saveState = game.previewSaveState - { - destinationViewController.emulatorCore.start() - destinationViewController.emulatorCore.pause() - - do - { - try destinationViewController.emulatorCore.load(saveState) - } - catch EmulatorCore.SaveStateError.doesNotExist - { - print("Save State \(saveState.name) does not exist.") - } - catch let error as NSError - { - print(error) - } - - } - } - } } @IBAction func unwindFromSettingsViewController(_ segue: UIStoryboardSegue) { } - - @IBAction func unwindFromEmulationViewController(_ segue: UIStoryboardSegue) - { - - } } private extension GamesViewController