Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: NSO-like Rewind Functionality #131

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 93 additions & 19 deletions Delta/Base.lproj/Settings.storyboard

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Delta/Database/Model/Human/SaveState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import struct DSDeltaCore.DS
case quick
case general
case locked
case rewind
}

@objc(SaveState)
Expand Down Expand Up @@ -128,7 +129,7 @@ extension SaveState: Syncable
// self.game may be nil if being downloaded, so don't enforce it.
// guard let identifier = self.game?.identifier else { return false }

let isSyncingEnabled = (self.type != .auto && self.type != .quick) && (self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier)
let isSyncingEnabled = (self.type != .auto && self.type != .quick && self.type != .rewind) && (self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier)
return isSyncingEnabled
}

Expand Down
136 changes: 132 additions & 4 deletions Delta/Emulation/GameViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ class GameViewController: DeltaCore.GameViewController
self.updateControllers()

self.presentedGyroAlert = false

self.clearRewindSaveStates()
}
}

Expand Down Expand Up @@ -164,6 +166,8 @@ class GameViewController: DeltaCore.GameViewController
private var sustainButtonsBackgroundView: RSTPlaceholderView!
private var inputsToSustain = [AnyInput: Double]()

private var rewindTimer: Timer?

private var isGyroActive = false
private var presentedGyroAlert = false

Expand Down Expand Up @@ -210,6 +214,8 @@ class GameViewController: DeltaCore.GameViewController
deinit
{
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)

self.invalidateRewindTimer()
}

// MARK: - GameControllerReceiver -
Expand Down Expand Up @@ -330,6 +336,8 @@ extension GameViewController

UserDefaults.standard.desmumeDeprecatedAlertCount += 1
}

self.activateRewindTimer()
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
Expand All @@ -338,7 +346,7 @@ extension GameViewController

coordinator.animate(alongsideTransition: { (context) in
self.updateControllerSkin()
}, completion: nil)
}, completion: nil)
}

// MARK: - Segues
Expand Down Expand Up @@ -764,11 +772,45 @@ extension GameViewController: SaveStatesViewControllerDelegate
}
}

private func update(_ saveState: SaveState, with replacementSaveState: SaveStateProtocol? = nil)
private func clearRewindSaveStates(afterDate: Date? = nil)
{
guard let game = self.game as? Game else { return }

let fetchRequest = SaveState.fetchRequest(for: game, type: .rewind)
fetchRequest.includesPropertyValues = false

// if afterDate is included, we have rewound and should clear any rewind states that exist after our new time location
if let afterDate = afterDate
{
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K >= %@", #keyPath(SaveState.type), NSNumber(value: SaveStateType.rewind.rawValue), #keyPath(SaveState.creationDate), afterDate as NSDate)
}
else
{
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(SaveState.type), NSNumber(value: SaveStateType.rewind.rawValue))
}

DatabaseManager.shared.performBackgroundTask { (context) in
do
{
let saveStates = try context.fetch(fetchRequest)
for saveState in saveStates {
let temporarySaveState = context.object(with: saveState.objectID)
context.delete(temporarySaveState)
}
context.saveWithErrorLogging()
}
catch
{
print(error)
}
}
}

private func update(_ saveState: SaveState, with replacementSaveState: SaveStateProtocol? = nil, shouldSuspendEmulation: Bool = true)
{
let isRunning = (self.emulatorCore?.state == .running)

if isRunning
if isRunning && shouldSuspendEmulation
{
self.pauseEmulation()
}
Expand Down Expand Up @@ -810,7 +852,7 @@ extension GameViewController: SaveStatesViewControllerDelegate
saveState.modifiedDate = Date()
saveState.coreIdentifier = self.emulatorCore?.deltaCore.identifier

if isRunning
if isRunning && shouldSuspendEmulation
{
self.resumeEmulation()
}
Expand Down Expand Up @@ -867,6 +909,18 @@ extension GameViewController: SaveStatesViewControllerDelegate
print(error)
}

// delay by 0.5 so as not to interfere with other operations
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if let rewindSaveState = saveState as? SaveState, rewindSaveState.type == .rewind
{
self.clearRewindSaveStates(afterDate: rewindSaveState.creationDate)
}
else
{
self.clearRewindSaveStates()
}
}

