diff --git a/Carthage/Checkouts/SteamController b/Carthage/Checkouts/SteamController index b26dda8147..80c984953e 160000 --- a/Carthage/Checkouts/SteamController +++ b/Carthage/Checkouts/SteamController @@ -1 +1 @@ -Subproject commit b26dda814768b4a60396d6a1028dbc304f5448fb +Subproject commit 80c984953eb68e3d340ff7ba36a9042470ae57bf diff --git a/PVLibrary/PVLibrary/Domain/SaveState.swift b/PVLibrary/PVLibrary/Domain/SaveState.swift index d26d3c00a9..15d56aa750 100644 --- a/PVLibrary/PVLibrary/Domain/SaveState.swift +++ b/PVLibrary/PVLibrary/Domain/SaveState.swift @@ -16,7 +16,7 @@ public protocol SaveStateInfoProvider { var date: Date { get } var lastOpened: Date? { get } var image: LocalFile? { get } - var isAutosave: Bool { get } + var saveType: SaveType { get } } public struct SaveState: SaveStateInfoProvider, Codable { @@ -27,5 +27,5 @@ public struct SaveState: SaveStateInfoProvider, Codable { public let date: Date public let lastOpened: Date? public let image: LocalFile? - public let isAutosave: Bool + public let saveType: SaveType } diff --git a/PVLibrary/PVLibrary/RealmPlatform/Entities/PVGame.swift b/PVLibrary/PVLibrary/RealmPlatform/Entities/PVGame.swift index 4dd8d232df..5184ba741f 100644 --- a/PVLibrary/PVLibrary/RealmPlatform/Entities/PVGame.swift +++ b/PVLibrary/PVLibrary/RealmPlatform/Entities/PVGame.swift @@ -127,12 +127,20 @@ extension PVGame: Filed, LocalFileProvider {} public extension PVGame { var autoSaves: Results { - return saveStates.filter("isAutosave == true").sorted(byKeyPath: "date", ascending: false) + return saveStates.filter("saveTypeRawValue == '\(SaveType.auto.rawValue)'").sorted(byKeyPath: "date", ascending: false) + } + + public var quickSaves : Results { + return saveStates.filter("saveTypeRawValue == '\(SaveType.quick.rawValue)'").sorted(byKeyPath: "date", ascending: false) } var newestAutoSave: PVSaveState? { return autoSaves.first } + + public var newestQuickSave : PVSaveState? { + return quickSaves.first + } var lastAutosaveAge: TimeInterval? { guard let first = autoSaves.first else { diff --git a/PVLibrary/PVLibrary/RealmPlatform/Entities/PVSaveState.swift b/PVLibrary/PVLibrary/RealmPlatform/Entities/PVSaveState.swift index 0b561314a3..0feb7f588b 100644 --- a/PVLibrary/PVLibrary/RealmPlatform/Entities/PVSaveState.swift +++ b/PVLibrary/PVLibrary/RealmPlatform/Entities/PVSaveState.swift @@ -10,6 +10,12 @@ import Foundation import PVSupport import RealmSwift +public enum SaveType: String, Codable { + case manual + case auto + case quick +} + public protocol Filed { associatedtype LocalFileProviderType: LocalFileProvider var file: LocalFileProviderType! { get } @@ -29,16 +35,22 @@ public final class PVSaveState: Object, Filed, LocalFileProvider { public dynamic var date: Date = Date() public dynamic var lastOpened: Date? public dynamic var image: PVImageFile? - public dynamic var isAutosave: Bool = false + + // Realm won't store enums, so we store the raw value but allow consumers to interact with the enum + @objc dynamic private var saveTypeRawValue: String = SaveType.manual.rawValue + public dynamic var saveType: SaveType { + get { return SaveType(rawValue: saveTypeRawValue)! } + set { saveTypeRawValue = newValue.rawValue } + } public dynamic var createdWithCoreVersion: String! - public convenience init(withGame game: PVGame, core: PVCore, file: PVFile, image: PVImageFile? = nil, isAutosave: Bool = false) { + public convenience init(withGame game: PVGame, core: PVCore, file: PVFile, type: SaveType = .manual, image: PVImageFile? = nil) { self.init() self.game = game self.file = file self.image = image - self.isAutosave = isAutosave + self.saveType = type self.core = core createdWithCoreVersion = core.projectVersion } @@ -47,12 +59,18 @@ public final class PVSaveState: Object, Filed, LocalFileProvider { do { // Temp store these URLs let fileURL = state.file.url + let jsonFileURL = fileURL.appendingPathExtension("json") let imageURl = state.image?.url let database = RomDatabase.sharedInstance try database.delete(state) try FileManager.default.removeItem(at: fileURL) + + if (FileManager.default.fileExists(atPath: jsonFileURL.absoluteString)) { + try FileManager.default.removeItem(at: jsonFileURL) + } + if let imageURl = imageURl { try FileManager.default.removeItem(at: imageURl) } @@ -63,13 +81,23 @@ public final class PVSaveState: Object, Filed, LocalFileProvider { } public dynamic var isNewestAutosave: Bool { - guard isAutosave, let game = game, let newestSave = game.autoSaves.first else { + guard saveType == .auto, let game = game, let newestSave = game.autoSaves.first else { return false } let isNewest = newestSave == self return isNewest } + + @objc dynamic public var isNewestQuicksave: Bool { + guard saveType == .quick, let game = game, let newestSave = game.quickSaves.first else { + return false + } + + let isNewest = newestSave == self + return isNewest + } + public static func == (lhs: PVSaveState, rhs: PVSaveState) -> Bool { return lhs.file.url == rhs.file.url @@ -96,7 +124,8 @@ private extension SaveState { } else { image = nil } - isAutosave = saveState.isAutosave + + saveType = saveState.saveType } } @@ -135,7 +164,7 @@ extension SaveState: RealmRepresentable { DLOG("path: \(imagePath)") object.image = PVImageFile(withURL: imagePath) } - object.isAutosave = isAutosave + object.saveType = saveType } } } diff --git a/PVLibrary/PVLibrary/RealmPlatform/RomDatabase.swift b/PVLibrary/PVLibrary/RealmPlatform/RomDatabase.swift index 396afbe69a..6f6f030795 100644 --- a/PVLibrary/PVLibrary/RealmPlatform/RomDatabase.swift +++ b/PVLibrary/PVLibrary/RealmPlatform/RomDatabase.swift @@ -11,7 +11,7 @@ import PVSupport import RealmSwift import UIKit -let schemaVersion: UInt64 = 8 +let schemaVersion: UInt64 = 9 public extension Notification.Name { static let DatabaseMigrationStarted = Notification.Name("DatabaseMigrarionStarted") @@ -73,9 +73,12 @@ public final class RealmConfiguration { } let migrationBlock: MigrationBlock = { migration, oldSchemaVersion in + if oldSchemaVersion < schemaVersion { + NotificationCenter.default.post(name: NSNotification.Name.DatabaseMigrationStarted, object: nil) + } + if oldSchemaVersion < 2 { ILOG("Migrating to version 2. Adding MD5s") - NotificationCenter.default.post(name: NSNotification.Name.DatabaseMigrationStarted, object: nil) var counter = 0 var deletions = 0 @@ -114,9 +117,25 @@ public final class RealmConfiguration { newObject!["importDate"] = Date() } - NotificationCenter.default.post(name: NSNotification.Name.DatabaseMigrationFinished, object: nil) ILOG("Migration complete of \(counter) roms. Removed \(deletions) bad entries.") } + + if oldSchemaVersion < 9 { + ILOG("Migrating to version 9. Adding support for quicksaves.") + + var counter = 0 + migration.enumerateObjects(ofType: PVSaveState.className()) { oldObject, newObject in + counter += 1 + let isAutosave = oldObject!["isAutosave"] as! Bool + newObject!["saveTypeRawValue"] = isAutosave ? SaveType.auto.rawValue : SaveType.manual.rawValue + } + + ILOG("Migration complete of \(counter) save states.") + } + + if oldSchemaVersion < schemaVersion { + NotificationCenter.default.post(name: NSNotification.Name.DatabaseMigrationFinished, object: nil) + } } #if DEBUG diff --git a/PVSupport/Settings/PVSettingsModel.swift b/PVSupport/Settings/PVSettingsModel.swift index c097b4660c..144e7eb547 100644 --- a/PVSupport/Settings/PVSettingsModel.swift +++ b/PVSupport/Settings/PVSettingsModel.swift @@ -242,6 +242,7 @@ extension MirroredSettings { public dynamic var askToAutoLoad = true public dynamic var autoLoadSaves = false + public dynamic var showQuicksaveButton = false public dynamic var disableAutoLock = false public dynamic var buttonVibration = true diff --git a/Provenance/Emulator/PVEmulatorViewController+Saves.swift b/Provenance/Emulator/PVEmulatorViewController+Saves.swift index 987abd7351..036d36c661 100644 --- a/Provenance/Emulator/PVEmulatorViewController+Saves.swift +++ b/Provenance/Emulator/PVEmulatorViewController+Saves.swift @@ -54,7 +54,7 @@ extension PVEmulatorViewController { autosaveTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { _ in DispatchQueue.main.async { let image = self.captureScreenshot() - self.createNewSaveState(auto: true, screenshot: image) { result in + self.createNewSaveState(type: .auto, screenshot: image) { result in switch result { case .success: break case let .error(error): @@ -91,11 +91,11 @@ extension PVEmulatorViewController { } let image = captureScreenshot() - createNewSaveState(auto: true, screenshot: image, completion: completion) + createNewSaveState(type: .auto, screenshot: image, completion: completion) } // #error ("Use to https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/iCloud/iCloud.html to save files to iCloud from local url, and setup packages for bundles") - func createNewSaveState(auto: Bool, screenshot: UIImage?, completion: @escaping SaveCompletion) { + func createNewSaveState(type: SaveType, screenshot: UIImage?, completion: @escaping SaveCompletion) { guard core.supportsSaveStates else { WLOG("Core \(core.description) doesn't support save states.") completion(.error(.saveStatesUnsupportedByCore)) @@ -131,7 +131,7 @@ extension PVEmulatorViewController { return } - DLOG("Succeeded saving state, auto: \(auto)") + DLOG("Succeeded saving state, type: \(type)") let realm = try! Realm() guard let core = realm.object(ofType: PVCore.self, forPrimaryKey: self.core.coreIdentifier) else { completion(.error(.noCoreFound(self.core.coreIdentifier))) @@ -142,7 +142,7 @@ extension PVEmulatorViewController { var saveState: PVSaveState! try realm.write { - saveState = PVSaveState(withGame: self.game, core: core, file: saveFile, image: imageFile, isAutosave: auto) + saveState = PVSaveState(withGame: self.game, core: core, file: saveFile, type: type, image: imageFile) realm.add(saveState) } @@ -160,13 +160,22 @@ extension PVEmulatorViewController { } do { - // Delete the oldest auto-saves over 5 count - try realm.write { + if (type == .auto) { + // Delete the oldest auto-saves over 5 count let autoSaves = self.game.autoSaves if autoSaves.count > 5 { - autoSaves.suffix(from: 5).forEach { + try autoSaves.suffix(from: 5).reversed().forEach { DLOG("Deleting old auto save of \($0.game.title) dated: \($0.date.description)") - realm.delete($0) + try PVSaveState.delete($0) + } + } + } else if (type == .quick) { + // Delete the oldest quicksaves over 5 count + let quickSaves = self.game.quickSaves + if quickSaves.count > 5 { + try quickSaves.suffix(from: 5).reversed().forEach { + DLOG("Deleting old quicksave of \($0.game.title) dated: \($0.date.description)") + try PVSaveState.delete($0) } } } @@ -198,6 +207,8 @@ extension PVEmulatorViewController { state.lastOpened = Date() } + self.core.setPauseEmulation(true) + self.core.loadStateFromFile(atPath: state.file.url.path) { success, error in let completion = { self.core.setPauseEmulation(false) @@ -239,11 +250,11 @@ extension PVEmulatorViewController { } func saveStatesViewControllerCreateNewState(_ saveStatesViewController: PVSaveStatesViewController, completion: @escaping SaveCompletion) { - createNewSaveState(auto: false, screenshot: saveStatesViewController.screenshot, completion: completion) + createNewSaveState(type: .manual, screenshot: saveStatesViewController.screenshot, completion: completion) } func saveStatesViewControllerOverwriteState(_ saveStatesViewController: PVSaveStatesViewController, state: PVSaveState, completion: @escaping SaveCompletion) { - createNewSaveState(auto: false, screenshot: saveStatesViewController.screenshot) { result in + createNewSaveState(type: .manual, screenshot: saveStatesViewController.screenshot) { result in switch result { case .success: do { @@ -321,7 +332,7 @@ extension PVEmulatorViewController { let newURL = saveStatePath.appendingPathComponent("\(game.md5Hash).\(Date().timeIntervalSinceReferenceDate)") try fileManager.moveItem(at: autoSaveURL, to: newURL) let saveFile = PVFile(withURL: newURL) - let newState = PVSaveState(withGame: game, core: core, file: saveFile, image: nil, isAutosave: true) + let newState = PVSaveState(withGame: game, core: core, file: saveFile, type: .auto, image: nil) try realm.write { realm.add(newState) } @@ -341,12 +352,12 @@ extension PVEmulatorViewController { let newURL = saveStatePath.appendingPathComponent("\(game.md5Hash).\(Date().timeIntervalSinceReferenceDate)") try fileManager.moveItem(at: url, to: newURL) let saveFile = PVFile(withURL: newURL) - let newState = PVSaveState(withGame: game, core: core, file: saveFile, image: nil, isAutosave: false) + let newState = PVSaveState(withGame: game, core: core, file: saveFile, type: .manual, image: nil) try realm.write { realm.add(newState) } } catch { - presentError("Unable to convert autosave to new format: \(error.localizedDescription)") + presentError("Unable to convert manual save state to new format: \(error.localizedDescription)") } } } diff --git a/Provenance/Emulator/PVEmulatorViewController.swift b/Provenance/Emulator/PVEmulatorViewController.swift index 9a5e1e8c51..f8a3d6ba0a 100644 --- a/Provenance/Emulator/PVEmulatorViewController.swift +++ b/Provenance/Emulator/PVEmulatorViewController.swift @@ -76,6 +76,7 @@ final class PVEmulatorViewController: PVEmulatorViewControllerRootClass, PVAudio var batterySavesPath: URL { return PVEmulatorConfiguration.batterySavesPath(forGame: game) } var BIOSPath: URL { return PVEmulatorConfiguration.biosPath(forGame: game) } var menuButton: MenuButton? + var quicksaveButton: MenuButton? private(set) lazy var glViewController: PVGLViewController = PVGLViewController(emulatorCore: core) private(set) lazy var controllerViewController: (UIViewController & StartSelectDelegate)? = PVCoreFactory.controllerViewController(forSystem: game.system, core: core) @@ -173,60 +174,84 @@ final class PVEmulatorViewController: PVEmulatorViewControllerRootClass, PVAudio } } - private func initNotifcationObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.appWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.appDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.appWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.appDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.controllerDidConnect(_:)), name: .GCControllerDidConnect, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.controllerDidDisconnect(_:)), name: .GCControllerDidDisconnect, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.screenDidConnect(_:)), name: UIScreen.didConnectNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.screenDidDisconnect(_:)), name: UIScreen.didDisconnectNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.handleControllerManagerControllerReassigned(_:)), name: .PVControllerManagerControllerReassigned, object: nil) - } - - private func initCore() { - core.audioDelegate = self - core.saveStatesPath = saveStatePath.path - core.batterySavesPath = batterySavesPath.path - core.biosPath = BIOSPath.path - core.controller1 = PVControllerManager.shared.player1 - core.controller2 = PVControllerManager.shared.player2 - core.controller3 = PVControllerManager.shared.player3 - core.controller4 = PVControllerManager.shared.player4 - - let md5Hash: String = game.md5Hash - core.romMD5 = md5Hash - core.romSerial = game.romSerial + private func initNotifcationObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.appWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.appDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.appWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.appDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.controllerDidConnect(_:)), name: .GCControllerDidConnect, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.controllerDidDisconnect(_:)), name: .GCControllerDidDisconnect, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.screenDidConnect(_:)), name: UIScreen.didConnectNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.screenDidDisconnect(_:)), name: UIScreen.didDisconnectNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(PVEmulatorViewController.handleControllerManagerControllerReassigned(_:)), name: .PVControllerManagerControllerReassigned, object: nil) + } + + private func initCore() { + core.audioDelegate = self + core.saveStatesPath = self.saveStatePath.path + core.batterySavesPath = batterySavesPath.path + core.biosPath = BIOSPath.path + core.controller1 = PVControllerManager.shared.player1 + core.controller2 = PVControllerManager.shared.player2 + core.controller3 = PVControllerManager.shared.player3 + core.controller4 = PVControllerManager.shared.player4 + + let md5Hash: String = game.md5Hash + core.romMD5 = md5Hash + core.romSerial = game.romSerial + } + + private func initMenuButton() { + // controllerViewController = PVCoreFactory.controllerViewController(forSystem: game.system, core: core) + if let aController = controllerViewController { + addChild(aController) + } + if let aView = controllerViewController?.view { + view.addSubview(aView) + } + controllerViewController?.didMove(toParent: self) + + let alpha: CGFloat = PVSettingsModel.shared.controllerOpacity + menuButton = MenuButton(type: .custom) + menuButton?.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin] + menuButton?.setImage(UIImage(named: "button-menu"), for: .normal) + menuButton?.setImage(UIImage(named: "button-menu-pressed"), for: .highlighted) + // Commenting out title label for now (menu has changed to graphic only) + //[self.menuButton setTitle:@"Menu" forState:UIControlStateNormal]; + //menuButton?.titleLabel?.font = UIFont.systemFont(ofSize: 12) + //menuButton?.setTitleColor(UIColor.white, for: .normal) + menuButton?.layer.shadowOffset = CGSize(width: 0, height: 1) + menuButton?.layer.shadowRadius = 3.0 + menuButton?.layer.shadowColor = UIColor.black.cgColor + menuButton?.layer.shadowOpacity = 0.75 + menuButton?.tintColor = UIColor.white + menuButton?.alpha = alpha + menuButton?.addTarget(self, action: #selector(PVEmulatorViewController.showMenu(_:)), for: .touchUpInside) + view.addSubview(menuButton!) + } + + private func initQuicksaveButton() { + let alpha: CGFloat = PVSettingsModel.shared.controllerOpacity + quicksaveButton = MenuButton(type: .custom) + quicksaveButton?.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin] + quicksaveButton?.setImage(UIImage(named: "button-save"), for: .normal) + quicksaveButton?.setImage(UIImage(named: "button-save-pressed"), for: .highlighted) + quicksaveButton?.layer.shadowOffset = CGSize(width: 0, height: 1) + quicksaveButton?.layer.shadowRadius = 3.0 + quicksaveButton?.layer.shadowColor = UIColor.black.cgColor + quicksaveButton?.layer.shadowOpacity = 0.75 + quicksaveButton?.tintColor = UIColor.white + quicksaveButton?.alpha = alpha + quicksaveButton?.addTarget(self, action: #selector(PVEmulatorViewController.quicksave(_:)), for: .touchUpInside) + let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(PVEmulatorViewController.quicksaveButtonLongPress)) + quicksaveButton?.addGestureRecognizer(longPressRecognizer) + view.addSubview(quicksaveButton!) } - private func initMenuButton() { - // controllerViewController = PVCoreFactory.controllerViewController(forSystem: game.system, core: core) - if let aController = controllerViewController { - addChild(aController) - } - if let aView = controllerViewController?.view { - view.addSubview(aView) + @objc private func quicksaveButtonLongPress(gesture: UILongPressGestureRecognizer) { + if gesture.state == UIGestureRecognizer.State.began { + self.quickload(gesture) } - controllerViewController?.didMove(toParent: self) - - let alpha: CGFloat = PVSettingsModel.shared.controllerOpacity - menuButton = MenuButton(type: .custom) - menuButton?.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin] - menuButton?.setImage(UIImage(named: "button-menu"), for: .normal) - menuButton?.setImage(UIImage(named: "button-menu-pressed"), for: .highlighted) - // Commenting out title label for now (menu has changed to graphic only) - // [self.menuButton setTitle:@"Menu" forState:UIControlStateNormal]; - // menuButton?.titleLabel?.font = UIFont.systemFont(ofSize: 12) - // menuButton?.setTitleColor(UIColor.white, for: .normal) - menuButton?.layer.shadowOffset = CGSize(width: 0, height: 1) - menuButton?.layer.shadowRadius = 3.0 - menuButton?.layer.shadowColor = UIColor.black.cgColor - menuButton?.layer.shadowOpacity = 0.75 - menuButton?.tintColor = UIColor.white - menuButton?.alpha = alpha - menuButton?.addTarget(self, action: #selector(PVEmulatorViewController.showMenu(_:)), for: .touchUpInside) - view.addSubview(menuButton!) } private func initFPSLabel() { @@ -331,9 +356,10 @@ final class PVEmulatorViewController: PVEmulatorViewControllerRootClass, PVAudio } glViewController.didMove(toParent: self) } - #if os(iOS) + #if os(iOS) initMenuButton() - #endif + initQuicksaveButton() + #endif if PVSettingsModel.shared.showFPSCount { initFPSLabel() @@ -342,6 +368,7 @@ final class PVEmulatorViewController: PVEmulatorViewControllerRootClass, PVAudio #if !targetEnvironment(simulator) if !GCController.controllers().isEmpty { menuButton?.isHidden = true + quicksaveButton?.isHidden = true } #endif @@ -463,11 +490,14 @@ final class PVEmulatorViewController: PVEmulatorViewControllerRootClass, PVAudio override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - #if os(iOS) - layoutMenuButton() - #endif + #if os(iOS) + layoutMenuButton() + + if PVSettingsModel.shared.showQuicksaveButton { + layoutQuicksaveButton() + } + #endif } - #if os(iOS) func layoutMenuButton() { if let menuButton = self.menuButton { @@ -478,6 +508,15 @@ final class PVEmulatorViewController: PVEmulatorViewControllerRootClass, PVAudio menuButton.frame = frame } } + func layoutQuicksaveButton() { + if let quicksaveButton = self.quicksaveButton { + let height: CGFloat = 42 + let width: CGFloat = 42 + quicksaveButton.imageView?.contentMode = .center + let frame = CGRect(x: self.view.frame.size.width - safeAreaInsets.right - width, y: safeAreaInsets.top + 5, width: width, height: height) + quicksaveButton.frame = frame + } + } #endif func documentsPath() -> String? { #if os(tvOS) diff --git a/Provenance/Emulator/PVEmulatorViewController~iOS.swift b/Provenance/Emulator/PVEmulatorViewController~iOS.swift index cc3873f718..cfb52a3ec2 100644 --- a/Provenance/Emulator/PVEmulatorViewController~iOS.swift +++ b/Provenance/Emulator/PVEmulatorViewController~iOS.swift @@ -388,6 +388,10 @@ extension PVEmulatorViewController { self.perform(#selector(self.showSpeedMenu), with: nil, afterDelay: 0.1) })) if core.supportsSaveStates { + actionSheet.addAction(Action( "Quicksave", style: .default, handler: { action in + self.perform(#selector(self.quicksave), with: nil, afterDelay: 0.1) + self.isShowingMenu = false + })) actionSheet.addAction(Action("Save States", style: .default, handler: { _ in self.perform(#selector(self.showSaveStateMenu), with: nil, afterDelay: 0.1) })) @@ -434,14 +438,37 @@ extension PVEmulatorViewController { self.isShowingMenu = false self.enableContorllerInput(false) } + } + + present(actionSheet, animated: true, completion: {() -> Void in + PVControllerManager.shared.iCadeController?.refreshListener() + }) + } + + @objc func quicksave(_ sender: Any?) { + self.core.setPauseEmulation(true) + + let image = self.captureScreenshot() + self.createNewSaveState(type: .quick, screenshot: image) { result in + switch result { + case .success: break + case .error(let error): + ELOG("Quicksave failed to make save state: \(error.localizedDescription)") + } + + self.core.setPauseEmulation(false) } - - present(actionSheet, animated: true, completion: { () -> Void in - PVControllerManager.shared.iCadeController?.refreshListener() - }) } - - // override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - // super.dismiss(animated: flag, completion: completion) - // } + + @objc func quickload(_ sender: Any?) { + guard let saveState = game.newestQuickSave else { + return + } + + loadSaveState(saveState) + } + + // override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + // super.dismiss(animated: flag, completion: completion) + // } } diff --git a/Provenance/Game Library/UI/CollectionViewInACell/RealmCollectinViewCell.swift b/Provenance/Game Library/UI/CollectionViewInACell/RealmCollectinViewCell.swift index 7f7769e31f..6f28226845 100644 --- a/Provenance/Game Library/UI/CollectionViewInACell/RealmCollectinViewCell.swift +++ b/Provenance/Game Library/UI/CollectionViewInACell/RealmCollectinViewCell.swift @@ -492,16 +492,16 @@ class SaveStatesCollectionCell: RealmCollectinViewCell = SelectionObject.all.filter("game != nil").sorted(by: sortDescriptors) @@ -510,7 +510,7 @@ class SaveStatesCollectionCell: RealmCollectinViewCell Bool { - return !object.isAutosave || object.isNewestAutosave + return object.saveType == .manual || object.isNewestAutosave || object.isNewestQuicksave } @objc override var additionalFilter: Bool { diff --git a/Provenance/Game Library/UI/SaveStates/PVSaveStateInfoViewController.swift b/Provenance/Game Library/UI/SaveStates/PVSaveStateInfoViewController.swift index 4017500a5e..f6073b1209 100644 --- a/Provenance/Game Library/UI/SaveStates/PVSaveStateInfoViewController.swift +++ b/Provenance/Game Library/UI/SaveStates/PVSaveStateInfoViewController.swift @@ -21,7 +21,7 @@ final class PVSaveStateInfoViewController: UIViewController, GameLaunchingViewCo @IBOutlet var coreVersionLabel: UILabel! @IBOutlet var createdLabel: UILabel! @IBOutlet var lastPlayedLabel: UILabel! - @IBOutlet var autosaveLabel: UILabel! + @IBOutlet var saveTypeLabel: UILabel! @IBOutlet var playBarButtonItem: UIBarButtonItem! @@ -77,6 +77,7 @@ final class PVSaveStateInfoViewController: UIViewController, GameLaunchingViewCo coreVersionLabel.text = "" createdLabel.text = "" lastPlayedLabel.text = "" + saveTypeLabel.text = "" return } @@ -105,7 +106,7 @@ final class PVSaveStateInfoViewController: UIViewController, GameLaunchingViewCo lastPlayedLabel.text = "Never" } - autosaveLabel.text = saveState.isAutosave ? "Yes" : "No" + saveTypeLabel.text = "\(saveState.saveType)".capitalized } @IBAction func playButtonTapped(_ sender: Any) { diff --git a/Provenance/Game Library/UI/SaveStates/PVSaveStatesViewController.swift b/Provenance/Game Library/UI/SaveStates/PVSaveStatesViewController.swift index 4ad99f914f..e0c7bd436b 100644 --- a/Provenance/Game Library/UI/SaveStates/PVSaveStatesViewController.swift +++ b/Provenance/Game Library/UI/SaveStates/PVSaveStatesViewController.swift @@ -30,6 +30,7 @@ struct SaveSection { final class PVSaveStatesViewController: UICollectionViewController { private var autoSaveStatesObserverToken: NotificationToken! + private var quickSaveStatesObserverToken: NotificationToken! private var manualSaveStatesObserverToken: NotificationToken! weak var delegate: PVSaveStatesViewControllerDelegate? @@ -40,12 +41,14 @@ final class PVSaveStatesViewController: UICollectionViewController { var coreID: String? private var autoSaves: Results! + private var quickSaves: Results! private var manualSaves: Results! deinit { autoSaveStatesObserverToken.invalidate() autoSaveStatesObserverToken = nil - manualSaveStatesObserverToken.invalidate() + quickSaveStatesObserverToken?.invalidate() + quickSaveStatesObserverToken = nil manualSaveStatesObserverToken = nil } @@ -69,8 +72,9 @@ final class PVSaveStatesViewController: UICollectionViewController { allSaves = saveStates.sorted(byKeyPath: "date", ascending: false) } - autoSaves = allSaves.filter("isAutosave == true") - manualSaves = allSaves.filter("isAutosave == false") + manualSaves = allSaves.filter("saveTypeRawValue == '\(SaveType.manual.rawValue)'") + autoSaves = allSaves.filter("saveTypeRawValue == '\(SaveType.auto.rawValue)'") + quickSaves = allSaves.filter("saveTypeRawValue == '\(SaveType.quick.rawValue)'") if screenshot == nil { navigationItem.rightBarButtonItem = nil @@ -107,52 +111,42 @@ final class PVSaveStatesViewController: UICollectionViewController { // autoSaveStatesObserverToken = autoSaves.observe { [weak self] (changes: RealmCollectionChange) in guard let `self` = self else { return } - switch changes { - case .initial: - self.collectionView?.reloadData() - case .update(_, let deletions, _, _): - guard !deletions.isEmpty else { - return - } - - let fromItem = { (item: Int) -> IndexPath in - let section = 0 - return IndexPath(item: item, section: section) - } - self.collectionView?.performBatchUpdates({ - self.collectionView?.deleteItems(at: deletions.map(fromItem)) - }, completion: nil) - case let .error(error): - ELOG("Error updating save states: " + error.localizedDescription) - } + self.handleRealmCollectionChange(changes: changes, collectionView: self.collectionView, section: 0) + } + + quickSaveStatesObserverToken = quickSaves.observe { [weak self] (changes: RealmCollectionChange) in + guard let `self` = self else { return } + self.handleRealmCollectionChange(changes: changes, collectionView: self.collectionView, section: 1) } manualSaveStatesObserverToken = manualSaves.observe { [weak self] (changes: RealmCollectionChange) in guard let `self` = self else { return } - - switch changes { - case .initial: - self.collectionView?.reloadData() - case .update(_, let deletions, let insertions, _): - guard !deletions.isEmpty || !insertions.isEmpty else { - return - } - let fromItem = { (item: Int) -> IndexPath in - let section = 1 - return IndexPath(item: item, section: section) - } - self.collectionView?.performBatchUpdates({ - self.collectionView?.deleteItems(at: deletions.map(fromItem)) - self.collectionView?.insertItems(at: insertions.map(fromItem)) - }, completion: nil) - case let .error(error): - ELOG("Error updating save states: " + error.localizedDescription) - } + self.handleRealmCollectionChange(changes: changes, collectionView: self.collectionView, section: 2) } - - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressRecognized(_:))) + + let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressRecognized(_:))) collectionView?.addGestureRecognizer(longPressRecognizer) } + + func handleRealmCollectionChange(changes: RealmCollectionChange>, collectionView: UICollectionView, section: Int) { + switch changes { + case .initial: + self.collectionView?.reloadData() + case .update(_, let deletions, let insertions, _): + guard deletions.count > 0 || insertions.count > 0 else { + return + } + let fromItem = { (item: Int) -> IndexPath in + return IndexPath(item: item, section: section) + } + self.collectionView?.performBatchUpdates({ + self.collectionView?.deleteItems(at: deletions.map(fromItem)) + self.collectionView?.insertItems(at: insertions.map(fromItem)) + }, completion: nil) + case .error(let error): + ELOG("Error updating save states: " + error.localizedDescription) + } + } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) @@ -185,6 +179,8 @@ final class PVSaveStatesViewController: UICollectionViewController { case 0: state = autoSaves[indexPath.item] case 1: + state = quickSaves[indexPath.item] + case 2: state = manualSaves[indexPath.item] default: break @@ -253,7 +249,7 @@ final class PVSaveStatesViewController: UICollectionViewController { } override func numberOfSections(in _: UICollectionView) -> Int { - return 2 + return 3 } override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { @@ -262,6 +258,8 @@ final class PVSaveStatesViewController: UICollectionViewController { case 0: reusableView.label.text = "Auto Save" case 1: + reusableView.label.text = "Quick Save" + case 2: reusableView.label.text = "Save States" default: break @@ -275,6 +273,8 @@ final class PVSaveStatesViewController: UICollectionViewController { case 0: return autoSaves.count case 1: + return quickSaves.count + case 2: return manualSaves.count default: return 0 @@ -288,6 +288,8 @@ final class PVSaveStatesViewController: UICollectionViewController { case 0: saveState = autoSaves[indexPath.item] case 1: + saveState = quickSaves[indexPath.item] + case 2: saveState = manualSaves[indexPath.item] default: break @@ -304,6 +306,9 @@ final class PVSaveStatesViewController: UICollectionViewController { let saveState = autoSaves[indexPath.item] delegate?.saveStatesViewController(self, load: saveState) case 1: + let saveState = quickSaves[indexPath.item] + delegate?.saveStatesViewController(self, load: saveState) + case 2: var saveState: PVSaveState? saveState = manualSaves[indexPath.item] guard let state = saveState else { diff --git a/Provenance/Resources/Assets.xcassets/button-save-pressed.imageset/Contents.json b/Provenance/Resources/Assets.xcassets/button-save-pressed.imageset/Contents.json new file mode 100644 index 0000000000..e8bb53e882 --- /dev/null +++ b/Provenance/Resources/Assets.xcassets/button-save-pressed.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "save-pressed.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Provenance/Resources/Assets.xcassets/button-save-pressed.imageset/save-pressed.png b/Provenance/Resources/Assets.xcassets/button-save-pressed.imageset/save-pressed.png new file mode 100644 index 0000000000..03d92e50e1 Binary files /dev/null and b/Provenance/Resources/Assets.xcassets/button-save-pressed.imageset/save-pressed.png differ diff --git a/Provenance/Resources/Assets.xcassets/button-save.imageset/Contents.json b/Provenance/Resources/Assets.xcassets/button-save.imageset/Contents.json new file mode 100644 index 0000000000..a83dc03960 --- /dev/null +++ b/Provenance/Resources/Assets.xcassets/button-save.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "save.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Provenance/Resources/Assets.xcassets/button-save.imageset/save.png b/Provenance/Resources/Assets.xcassets/button-save.imageset/save.png new file mode 100644 index 0000000000..0f067da43e Binary files /dev/null and b/Provenance/Resources/Assets.xcassets/button-save.imageset/save.png differ diff --git a/Provenance/Settings/PVSettingsViewController.swift b/Provenance/Settings/PVSettingsViewController.swift index 04e6d968b6..21f42ca2d7 100644 --- a/Provenance/Settings/PVSettingsViewController.swift +++ b/Provenance/Settings/PVSettingsViewController.swift @@ -97,6 +97,7 @@ final class PVSettingsViewController: PVQuickTableViewController { PVSettingsSwitchRow(text: "Timed Auto Saves", key: \PVSettingsModel.timedAutoSaves), PVSettingsSwitchRow(text: "Auto Load Saves", key: \PVSettingsModel.autoLoadSaves), PVSettingsSwitchRow(text: "Ask to Load Saves", key: \PVSettingsModel.askToAutoLoad), + PVSettingsSwitchRow(text: "Show Quicksave Button", key: \PVSettingsModel.showQuicksaveButton) ] let savesSection = Section(title: "Saves", rows: saveRows) diff --git a/Provenance/User Interface/SaveStates.storyboard b/Provenance/User Interface/SaveStates.storyboard index fba9882bd6..156b5bb3c2 100644 --- a/Provenance/User Interface/SaveStates.storyboard +++ b/Provenance/User Interface/SaveStates.storyboard @@ -1,7 +1,11 @@ - + + + + - + + @@ -11,7 +15,7 @@ - + @@ -180,7 +184,7 @@ -