diff --git a/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents b/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents index 8d95c92..2b5be3b 100644 --- a/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -7,7 +7,7 @@ - + diff --git a/Cores/DeltaCore b/Cores/DeltaCore index d819614..7149d73 160000 --- a/Cores/DeltaCore +++ b/Cores/DeltaCore @@ -1 +1 @@ -Subproject commit d819614e5fe422aa4975f70e3d4f1d2dc97b9c24 +Subproject commit 7149d73128c0f5c95b3e58990f9d185cf33277df diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index f936360..f749d2f 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; }; BF27CC971BCC890700A20D89 /* GamesCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */; }; BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */; }; + BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF31878A1D489AAA00BD020D /* CheatValidator.swift */; }; BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */; }; BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA101CF1899D006624C7 /* CheatTextView.swift */; }; BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; }; @@ -102,6 +103,7 @@ BF27CC941BCB7B7A00A20D89 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; }; BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesCollectionViewController.swift; sourceTree = ""; }; BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesCollectionHeaderView.swift; path = "Pause Menu/Save States/SaveStatesCollectionHeaderView.swift"; sourceTree = ""; }; + BF31878A1D489AAA00BD020D /* CheatValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatValidator.swift; path = "Pause Menu/Cheats/CheatValidator.swift"; sourceTree = ""; }; BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EditCheatViewController.swift; path = "Pause Menu/Cheats/EditCheatViewController.swift"; sourceTree = ""; }; BF34FA101CF1899D006624C7 /* CheatTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatTextView.swift; path = "Pause Menu/Cheats/CheatTextView.swift"; sourceTree = ""; }; BF353FF11C5D7FB000C1184C /* PauseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseViewController.swift; path = "Pause Menu/PauseViewController.swift"; sourceTree = ""; }; @@ -320,6 +322,7 @@ BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */, BF34FA061CF0F510006624C7 /* EditCheatViewController.swift */, BF34FA101CF1899D006624C7 /* CheatTextView.swift */, + BF31878A1D489AAA00BD020D /* CheatValidator.swift */, ); name = Cheats; sourceTree = ""; @@ -542,6 +545,7 @@ BF7AE8241C2E984300B1B5BC /* GridCollectionViewLayout.swift in Sources */, BF1FB1861C5EE643007E2494 /* SaveState.swift in Sources */, BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */, + BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */, BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */, BFA2315C1CED10BE0011E35A /* Action.swift in Sources */, BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */, diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index 7698503..a2d1422 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -10,13 +10,20 @@ import UIKit import DeltaCore +private var kvoContext = 0 + 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) + } didSet { guard let emulatorCore = self.emulatorCore else { return } self.preferredContentSize = emulatorCore.preferredRenderingSize + + emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext) } } @@ -51,6 +58,11 @@ class GameViewController: DeltaCore.GameViewController NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalControllerDidDisconnect, object: nil) } + deinit + { + self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext) + } + // MARK: GameControllerReceiver - override func gameController(_ gameController: GameController, didActivate input: Input) { @@ -116,6 +128,7 @@ extension GameViewController pauseViewController.pauseText = (self.game as? Game)?.name ?? NSLocalizedString("Delta", comment: "") pauseViewController.emulatorCore = self.emulatorCore pauseViewController.saveStatesViewControllerDelegate = self + pauseViewController.cheatsViewControllerDelegate = self self.pauseViewController = pauseViewController } @@ -135,6 +148,20 @@ extension GameViewController } } } + + // MARK: - KVO - + /// KVO + override func observeValue(forKeyPath keyPath: String?, of object: AnyObject?, change: [NSKeyValueChangeKey : AnyObject]?, context: UnsafeMutablePointer?) + { + 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 previousState == .stopped + { + self.updateCheats() + } + } } //MARK: Controllers - @@ -244,10 +271,81 @@ extension GameViewController: SaveStatesViewControllerDelegate print(error) } + self.updateCheats() + self.pauseViewController?.dismiss() } } +//MARK: - Cheats +/// Cheats +extension GameViewController: CheatsViewControllerDelegate +{ + func cheatsViewController(_ cheatsViewController: CheatsViewController, activateCheat cheat: Cheat) + { + self.activate(cheat) + } + + func cheatsViewController(_ cheatsViewController: CheatsViewController, deactivateCheat cheat: Cheat) + { + self.emulatorCore?.deactivate(cheat) + } + + private func activate(_ cheat: Cheat) + { + 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) + } + } + + private func updateCheats() + { + guard let game = self.game as? Game else { return } + + 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 backgroundContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() + backgroundContext.performAndWait { + + let predicate = Predicate(format: "%K == %@", Cheat.Attributes.game.rawValue, game) + + let cheats = Cheat.instancesWithPredicate(predicate, inManagedObjectContext: backgroundContext, type: Cheat.self) + for cheat in cheats + { + if cheat.enabled + { + self.activate(cheat) + } + else + { + self.emulatorCore?.deactivate(cheat) + } + } + } + + if running + { + self.resumeEmulation() + } + + } +} + //MARK: GameViewControllerDelegate - /// GameViewControllerDelegate extension GameViewController: GameViewControllerDelegate diff --git a/Delta/Pause Menu/Cheats/CheatValidator.swift b/Delta/Pause Menu/Cheats/CheatValidator.swift new file mode 100644 index 0000000..0c2593d --- /dev/null +++ b/Delta/Pause Menu/Cheats/CheatValidator.swift @@ -0,0 +1,59 @@ +// +// CheatValidator.swift +// Delta +// +// Created by Riley Testut on 7/27/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import Foundation + +import DeltaCore + +extension CheatValidator +{ + enum Error: ErrorProtocol + { + case invalidCode + case invalidName + case duplicateName + case duplicateCode + } +} + +struct CheatValidator +{ + let format: CheatFormat + let managedObjectContext: NSManagedObjectContext + + func validate(_ cheat: Cheat) throws + { + guard let name = cheat.name else { throw Error.invalidName } + + let code = cheat.code + + // Find all cheats that are for the same game, don't have the same identifier as the current cheat, but have either the same name or code + let predicate = Predicate(format: "%K == %@ AND %K != %@ AND (%K == %@ OR %K == %@)", Cheat.Attributes.game.rawValue, cheat.game, Cheat.Attributes.identifier.rawValue, cheat.identifier, Cheat.Attributes.code.rawValue, code, Cheat.Attributes.name.rawValue, name) + + let cheats = Cheat.instancesWithPredicate(predicate, inManagedObjectContext: self.managedObjectContext, type: Cheat.self) + for cheat in cheats + { + if cheat.name == name + { + throw Error.duplicateName + } + else if cheat.code == code + { + throw Error.duplicateCode + } + } + + // Remove newline characters (code should already be formatted) + let sanitizedCode = (cheat.code as NSString).replacingOccurrences(of: "\n", with: "") + + if sanitizedCode.characters.count % self.format.format.characters.count != 0 + { + throw Error.invalidCode + } + } +} diff --git a/Delta/Pause Menu/Cheats/CheatsViewController.swift b/Delta/Pause Menu/Cheats/CheatsViewController.swift index c0cc69c..1b6b03e 100644 --- a/Delta/Pause Menu/Cheats/CheatsViewController.swift +++ b/Delta/Pause Menu/Cheats/CheatsViewController.swift @@ -15,19 +15,20 @@ import Roxas protocol CheatsViewControllerDelegate: class { - func cheatsViewControllerActiveEmulatorCore(_ saveStatesViewController: CheatsViewController) -> EmulatorCore - func cheatsViewController(_ cheatsViewController: CheatsViewController, didActivateCheat cheat: Cheat) throws - func cheatsViewController(_ cheatsViewController: CheatsViewController, didDeactivateCheat cheat: Cheat) + func cheatsViewController(_ cheatsViewController: CheatsViewController, activateCheat cheat: Cheat) + func cheatsViewController(_ cheatsViewController: CheatsViewController, deactivateCheat cheat: Cheat) } class CheatsViewController: UITableViewController { - weak var delegate: CheatsViewControllerDelegate! { + var game: Game! { didSet { self.updateFetchedResultsController() } } + weak var delegate: CheatsViewControllerDelegate? + private var backgroundView: RSTBackgroundView! private var fetchedResultsController: NSFetchedResultsController! @@ -83,11 +84,9 @@ private extension CheatsViewController { func updateFetchedResultsController() { - let game = self.delegate.cheatsViewControllerActiveEmulatorCore(self).game as! Game - let fetchRequest = Cheat.rst_fetchRequest() fetchRequest.returnsObjectsAsFaults = false - fetchRequest.predicate = Predicate(format: "%K == %@", Cheat.Attributes.game.rawValue, game) + fetchRequest.predicate = Predicate(format: "%K == %@", Cheat.Attributes.game.rawValue, self.game) fetchRequest.sortDescriptors = [SortDescriptor(key: Cheat.Attributes.name.rawValue, ascending: true)] self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.sharedManager.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil) @@ -121,7 +120,7 @@ private extension CheatsViewController func deleteCheat(_ cheat: Cheat) { - self.delegate.cheatsViewController(self, didDeactivateCheat: cheat) + self.delegate?.cheatsViewController(self, deactivateCheat: cheat) let backgroundContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() backgroundContext.perform { @@ -145,12 +144,11 @@ private extension CheatsViewController } func makeEditCheatViewController(cheat: Cheat?) -> EditCheatViewController - { + { let editCheatViewController = self.storyboard!.instantiateViewController(withIdentifier: "editCheatViewController") as! EditCheatViewController editCheatViewController.delegate = self - editCheatViewController.supportedCheatFormats = self.delegate.cheatsViewControllerActiveEmulatorCore(self).configuration.supportedCheatFormats editCheatViewController.cheat = cheat - editCheatViewController.game = self.delegate.cheatsViewControllerActiveEmulatorCore(self).game as! Game + editCheatViewController.game = self.game return editCheatViewController } @@ -193,25 +191,13 @@ extension CheatsViewController if temporaryCheat.enabled { - do - { - try self.delegate.cheatsViewController(self, didActivateCheat: temporaryCheat) - } - 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) - } + self.delegate?.cheatsViewController(self, activateCheat: temporaryCheat) } else { - self.delegate.cheatsViewController(self, didDeactivateCheat: temporaryCheat) + self.delegate?.cheatsViewController(self, deactivateCheat: temporaryCheat) } - backgroundContext.saveWithErrorLogging() } @@ -266,9 +252,9 @@ extension CheatsViewController: UIViewControllerPreviewingDelegate //MARK: - - extension CheatsViewController: EditCheatViewControllerDelegate { - func editCheatViewController(_ editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?) throws + func editCheatViewController(_ editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?) { - try self.delegate.cheatsViewController(self, didActivateCheat: cheat) + self.delegate?.cheatsViewController(self, activateCheat: cheat) if let previousCheat = previousCheat { @@ -278,14 +264,14 @@ extension CheatsViewController: EditCheatViewControllerDelegate guard previousCheat.code != code else { return } - self.delegate.cheatsViewController(self, didDeactivateCheat: previousCheat) + self.delegate?.cheatsViewController(self, deactivateCheat: previousCheat) }) } } func editCheatViewController(_ editCheatViewController: EditCheatViewController, deactivateCheat cheat: Cheat) { - self.delegate.cheatsViewController(self, didDeactivateCheat: cheat) + self.delegate?.cheatsViewController(self, deactivateCheat: cheat) } } diff --git a/Delta/Pause Menu/Cheats/EditCheatViewController.swift b/Delta/Pause Menu/Cheats/EditCheatViewController.swift index 55ac730..0a9d9f5 100644 --- a/Delta/Pause Menu/Cheats/EditCheatViewController.swift +++ b/Delta/Pause Menu/Cheats/EditCheatViewController.swift @@ -14,19 +14,12 @@ import Roxas protocol EditCheatViewControllerDelegate: class { - func editCheatViewController(_ editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?) throws + func editCheatViewController(_ editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?) func editCheatViewController(_ editCheatViewController: EditCheatViewController, deactivateCheat cheat: Cheat) } private extension EditCheatViewController { - enum ValidationError: ErrorProtocol - { - case invalidCode - case duplicateName - case duplicateCode - } - enum Section: Int { case name @@ -37,11 +30,18 @@ private extension EditCheatViewController class EditCheatViewController: UITableViewController { - weak var delegate: EditCheatViewControllerDelegate? + var game: Game! { + didSet { + let deltaCore = Delta.core(for: self.game.type)! + self.supportedCheatFormats = deltaCore.emulatorConfiguration.supportedCheatFormats + } + } var cheat: Cheat? - var game: Game! - var supportedCheatFormats: [CheatFormat]! + + weak var delegate: EditCheatViewControllerDelegate? + + private var supportedCheatFormats: [CheatFormat]! private var selectedCheatFormat: CheatFormat { let cheatFormat = self.supportedCheatFormats[self.typeSegmentedControl.selectedSegmentIndex] @@ -271,63 +271,51 @@ private extension EditCheatViewController do { try self.validateCheat(self.mutableCheat) + + self.delegate?.editCheatViewController(self, activateCheat: self.mutableCheat, previousCheat: self.cheat) + self.mutableCheat.managedObjectContext?.saveWithErrorLogging() self.performSegue(withIdentifier: "unwindEditCheatSegue", sender: sender) } - catch ValidationError.invalidCode + catch CheatValidator.Error.invalidCode { self.presentErrorAlert(title: NSLocalizedString("Invalid Code", comment: ""), message: NSLocalizedString("Please make sure you typed the cheat code in correctly and try again.", comment: "")) { self.codeTextView.becomeFirstResponder() } } - catch ValidationError.duplicateCode + catch CheatValidator.Error.invalidName + { + self.presentErrorAlert(title: NSLocalizedString("Invalid Name", comment: ""), message: NSLocalizedString("Please rename this cheat and try again.", comment: "")) { + self.codeTextView.becomeFirstResponder() + } + } + catch CheatValidator.Error.duplicateCode { self.presentErrorAlert(title: NSLocalizedString("Duplicate Code", comment: ""), message: NSLocalizedString("A cheat already exists with this code. Please type in a different code and try again.", comment: "")) { self.codeTextView.becomeFirstResponder() } } - catch ValidationError.duplicateName + catch CheatValidator.Error.duplicateName { self.presentErrorAlert(title: NSLocalizedString("Duplicate Name", comment: ""), message: NSLocalizedString("A cheat already exists with this name. Please rename this cheat and try again.", comment: "")) { self.nameTextField.becomeFirstResponder() } } - catch let error as NSError + catch { print(error) + + self.presentErrorAlert(title: NSLocalizedString("Unknown Error", comment: ""), message: NSLocalizedString("An error occured. Please make sure you typed the cheat code in correctly and try again.", comment: "")) { + self.codeTextView.becomeFirstResponder() + } } } } func validateCheat(_ cheat: Cheat) throws { - let name = cheat.name! - let code = cheat.code - - // Find all cheats that are for the same game, don't have the same identifier as the current cheat, but have either the same name or code - let predicate = Predicate(format: "%K == %@ AND %K != %@ AND (%K == %@ OR %K == %@)", Cheat.Attributes.game.rawValue, cheat.game, Cheat.Attributes.identifier.rawValue, cheat.identifier, Cheat.Attributes.code.rawValue, code, Cheat.Attributes.name.rawValue, name) - - let cheats = Cheat.instancesWithPredicate(predicate, inManagedObjectContext: self.managedObjectContext, type: Cheat.self) - for cheat in cheats - { - if cheat.name == name - { - throw ValidationError.duplicateName - } - else if cheat.code == code - { - throw ValidationError.duplicateCode - } - } - - do - { - try self.delegate?.editCheatViewController(self, activateCheat: cheat, previousCheat: self.cheat) - } - catch - { - throw ValidationError.invalidCode - } + let validator = CheatValidator(format: self.selectedCheatFormat, managedObjectContext: self.managedObjectContext) + try validator.validate(cheat) } @IBAction func textFieldDidEndEditing(_ sender: UITextField) diff --git a/Delta/Pause Menu/PauseViewController.swift b/Delta/Pause Menu/PauseViewController.swift index 53602da..c35ea24 100644 --- a/Delta/Pause Menu/PauseViewController.swift +++ b/Delta/Pause Menu/PauseViewController.swift @@ -32,10 +32,12 @@ class PauseViewController: UIViewController, PauseInfoProviding /// PauseInfoProviding var pauseText: String? + /// Cheats + weak var cheatsViewControllerDelegate: CheatsViewControllerDelegate? + /// Save States weak var saveStatesViewControllerDelegate: SaveStatesViewControllerDelegate? - // Hopefully this can be removed once SE-0116 is implemented private var saveStatesViewControllerMode = SaveStatesViewController.Mode.loading private var pauseNavigationController: UINavigationController! @@ -104,7 +106,12 @@ extension PauseViewController saveStatesViewController.game = self.emulatorCore?.game as? Game saveStatesViewController.emulatorCore = self.emulatorCore saveStatesViewController.mode = self.saveStatesViewControllerMode - + + case "cheats": + let cheatsViewController = segue.destinationViewController as! CheatsViewController + cheatsViewController.delegate = self.cheatsViewControllerDelegate + cheatsViewController.game = self.emulatorCore?.game as? Game + default: break } } @@ -150,7 +157,10 @@ private extension PauseViewController self.performSegue(withIdentifier: "saveStates", sender: self) }) - self.cheatCodesItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Cheat Codes", comment: ""), action: { _ in }) + self.cheatCodesItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Cheat Codes", comment: ""), action: { [unowned self] _ in + self.performSegue(withIdentifier: "cheats", sender: self) + }) + self.sustainButtonsItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Sustain Buttons", comment: ""), action: { _ in }) self.fastForwardItem = PauseItem(image: UIImage(named: "FastForward")!, text: NSLocalizedString("Fast Forward", comment: ""), action: { _ in }) }