if isRunning
{
self.resumeEmulation()
Expand Down Expand Up @@ -1232,3 +1286,77 @@ private extension UserDefaults
{
@NSManaged var desmumeDeprecatedAlertCount: Int
}

//MARK: - Timer -
private extension GameViewController
{
func activateRewindTimer()
{
self.invalidateRewindTimer()
guard Settings.isRewindEnabled == true else { return }
let interval = TimeInterval(Settings.rewindTimerInterval)
self.rewindTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(rewindPollFunction), userInfo: nil, repeats: true)
}

func invalidateRewindTimer()
{
self.rewindTimer?.invalidate()
}

@objc func rewindPollFunction() {

guard Settings.isRewindEnabled == true,
self.emulatorCore?.state == .running,
let game = self.game as? Game else { return }

// first; cap number of rewind states to 30. do this by deleting oldest state if >= 30 exist
let fetchRequest: NSFetchRequest<SaveState> = SaveState.fetchRequest()
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(SaveState.creationDate), ascending: true)]

if let system = System(gameType: game.type)
{
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@ AND %K == %@", #keyPath(SaveState.game), game, #keyPath(SaveState.coreIdentifier), system.deltaCore.identifier, #keyPath(SaveState.type), NSNumber(value: SaveStateType.rewind.rawValue))
}
else
{
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@", #keyPath(SaveState.game), game, #keyPath(SaveState.type), NSNumber(value: SaveStateType.rewind.rawValue))
}

do
{
let rewindStateCount = try DatabaseManager.shared.viewContext.count(for: fetchRequest)
if rewindStateCount >= 30
{
fetchRequest.fetchLimit = 1
if let oldestRewindSaveState = try DatabaseManager.shared.viewContext.fetch(fetchRequest).first
{
DatabaseManager.shared.performBackgroundTask { (context) in
let temporarySaveState = context.object(with: oldestRewindSaveState.objectID)
context.delete(temporarySaveState)
context.saveWithErrorLogging()
}
}
}
}
catch
{
print(error)
}

// second; save new state
let backgroundContext = DatabaseManager.shared.newBackgroundContext()
backgroundContext.perform {

let game = backgroundContext.object(with: game.objectID) as! Game

let saveState = SaveState(context: backgroundContext)
saveState.type = .rewind
saveState.game = game

self.update(saveState, shouldSuspendEmulation: false)

backgroundContext.saveWithErrorLogging()
}
}
}
14 changes: 12 additions & 2 deletions Delta/Pause Menu/PauseViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class PauseViewController: UIViewController, PauseInfoProviding
}

var pauseItems: [MenuItem] {
return [self.saveStateItem, self.loadStateItem, self.cheatCodesItem, self.fastForwardItem, self.sustainButtonsItem].compactMap { $0 }
return [self.saveStateItem, self.loadStateItem, self.cheatCodesItem, self.fastForwardItem, self.sustainButtonsItem, self.rewindItem].compactMap { $0 }
}

/// Pause Items
Expand All @@ -28,6 +28,7 @@ class PauseViewController: UIViewController, PauseInfoProviding
var cheatCodesItem: MenuItem?
var fastForwardItem: MenuItem?
var sustainButtonsItem: MenuItem?
var rewindItem: MenuItem?

/// PauseInfoProviding
var pauseText: String?
Expand Down Expand Up @@ -114,9 +115,9 @@ extension PauseViewController
case "saveStates":
let saveStatesViewController = segue.destination as! SaveStatesViewController
saveStatesViewController.delegate = self.saveStatesViewControllerDelegate
saveStatesViewController.mode = self.saveStatesViewControllerMode
saveStatesViewController.game = self.emulatorCore?.game as? Game
saveStatesViewController.emulatorCore = self.emulatorCore
saveStatesViewController.mode = self.saveStatesViewControllerMode

case "cheats":
let cheatsViewController = segue.destination as! CheatsViewController
Expand Down Expand Up @@ -160,6 +161,7 @@ private extension PauseViewController
self.cheatCodesItem = nil
self.sustainButtonsItem = nil
self.fastForwardItem = nil
self.rewindItem = nil

guard self.emulatorCore != nil else { return }

Expand All @@ -173,6 +175,14 @@ private extension PauseViewController
self.performSegue(withIdentifier: "saveStates", sender: self)
})

if Settings.isRewindEnabled
{
self.rewindItem = MenuItem(text: NSLocalizedString("Rewind", comment: ""), image: #imageLiteral(resourceName: "Rewind"), action: { [unowned self] _ in
self.saveStatesViewControllerMode = .rewind
self.performSegue(withIdentifier: "saveStates", sender: self)
})
}

self.cheatCodesItem = MenuItem(text: NSLocalizedString("Cheat Codes", comment: ""), image: #imageLiteral(resourceName: "CheatCodes"), action: { [unowned self] _ in
self.performSegue(withIdentifier: "cheats", sender: self)
})
Expand Down
34 changes: 27 additions & 7 deletions Delta/Pause Menu/Save States/SaveStatesViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extension SaveStatesViewController
{
case saving
case loading
case rewind
}

