[Experimental Feature] Implements VariableFastForward feature

This commit is contained in:
Riley Testut 2023-04-27 17:11:55 -05:00
parent 233ef7d418
commit 6fd7f9e1d5
7 changed files with 161 additions and 25 deletions

View File

@ -187,7 +187,6 @@
D57D795629F300E100BB2CF8 /* CustomTintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A817AF29DF4E6E00904AFE /* CustomTintColor.swift */; }; D57D795629F300E100BB2CF8 /* CustomTintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A817AF29DF4E6E00904AFE /* CustomTintColor.swift */; };
D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; }; D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; };
D57D796029F315F700BB2CF8 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.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 */; }; D5864970297734280081477E /* CheatMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586496F297734280081477E /* CheatMetadata.swift */; };
D586497229774ABD0081477E /* CheatBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586497129774ABD0081477E /* CheatBase.swift */; }; D586497229774ABD0081477E /* CheatBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586497129774ABD0081477E /* CheatBase.swift */; };
D5864978297756CE0081477E /* CheatBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5864977297756CE0081477E /* CheatBaseView.swift */; }; D5864978297756CE0081477E /* CheatBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5864977297756CE0081477E /* CheatBaseView.swift */; };
@ -1647,7 +1646,6 @@
D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */, D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */,
D51CB7A629EDC15900B59678 /* ExperimentalFeatures.swift in Sources */, D51CB7A629EDC15900B59678 /* ExperimentalFeatures.swift in Sources */,
D57D796029F315F700BB2CF8 /* ExperimentalFeaturesView.swift in Sources */, D57D796029F315F700BB2CF8 /* ExperimentalFeaturesView.swift in Sources */,
D57D796129F31E7500BB2CF8 /* VariableFastForward.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -1108,9 +1108,18 @@ extension GameViewController
guard let emulatorCore = self.emulatorCore else { return } guard let emulatorCore = self.emulatorCore else { return }
if activate if activate
{
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 emulatorCore.rate = emulatorCore.deltaCore.supportedRates.upperBound
} }
}
else else
{ {
emulatorCore.rate = emulatorCore.deltaCore.supportedRates.lowerBound emulatorCore.rate = emulatorCore.deltaCore.supportedRates.lowerBound

View File

@ -8,15 +8,32 @@
import SwiftUI import SwiftUI
import DeltaCore
import DeltaFeatures import DeltaFeatures
enum FastForwardSpeed: Double, CaseIterable, CustomStringConvertible struct FastForwardSpeed: RawRepresentable
{ {
case x2 = 2 let rawValue: Double
case x3 = 3
case x4 = 4
case x8 = 8
init(rawValue: Double)
{
self.rawValue = rawValue
}
static func speeds(in range: ClosedRange<Double>) -> [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 { var description: String {
if #available(iOS 15, *) if #available(iOS 15, *)
{ {
@ -28,10 +45,7 @@ enum FastForwardSpeed: Double, CaseIterable, CustomStringConvertible
return "\(self.rawValue)x" return "\(self.rawValue)x"
} }
} }
}
extension FastForwardSpeed: LocalizedOptionValue
{
var localizedDescription: Text { var localizedDescription: Text {
Text(self.description) Text(self.description)
} }
@ -50,24 +64,56 @@ struct VariableFastForwardOptions
// @Option // No name = hidden // @Option // No name = hidden
// var preferredSpeedsBySystem: [String: Double] = [:] // 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? 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? 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? 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? 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? 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? 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? 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
}
}
}
}

View File

@ -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)
}
}

View File

@ -15,6 +15,8 @@ class MenuItem: NSObject
var image: UIImage? var image: UIImage?
var action: ((MenuItem) -> Void) var action: ((MenuItem) -> Void)
var menu: UIMenu?
@objc dynamic var isSelected = false @objc dynamic var isSelected = false
init(text: String, image: UIImage?, action: @escaping ((MenuItem) -> Void)) init(text: String, image: UIImage?, action: @escaping ((MenuItem) -> Void))

View File

@ -161,7 +161,7 @@ private extension PauseViewController
self.sustainButtonsItem = nil self.sustainButtonsItem = nil
self.fastForwardItem = 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.saveStateItem = MenuItem(text: NSLocalizedString("Save State", comment: ""), image: #imageLiteral(resourceName: "SaveSaveState"), action: { [unowned self] _ in
self.saveStatesViewControllerMode = .saving 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.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 }) 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() func updateSafeAreaInsets()
@ -194,4 +200,56 @@ private extension PauseViewController
self.additionalSafeAreaInsets.right = 0 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
}
} }

View File

@ -22,11 +22,6 @@ struct ExperimentalFeatures: FeatureContainer
options: CustomTintColorOptions()) options: CustomTintColorOptions())
var customTintColor 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() private init()
{ {
self.prepareFeatures() self.prepareFeatures()