diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 5bae28e..e240816 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -187,7 +187,6 @@ D57D795629F300E100BB2CF8 /* CustomTintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A817AF29DF4E6E00904AFE /* CustomTintColor.swift */; }; D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; }; D57D796029F315F700BB2CF8 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.swift */; }; - D57D796129F31E7500BB2CF8 /* VariableFastForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */; }; D5864970297734280081477E /* CheatMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586496F297734280081477E /* CheatMetadata.swift */; }; D586497229774ABD0081477E /* CheatBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586497129774ABD0081477E /* CheatBase.swift */; }; D5864978297756CE0081477E /* CheatBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5864977297756CE0081477E /* CheatBaseView.swift */; }; @@ -1647,7 +1646,6 @@ D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */, D51CB7A629EDC15900B59678 /* ExperimentalFeatures.swift in Sources */, D57D796029F315F700BB2CF8 /* ExperimentalFeaturesView.swift in Sources */, - D57D796129F31E7500BB2CF8 /* VariableFastForward.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index d9346e6..f8ae447 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -1109,7 +1109,16 @@ extension GameViewController if activate { - emulatorCore.rate = emulatorCore.deltaCore.supportedRates.upperBound + if ExperimentalFeatures.shared.variableFastForward.isEnabled, + let preferredSpeed = ExperimentalFeatures.shared.variableFastForward[emulatorCore.game.type], + preferredSpeed.rawValue <= emulatorCore.deltaCore.supportedRates.upperBound + { + emulatorCore.rate = preferredSpeed.rawValue + } + else + { + emulatorCore.rate = emulatorCore.deltaCore.supportedRates.upperBound + } } else { diff --git a/Delta/Experimental Features/Features/VariableFastForward.swift b/Delta/Experimental Features/Features/VariableFastForward.swift index c33fb59..735ec11 100644 --- a/Delta/Experimental Features/Features/VariableFastForward.swift +++ b/Delta/Experimental Features/Features/VariableFastForward.swift @@ -8,15 +8,32 @@ import SwiftUI +import DeltaCore import DeltaFeatures -enum FastForwardSpeed: Double, CaseIterable, CustomStringConvertible +struct FastForwardSpeed: RawRepresentable { - case x2 = 2 - case x3 = 3 - case x4 = 4 - case x8 = 8 + let rawValue: Double + init(rawValue: Double) + { + self.rawValue = rawValue + } + + static func speeds(in range: ClosedRange) -> [FastForwardSpeed] + { + // .dropFirst() to remove 1x speed. + var speeds = stride(from: range.lowerBound, to: range.upperBound, by: 1.0).dropFirst().map { FastForwardSpeed(rawValue: $0) } + + // Handles both integer and non-integer maximum speeds, because range.upperBound is not included in `speeds`. + speeds.append(.init(rawValue: range.upperBound)) + + return speeds + } +} + +extension FastForwardSpeed: CustomStringConvertible, LocalizedOptionValue +{ var description: String { if #available(iOS 15, *) { @@ -28,10 +45,7 @@ enum FastForwardSpeed: Double, CaseIterable, CustomStringConvertible return "\(self.rawValue)x" } } -} - -extension FastForwardSpeed: LocalizedOptionValue -{ + var localizedDescription: Text { Text(self.description) } @@ -50,24 +64,56 @@ struct VariableFastForwardOptions // @Option // No name = hidden // var preferredSpeedsBySystem: [String: Double] = [:] - @Option(name: "Nintendo", description: "Preferred NES fast forward speed.", values: FastForwardSpeed.allCases) + @Option(name: "Nintendo", description: "Preferred NES fast forward speed.", values: FastForwardSpeed.speeds(in: System.nes.deltaCore.supportedRates)) var nes: FastForwardSpeed? - @Option(name: "Super Nintendo", description: "Preferred SNES fast forward speed.", values: FastForwardSpeed.allCases) + @Option(name: "Super Nintendo", description: "Preferred SNES fast forward speed.", values: FastForwardSpeed.speeds(in: System.snes.deltaCore.supportedRates)) var snes: FastForwardSpeed? - @Option(name: "Sega Genesis", description: "Preferred Genesis fast forward speed.", values: FastForwardSpeed.allCases) + @Option(name: "Sega Genesis", description: "Preferred Genesis fast forward speed.", values: FastForwardSpeed.speeds(in: System.genesis.deltaCore.supportedRates)) var genesis: FastForwardSpeed? - @Option(name: "Nintendo 64", description: "Preferred N64 fast forward speed.", values: FastForwardSpeed.allCases) + @Option(name: "Nintendo 64", description: "Preferred N64 fast forward speed.", values: FastForwardSpeed.speeds(in: System.n64.deltaCore.supportedRates)) var n64: FastForwardSpeed? - @Option(name: "Game Boy Color", description: "Preferred GBC fast forward speed.", values: FastForwardSpeed.allCases) + @Option(name: "Game Boy Color", description: "Preferred GBC fast forward speed.", values: FastForwardSpeed.speeds(in: System.gbc.deltaCore.supportedRates)) var gbc: FastForwardSpeed? - @Option(name: "Game Boy Advance", description: "Preferred GBA fast forward speed.", values: FastForwardSpeed.allCases) + @Option(name: "Game Boy Advance", description: "Preferred GBA fast forward speed.", values: FastForwardSpeed.speeds(in: System.gba.deltaCore.supportedRates)) var gba: FastForwardSpeed? - @Option(name: "Nintendo DS", description: "Preferred DS fast forward speed.", values: FastForwardSpeed.allCases) + @Option(name: "Nintendo DS", description: "Preferred DS fast forward speed.", values: FastForwardSpeed.speeds(in: System.ds.deltaCore.supportedRates)) var ds: FastForwardSpeed? } + +extension Feature where Options == VariableFastForwardOptions +{ + subscript(gameType: GameType) -> FastForwardSpeed? { + get { + guard let system = System(gameType: gameType) else { return nil } + switch system + { + case .nes: return self.nes + case .snes: return self.snes + case .genesis: return self.genesis + case .n64: return self.n64 + case .gbc: return self.gbc + case .gba: return self.gba + case .ds: return self.ds + } + } + set { + guard let system = System(gameType: gameType) else { return } + switch system + { + case .nes: self.nes = newValue + case .snes: self.snes = newValue + case .genesis: self.genesis = newValue + case .n64: self.n64 = newValue + case .gbc: self.gbc = newValue + case .gba: self.gba = newValue + case .ds: self.ds = newValue + } + } + } +} diff --git a/Delta/Pause Menu/GridMenuViewController.swift b/Delta/Pause Menu/GridMenuViewController.swift index e1402d6..ab12807 100644 --- a/Delta/Pause Menu/GridMenuViewController.swift +++ b/Delta/Pause Menu/GridMenuViewController.swift @@ -174,3 +174,31 @@ extension GridMenuViewController } } +extension GridMenuViewController +{ + override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? + { + let item = self.dataSource.item(at: indexPath) + guard let menu = item.menu else { return nil } + + return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { _ in menu } + } + + override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? + { + guard let indexPath = configuration.identifier as? IndexPath else { return nil } + guard let cell = collectionView.cellForItem(at: indexPath) as? GridCollectionViewCell else { return nil } + + let parameters = UIPreviewParameters() + parameters.backgroundColor = .clear + parameters.visiblePath = UIBezierPath(rect: cell.contentView.bounds) + + let preview = UITargetedPreview(view: cell.contentView, parameters: parameters) + return preview + } + + override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? + { + return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration) + } +} diff --git a/Delta/Pause Menu/MenuItem.swift b/Delta/Pause Menu/MenuItem.swift index 3335b9c..7ae73d6 100644 --- a/Delta/Pause Menu/MenuItem.swift +++ b/Delta/Pause Menu/MenuItem.swift @@ -15,6 +15,8 @@ class MenuItem: NSObject var image: UIImage? var action: ((MenuItem) -> Void) + var menu: UIMenu? + @objc dynamic var isSelected = false init(text: String, image: UIImage?, action: @escaping ((MenuItem) -> Void)) diff --git a/Delta/Pause Menu/PauseViewController.swift b/Delta/Pause Menu/PauseViewController.swift index 9a8df82..4ba0312 100644 --- a/Delta/Pause Menu/PauseViewController.swift +++ b/Delta/Pause Menu/PauseViewController.swift @@ -161,7 +161,7 @@ private extension PauseViewController self.sustainButtonsItem = nil self.fastForwardItem = nil - guard self.emulatorCore != nil else { return } + guard let emulatorCore = self.emulatorCore else { return } self.saveStateItem = MenuItem(text: NSLocalizedString("Save State", comment: ""), image: #imageLiteral(resourceName: "SaveSaveState"), action: { [unowned self] _ in self.saveStatesViewControllerMode = .saving @@ -179,6 +179,12 @@ private extension PauseViewController self.fastForwardItem = MenuItem(text: NSLocalizedString("Fast Forward", comment: ""), image: #imageLiteral(resourceName: "FastForward"), action: { _ in }) self.sustainButtonsItem = MenuItem(text: NSLocalizedString("Hold Buttons", comment: ""), image: #imageLiteral(resourceName: "SustainButtons"), action: { _ in }) + + if ExperimentalFeatures.shared.variableFastForward.isEnabled + { + let menu = self.makeFastForwardMenu(for: emulatorCore.game) + self.fastForwardItem?.menu = menu + } } func updateSafeAreaInsets() @@ -194,4 +200,56 @@ private extension PauseViewController self.additionalSafeAreaInsets.right = 0 } } + + func makeFastForwardMenu(for game: GameProtocol) -> UIMenu? + { + guard let deltaCore = Delta.core(for: game.type), #available(iOS 15, *) else { return nil } + + let menu = UIMenu(title: NSLocalizedString("Change the Fast Forward speed for this system.", comment: ""), options: [.singleSelection], children: [ + UIDeferredMenuElement.uncached { [weak self] completion in + let preferredSpeed = ExperimentalFeatures.shared.variableFastForward[game.type] + + let supportedSpeeds = FastForwardSpeed.speeds(in: deltaCore.supportedRates) + var actions = zip(0..., supportedSpeeds).map { (index, speed) in + + let state: UIAction.State = (speed == preferredSpeed) ? .on : .off + let action = UIAction(title: speed.description, state: state) { action in + ExperimentalFeatures.shared.variableFastForward[game.type] = speed + + if let fastForwardItem = self?.fastForwardItem + { + fastForwardItem.isSelected = true // Always enable FF after selecting speed. + fastForwardItem.action(fastForwardItem) + } + } + + if #available(iOS 16, *) + { + let configuration = UIImage.SymbolConfiguration(hierarchicalColor: .deltaPurple) + + let percentage = Double(index + 1) / Double(supportedSpeeds.count) + action.image = UIImage(systemName: "timelapse", variableValue: percentage, configuration: configuration) + } + + return action + } + + let state: UIAction.State = (preferredSpeed == nil) ? .on : .off + let action = UIAction(title: NSLocalizedString("Maximum", comment: ""), state: state) { action in + ExperimentalFeatures.shared.variableFastForward[game.type] = nil + + if let fastForwardItem = self?.fastForwardItem + { + fastForwardItem.isSelected = true // Always enable FF after selecting speed. + fastForwardItem.action(fastForwardItem) + } + } + actions.append(action) + + completion(actions) + } + ]) + + return menu + } } diff --git a/DeltaPreviews/Experimental Features/ExperimentalFeatures.swift b/DeltaPreviews/Experimental Features/ExperimentalFeatures.swift index f14f5d8..26f6e63 100644 --- a/DeltaPreviews/Experimental Features/ExperimentalFeatures.swift +++ b/DeltaPreviews/Experimental Features/ExperimentalFeatures.swift @@ -22,11 +22,6 @@ struct ExperimentalFeatures: FeatureContainer options: CustomTintColorOptions()) var customTintColor - @Feature(name: "Variable Fast Forward", - description: "Change the preferred Fast Foward speed per-system. You can also change it by long-pressing the Fast Forward button from the Pause Menu.", - options: VariableFastForwardOptions()) - var variableFastForward - private init() { self.prepareFeatures()