enum Section: Int
Expand All @@ -32,6 +33,7 @@ extension SaveStatesViewController
case quick
case general
case locked
case rewind
}
}

Expand Down Expand Up @@ -111,6 +113,11 @@ extension SaveStatesViewController
self.title = NSLocalizedString("Load State", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("You can create a new save state by pressing the Save State option in the pause menu.", comment: "")
self.navigationItem.rightBarButtonItems?.removeFirst()

case .rewind:
self.title = NSLocalizedString("Rewind State", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("Rewind States will appear here as gameplay advances.", comment: "")
self.navigationItem.rightBarButtonItems?.removeFirst()
}

// Manually update prototype cell properties
Expand All @@ -132,7 +139,7 @@ extension SaveStatesViewController
self.navigationController?.toolbar.barStyle = .blackTranslucent

self.update()
}
}

override func viewWillDisappear(_ animated: Bool)
{
Expand Down Expand Up @@ -204,13 +211,15 @@ private extension SaveStatesViewController
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(SaveState.type), ascending: true), NSSortDescriptor(key: #keyPath(SaveState.creationDate), ascending: Settings.sortSaveStatesByOldestFirst)]

let rewindEvaluationOperator = self.mode == .rewind ? "==" : "!="

if let system = System(gameType: self.game.type)
{
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@", #keyPath(SaveState.game), self.game, #keyPath(SaveState.coreIdentifier), system.deltaCore.identifier)
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@ AND %K \(rewindEvaluationOperator) %@", #keyPath(SaveState.game), game, #keyPath(SaveState.coreIdentifier), system.deltaCore.identifier, #keyPath(SaveState.type), NSNumber(value: SaveStateType.rewind.rawValue))
}
else
{
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(SaveState.game), self.game)
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K \(rewindEvaluationOperator) %@", #keyPath(SaveState.game), game, #keyPath(SaveState.type), NSNumber(value: SaveStateType.rewind.rawValue))
}

self.dataSource.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(SaveState.type), cacheName: nil)
Expand Down Expand Up @@ -259,15 +268,23 @@ private extension SaveStatesViewController
case .translucent:
cell.isTextLabelVibrancyEnabled = true
cell.isImageViewVibrancyEnabled = true
}
}

let deltaCore = Delta.core(for: self.game.type)!

let dimensions = deltaCore.videoFormat.dimensions
cell.maximumImageSize = CGSize(width: self.prototypeCellWidthConstraint.constant, height: (self.prototypeCellWidthConstraint.constant / dimensions.width) * dimensions.height)

cell.textLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
cell.textLabel.text = saveState.localizedName
if self.mode == .rewind
{
let differenceInSeconds = Int(Date().timeIntervalSince(saveState.modifiedDate))
cell.textLabel.text = "\(differenceInSeconds)s Ago"
}
else
{
cell.textLabel.text = saveState.localizedName
}
}

func configure(_ headerView: SaveStatesCollectionHeaderView, forSection section: Int)
Expand All @@ -282,6 +299,7 @@ private extension SaveStatesViewController
case .quick: title = NSLocalizedString("Quick Save", comment: "")
case .general: title = NSLocalizedString("General", comment: "")
case .locked: title = NSLocalizedString("Locked", comment: "")
case .rewind: title = NSLocalizedString("Rewind", comment: "")
}

headerView.textLabel.text = title
Expand Down Expand Up @@ -494,7 +512,7 @@ private extension SaveStatesViewController

func actionsForSaveState(_ saveState: SaveState) -> [Action]?
{
guard saveState.type != .auto else { return nil }
guard saveState.type != .auto && saveState.type != .rewind else { return nil }

let isPreviewAvailable: Bool

Expand Down Expand Up @@ -550,6 +568,7 @@ private extension SaveStatesViewController
self.unlockSaveState(saveState)
})
actions.append(unlockAction)
case .rewind: break
}

let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, image: UIImage(symbolNameIfAvailable: "trash"), action: { [unowned self] action in
Expand Down Expand Up @@ -720,7 +739,7 @@ extension SaveStatesViewController
let section = self.correctedSectionForSectionIndex(indexPath.section)
switch section
{
case .auto: break
case .auto, .rewind: break
case .quick, .general:
let backgroundContext = DatabaseManager.shared.newBackgroundContext()
backgroundContext.performAndWait() {
Expand All @@ -736,6 +755,7 @@ extension SaveStatesViewController
}

case .loading: self.loadSaveState(saveState)
case .rewind: self.loadSaveState(saveState)
}
}
}
Expand Down
Loading