Skip to content

Commit 53277cd

Browse files
committed
quickload quick save support
Signed-off-by: Joseph Mattiello <git@joemattiello.com>
1 parent 034320d commit 53277cd

File tree

7 files changed

+148
-14
lines changed

7 files changed

+148
-14
lines changed

PVLibrary/Sources/PVLibrary/Errors/SaveStateError.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public enum SaveStateError: Error {
1313
case noCoreFound(String)
1414
case realmWriteError(Error)
1515
case realmDeletionError(Error)
16-
16+
case noSaveStatesFound
17+
case saveStateFileNotFound
18+
1719
var localizedDescription: String {
1820
switch self {
1921
case let .coreSaveError(coreError): return "Core failed to save: \(coreError?.localizedDescription ?? "No reason given.")"
@@ -23,6 +25,8 @@ public enum SaveStateError: Error {
2325
case let .noCoreFound(id): return "No core found to match id: \(id)"
2426
case let .realmWriteError(realmError): return "Unable to write save state to realm: \(realmError.localizedDescription)"
2527
case let .realmDeletionError(realmError): return "Unable to delete old auto-save from database: \(realmError.localizedDescription)"
28+
case let .noSaveStatesFound: return "Save state file not found"
29+
case let .saveStateFileNotFound: return "Save state file not found"
2630
}
2731
}
2832
}

PVUI/Sources/PVUIBase/Controller/PVEmulatorViewController+DeltaSkin.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,13 @@ extension PVEmulatorViewController {
154154
DLOG("No controller view controller found")
155155
}
156156

157-
// Pass the controller view controller directly without casting
158-
let inputHandler = DeltaSkinInputHandler(emulatorCore: core, controllerVC: controllerViewController)
157+
// Log emulator controller availability
158+
DLOG("Using self as emulator controller for special commands (quicksave/quickload)")
159+
160+
// Pass the controller view controller and emulator controller (self) to the input handler
161+
let inputHandler = DeltaSkinInputHandler(emulatorCore: core,
162+
controllerVC: controllerViewController,
163+
emulatorController: self)
159164

160165
// Set up the menu button handler to show the emulator menu
161166
inputHandler.menuButtonHandler = { [weak self] in

PVUI/Sources/PVUIBase/PVEmulatorVC/GameplayDurationTrackerUtil.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// MARK: Play Duration
1111
protocol GameplayDurationTrackerUtil: class {
1212
var gameStartTime: Date? { get set }
13-
var game: PVGame { get }
13+
var game: PVGame! { get }
1414

1515
func updatePlayedDuration()
1616
func updateLastPlayedTime()
@@ -22,7 +22,8 @@ extension GameplayDurationTrackerUtil {
2222
// Clear any temp pointer to start time
2323
self.gameStartTime = nil
2424
}
25-
guard let gameStartTime = gameStartTime else {
25+
guard let gameStartTime = gameStartTime, let game = game else {
26+
ELOG("Game start time or game is nil")
2627
return
2728
}
2829

PVUI/Sources/PVUIBase/PVEmulatorVC/PVEmulatorControllerProtocol.swift

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public protocol PVEmualatorControllerProtocol: AnyObject {
1818

1919
// MARK: Memebers
2020
var core: PVEmulatorCore { get }
21-
var game: PVGame { get }
21+
var game: PVGame! { get }
2222

2323
// MARK: UI
2424
var isShowingMenu: Bool { get set }
@@ -41,6 +41,11 @@ public protocol PVEmualatorControllerProtocol: AnyObject {
4141

4242
// MARK: Saves
4343
func quit(optionallySave canSave: Bool, completion: QuitCompletion?) async
44+
func quicksave() async throws -> Bool
45+
func quickload() async throws -> Bool
46+
func autoSaveState() async throws -> Bool
47+
48+
func takeScreenshot()
4449

4550
// MARK: Menus
4651
func hideOrShowMenuButton()
@@ -145,6 +150,49 @@ public extension PVEmualatorControllerProtocol {
145150

146151
// MARK: Screenshots
147152
public extension PVEmualatorControllerProtocol {
153+
154+
@discardableResult
155+
@MainActor
156+
func quicksave() async throws -> Bool {
157+
guard core.supportsSaveStates else {
158+
WLOG("Core \(core.description) doesn't support save states.")
159+
throw SaveStateError.saveStatesUnsupportedByCore
160+
}
161+
162+
DLOG("Performing quick save for \(game.title)")
163+
let image = captureScreenshot()
164+
return try await createNewSaveState(auto: false, screenshot: image)
165+
}
166+
167+
@discardableResult
168+
@MainActor
169+
func quickload() async throws -> Bool {
170+
guard core.supportsSaveStates else {
171+
WLOG("Core \(core.description) doesn't support save states.")
172+
throw SaveStateError.saveStatesUnsupportedByCore
173+
}
174+
175+
// Get the most recent save state (manual or auto)
176+
let saveStates = game.saveStates.sorted(byKeyPath: "date", ascending: false)
177+
178+
guard let latestSaveState = saveStates.first else {
179+
WLOG("No save states found for \(game.title)")
180+
throw SaveStateError.noSaveStatesFound
181+
}
182+
183+
DLOG("Loading most recent save state for \(game.title) from \(latestSaveState.date)")
184+
185+
// Load the save state
186+
guard let saveStateURL = latestSaveState.url else {
187+
ELOG("Save state file URL is nil")
188+
throw SaveStateError.saveStateFileNotFound
189+
}
190+
191+
try await core.loadState(fromFileAtPath: saveStateURL.path)
192+
DLOG("Successfully loaded save state")
193+
194+
return true
195+
}
148196

149197
@discardableResult
150198
@MainActor
@@ -175,12 +223,13 @@ public extension PVEmualatorControllerProtocol {
175223
return try await createNewSaveState(auto: true, screenshot: image)
176224
}
177225

178-
#if os(iOS)
179226
func takeScreenshot() {
180227
if let screenshot = captureScreenshot() {
228+
#if os(iOS)
181229
Task.detached {
182230
UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
183231
}
232+
#endif
184233

185234
if let pngData = screenshot.pngData() {
186235
let dateString = PVEmulatorConfiguration.string(fromDate: Date())
@@ -203,7 +252,6 @@ public extension PVEmualatorControllerProtocol {
203252
isShowingMenu = false
204253
}
205254

206-
#endif
207255

208256
// #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")
209257
@MainActor

PVUI/Sources/PVUIBase/PVEmulatorVC/PVEmulatorViewController+ScreenShot.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
extension PVEmulatorViewController {
1212

13+
@MainActor
1314
public func captureScreenshot() -> UIImage? {
1415
fpsLabel.alpha = 0.0
1516
if (core.skipLayout && core.touchViewController != nil) {

PVUI/Sources/PVUIBase/PVEmulatorVC/PVEmulatorViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ final class PVEmulatorViewController: PVEmulatorViewControllerRootClass, PVEmual
5353

5454

5555
public let core: PVEmulatorCore
56-
public let game: PVGame
56+
@ThreadSafe
57+
public var game: PVGame!
5758
public internal(set) var autosaveTimer: Timer?
5859
public internal(set) var gameStartTime: Date?
5960
// Store a reference to the skin container view

PVUI/Sources/PVUIBase/SwiftUI/DeltaSkins/Models/DeltaSkinInputHandler.swift

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Combine
44
import PVEmulatorCore
55
import PVCoreBridge
66
import PVLogging
7+
import PVUIBase
78

89
/// Handles input from Delta Skins and forwards it to the emulator core or controller
910
public class DeltaSkinInputHandler: ObservableObject {
@@ -13,16 +14,20 @@ public class DeltaSkinInputHandler: ObservableObject {
1314
/// The controller view controller to send controller-based inputs to
1415
private weak var controllerVC: (any ControllerVC)?
1516

17+
/// The emulator controller for handling special commands like quicksave and quickload
18+
private weak var emulatorController: (any PVEmualatorControllerProtocol)?
19+
1620
/// A dummy D-pad for sending directional input to the controller
1721
private let dummyDPad = JSDPad(frame: .zero)
1822

1923
/// Callback for menu button presses
2024
var menuButtonHandler: (() -> Void)?
2125

2226
/// Initialize with an emulator core and optional controller view controller
23-
public init(emulatorCore: PVEmulatorCore? = nil, controllerVC: (any ControllerVC)? = nil) {
27+
public init(emulatorCore: PVEmulatorCore? = nil, controllerVC: (any ControllerVC)? = nil, emulatorController: (any PVEmualatorControllerProtocol)? = nil) {
2428
self.emulatorCore = emulatorCore
2529
self.controllerVC = controllerVC
30+
self.emulatorController = emulatorController
2631

2732
// Set the tag to match the D-pad tag expected by the controller
2833
dummyDPad.tag = ControlTag.dpad1.rawValue
@@ -37,17 +42,37 @@ public class DeltaSkinInputHandler: ObservableObject {
3742
func setControllerVC(_ controller: (any ControllerVC)?) {
3843
self.controllerVC = controller
3944
}
45+
46+
/// Set the emulator controller
47+
func setEmulatorController(_ controller: (any PVEmualatorControllerProtocol)?) {
48+
self.emulatorController = controller
49+
}
4050

4151
/// Handle button press
4252
func buttonPressed(_ buttonId: String) {
4353
DLOG("Delta Skin button pressed: \(buttonId)")
4454

45-
// Check if this is a menu button
46-
if buttonId.lowercased().contains("menu") {
55+
// Check for special commands
56+
let lowercasedId = buttonId.lowercased()
57+
58+
// Handle menu button
59+
if lowercasedId.contains("menu") {
4760
menuButtonPressed()
4861
return
4962
}
5063

64+
// Handle quicksave button
65+
if lowercasedId.contains("quicksave") {
66+
quicksaveButtonPressed()
67+
return
68+
}
69+
70+
// Handle quickload button
71+
if lowercasedId.contains("quickload") {
72+
quickloadButtonPressed()
73+
return
74+
}
75+
5176
// Normalize the button ID
5277
let normalizedId = buttonId.lowercased()
5378
DLOG("Normalized button ID: \(normalizedId)")
@@ -77,8 +102,11 @@ public class DeltaSkinInputHandler: ObservableObject {
77102
func buttonReleased(_ buttonId: String) {
78103
DLOG("Delta Skin button released: \(buttonId)")
79104

80-
// Skip menu button releases
81-
if buttonId.lowercased().contains("menu") {
105+
// Skip special button releases
106+
let lowercasedId = buttonId.lowercased()
107+
if lowercasedId.contains("menu") ||
108+
lowercasedId.contains("quicksave") ||
109+
lowercasedId.contains("quickload") {
82110
return
83111
}
84112

@@ -114,6 +142,52 @@ public class DeltaSkinInputHandler: ObservableObject {
114142
// Call the menu button handler if set
115143
menuButtonHandler?()
116144
}
145+
146+
/// Handle quicksave button press
147+
private func quicksaveButtonPressed() {
148+
DLOG("Quicksave button pressed")
149+
guard let controller = emulatorController else {
150+
ELOG("Cannot perform quicksave - emulatorController is nil")
151+
return
152+
}
153+
154+
// Perform quicksave asynchronously
155+
Task {
156+
do {
157+
let success = try await controller.quicksave()
158+
if success {
159+
DLOG("Quicksave completed successfully")
160+
} else {
161+
ELOG("Quicksave failed")
162+
}
163+
} catch {
164+
ELOG("Error during quicksave: \(error)")
165+
}
166+
}
167+
}
168+
169+
/// Handle quickload button press
170+
private func quickloadButtonPressed() {
171+
DLOG("Quickload button pressed")
172+
guard let controller = emulatorController else {
173+
ELOG("Cannot perform quickload - emulatorController is nil")
174+
return
175+
}
176+
177+
// Perform quickload asynchronously
178+
Task {
179+
do {
180+
let success = try await controller.quickload()
181+
if success {
182+
DLOG("Quickload completed successfully")
183+
} else {
184+
ELOG("Quickload failed")
185+
}
186+
} catch {
187+
ELOG("Error during quickload: \(error)")
188+
}
189+
}
190+
}
117191

118192
/// Handle analog stick movement
119193
func analogStickMoved(_ stickId: String, x: Float, y: Float) {

0 commit comments

Comments
 (0)