Merge branch 'experimental_features' into develop

# Conflicts:
#	Delta.xcodeproj/project.pbxproj
#	Delta/Emulation/GameViewController.swift
#	Delta/Pause Menu/PauseViewController.swift
This commit is contained in:
Riley Testut 2023-04-28 15:51:08 -05:00
commit b1a3a5076f
12 changed files with 332 additions and 1 deletions

View File

@ -34,6 +34,10 @@
/* Begin PBXBuildFile section */
1FA4ABA79AB72914FE414A61 /* libPods-Delta.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */; };
87343D7B985519A5890A61C6 /* libPods-DeltaPreviews.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E3E5A45AB20C8A87754453B /* libPods-DeltaPreviews.a */; };
AC1C991029F8B8C30020E6E4 /* ToastNotificationOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1C990F29F8B8C30020E6E4 /* ToastNotificationOptions.swift */; };
AC1C992729F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1C992629F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift */; };
ACF7E30D29F73D03000FE071 /* GameScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */; };
ACF7E30F29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF7E30E29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift */; };
BF00BEA625B758AA00C8607D /* SystemBIOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF00BEA525B758AA00C8607D /* SystemBIOS.swift */; };
BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */; };
BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */; };
@ -261,6 +265,10 @@
8ECE6641DE30D01EA30FE7F6 /* Pods-DeltaPreviews.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DeltaPreviews.release.xcconfig"; path = "Pods/Target Support Files/Pods-DeltaPreviews/Pods-DeltaPreviews.release.xcconfig"; sourceTree = "<group>"; };
A01281C7023C0041B25963BE /* Pods-DeltaPreviews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DeltaPreviews.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DeltaPreviews/Pods-DeltaPreviews.debug.xcconfig"; sourceTree = "<group>"; };
A19FF50F55441BC2B2248241 /* Pods-Delta.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.release.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.release.xcconfig"; sourceTree = "<group>"; };
AC1C990F29F8B8C30020E6E4 /* ToastNotificationOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastNotificationOptions.swift; sourceTree = "<group>"; };
AC1C992629F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+ExperimentalToasts.swift"; sourceTree = "<group>"; };
ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameScreenshots.swift; sourceTree = "<group>"; };
ACF7E30E29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHPhotoLibrary+Authorization.swift"; sourceTree = "<group>"; };
BF00BEA525B758AA00C8607D /* SystemBIOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemBIOS.swift; sourceTree = "<group>"; };
BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = openvgdb.sqlite; sourceTree = "<group>"; };
BF0418131D01E93400E85BCF /* GBADeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBADeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -535,6 +543,8 @@
D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */,
D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */,
D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */,
ACF7E30E29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift */,
AC1C992629F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1088,6 +1098,8 @@
children = (
D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */,
D5DF06DE29F326E6009E577C /* AirPlaySkins.swift */,
ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */,
AC1C990F29F8B8C30020E6E4 /* ToastNotificationOptions.swift */,
);
path = Features;
sourceTree = "<group>";
@ -1513,6 +1525,7 @@
BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */,
BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */,
BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */,
AC1C991029F8B8C30020E6E4 /* ToastNotificationOptions.swift in Sources */,
BF8A334621A4926F00A42FD4 /* GameSyncStatusViewController.swift in Sources */,
BF59427E1E09BC830051894B /* Game.swift in Sources */,
D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */,
@ -1587,11 +1600,14 @@
D5A9C00329DDED6D00A8D610 /* ExperimentalFeaturesView.swift in Sources */,
BF1F45BF21AF676F00EF9895 /* Box.swift in Sources */,
BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */,
ACF7E30F29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift in Sources */,
ACF7E30D29F73D03000FE071 /* GameScreenshots.swift in Sources */,
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */,
BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */,
BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */,
D5A98CE2284EF14B00E023E5 /* SceneDelegate.swift in Sources */,
BFE56E1923EB7BE00014FECD /* UIImage+SymbolFallback.swift in Sources */,
AC1C992729F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift in Sources */,
BF6EE5E91F7C5F860051AD6C /* _GameControllerInputMapping.swift in Sources */,
BF5942871E09BC8B0051894B /* _ControllerSkin.swift in Sources */,
BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */,

View File

@ -7,6 +7,7 @@
//
import UIKit
import Photos
import DeltaCore
import GBADeltaCore
@ -180,6 +181,14 @@ class GameViewController: DeltaCore.GameViewController
return .all
}
override var prefersStatusBarHidden: Bool {
return !ExperimentalFeatures.shared.showStatusBar.isEnabled
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
required init()
{
super.init()
@ -407,6 +416,9 @@ extension GameViewController
pauseViewController.fastForwardItem?.action = { [unowned self] item in
self.performFastForwardAction(activate: item.isSelected)
}
pauseViewController.screenshotItem?.action = { [unowned self] item in
self.performScreenshotAction()
}
pauseViewController.sustainButtonsItem?.isSelected = gameController.sustainedInputs.count > 0
pauseViewController.sustainButtonsItem?.action = { [unowned self, unowned pauseViewController] item in
@ -760,6 +772,11 @@ private extension GameViewController
try context.save()
try game.gameSaveURL.setExtendedAttribute(name: "com.rileytestut.delta.sha1Hash", value: hash)
if ExperimentalFeatures.shared.toastNotifications.gameSaveEnabled
{
self.presentExperimentalToastView(NSLocalizedString("Game Data Saved", comment: ""))
}
}
catch CocoaError.fileNoSuchFile
{
@ -878,6 +895,12 @@ extension GameViewController: SaveStatesViewControllerDelegate
saveState.modifiedDate = Date()
saveState.coreIdentifier = self.emulatorCore?.deltaCore.identifier
if ExperimentalFeatures.shared.toastNotifications.stateSaveEnabled,
saveState.type != .auto
{
self.presentExperimentalToastView(NSLocalizedString("Saved Save State", comment: ""))
}
if isRunning
{
self.resumeEmulation()
@ -925,6 +948,11 @@ extension GameViewController: SaveStatesViewControllerDelegate
{
try self.emulatorCore?.load(saveState)
}
if ExperimentalFeatures.shared.toastNotifications.stateLoadEnabled
{
self.presentExperimentalToastView(NSLocalizedString("Loaded Save State", comment: ""))
}
}
catch EmulatorCore.SaveStateError.doesNotExist
{
@ -1119,12 +1147,89 @@ extension GameViewController
{
emulatorCore.rate = emulatorCore.deltaCore.supportedRates.upperBound
}
if ExperimentalFeatures.shared.toastNotifications.fastForwardEnabled
{
self.presentExperimentalToastView(NSLocalizedString("Fast Forward Enabled", comment: ""))
}
}
else
{
emulatorCore.rate = emulatorCore.deltaCore.supportedRates.lowerBound
if ExperimentalFeatures.shared.toastNotifications.fastForwardEnabled
{
self.presentExperimentalToastView(NSLocalizedString("Fast Forward Disabled", comment: ""))
}
}
}
func performScreenshotAction()
{
guard let snapshot = self.emulatorCore?.videoManager.snapshot() else { return }
let imageScale = ExperimentalFeatures.shared.gameScreenshots.size?.rawValue ?? 1.0
let imageSize = CGSize(width: snapshot.size.width * imageScale, height: snapshot.size.height * imageScale)
let format = UIGraphicsImageRendererFormat()
format.scale = 1
let renderer = UIGraphicsImageRenderer(size: imageSize, format: format)
let scaledSnapshot = renderer.image { (context) in
context.cgContext.interpolationQuality = .none
snapshot.draw(in: CGRect(origin: .zero, size: imageSize))
}
if ExperimentalFeatures.shared.gameScreenshots.saveToPhotos
{
PHPhotoLibrary.runIfAuthorized
{
PHPhotoLibrary.saveUIImage(image: scaledSnapshot)
}
}
if ExperimentalFeatures.shared.gameScreenshots.saveToFiles
{
guard let data = scaledSnapshot.pngData() else { return }
let screenshotsDirectory = FileManager.default.documentsDirectory.appendingPathComponent("Screenshots")
do
{
try FileManager.default.createDirectory(at: screenshotsDirectory, withIntermediateDirectories: true, attributes: nil)
}
catch
{
print(error)
}
let date = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
let fileName: URL
if let game = self.game as? Game
{
let filename = game.name + "_" + dateFormatter.string(from: date) + ".png"
fileName = screenshotsDirectory.appendingPathComponent(filename)
}
else
{
fileName = screenshotsDirectory.appendingPathComponent(dateFormatter.string(from: date) + ".png")
}
do
{
try data.write(to: fileName)
}
catch
{
print(error)
}
}
self.pauseViewController?.screenshotItem?.isSelected = false
}
}
private extension GameViewController

View File

@ -22,6 +22,20 @@ struct ExperimentalFeatures: FeatureContainer
options: VariableFastForwardOptions())
var variableFastForward
@Feature(name: "Show Status Bar",
description: "Enable to show the Status Bar during gameplay.")
var showStatusBar
@Feature(name: "Game Screenshots",
description: "When enabled, a Screenshot button will appear in the Pause Menu, allowing you to save a screenshot of your game. You can choose to save the screenshot to Photos or Files.",
options: GameScreenshotsOptions())
var gameScreenshots
@Feature(name: "Toast Notifications",
description: "Show toast notifications as a confirmation for various actions, such as saving your game or loading a save state.",
options: ToastNotificationOptions())
var toastNotifications
private init()
{
self.prepareFeatures()

View File

@ -0,0 +1,54 @@
//
// GameScreenshots.swift
// Delta
//
// Created by Chris Rittenhouse on 4/24/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import DeltaFeatures
enum ScreenshotSize: Double, CaseIterable, CustomStringConvertible
{
case x5 = 5
case x4 = 4
case x3 = 3
case x2 = 2
var description: String {
if #available(iOS 15, *)
{
let formattedText = self.rawValue.formatted(.number.decimalSeparator(strategy: .automatic))
return "\(formattedText)x Size"
}
else
{
return "\(self.rawValue)x Size"
}
}
}
extension ScreenshotSize: LocalizedOptionValue
{
var localizedDescription: Text {
Text(self.description)
}
static var localizedNilDescription: Text {
Text("Original Size")
}
}
struct GameScreenshotsOptions
{
@Option(name: "Save to Files", description: "Save the screenshot to the app's directory in Files.")
var saveToFiles: Bool = true
@Option(name: "Save to Photos", description: "Save the screenshot to the Photo Library.")
var saveToPhotos: Bool = false
@Option(name: "Image Size", description: "Choose the size of screenshots. This only increases the export size, it does not increase the quality.", values: ScreenshotSize.allCases)
var size: ScreenshotSize?
}

View File

@ -0,0 +1,38 @@
//
// ToastNotificationOptions.swift
// Delta
//
// Created by Chris Rittenhouse on 4/25/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import DeltaFeatures
struct ToastNotificationOptions
{
@Option(name: "Duration", description: "Change how long toasts should be shown.", detailView: { duration in
HStack {
Text("Duration: \(duration.wrappedValue, specifier: "%.1f")s")
Slider(value: duration, in: 1...5, step: 0.5).displayInline()
}
})
var duration: Double = 1.5
@Option(name: "Game Data Saved",
description: "Show toasts when performing an in game save.")
var gameSaveEnabled: Bool = true
@Option(name: "Saved Save State",
description: "Show toasts when saving a save state.")
var stateSaveEnabled: Bool = true
@Option(name: "Loaded Save State",
description: "Show toasts when loading a save state.")
var stateLoadEnabled: Bool = true
@Option(name: "Fast Forward Toggled",
description: "Show toasts when toggling fast forward.")
var fastForwardEnabled: Bool = true
}

View File

@ -0,0 +1,25 @@
//
// GameViewController+ExperimentalToasts.swift
// Delta
//
// Created by Chris Rittenhouse on 4/26/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Roxas
extension GameViewController
{
func presentExperimentalToastView(_ text: String)
{
guard ExperimentalFeatures.shared.toastNotifications.isEnabled else { return }
DispatchQueue.main.async {
let toastView = RSTToastView(text: text, detailText: nil)
toastView.edgeOffset.vertical = 8
toastView.textLabel.textAlignment = .center
toastView.presentationEdge = .top
toastView.show(in: self.view, duration: ExperimentalFeatures.shared.toastNotifications.duration)
}
}
}

View File

@ -0,0 +1,46 @@
//
// PHPhotoLibrary+Authorization.swift
// Delta
//
// Created by Chris Rittenhouse on 4/24/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import Photos
extension PHPhotoLibrary
{
static func runIfAuthorized(code: @escaping () -> Void)
{
PHPhotoLibrary.requestAuthorization(for: .addOnly, handler: { success in
switch success
{
case .authorized:
code()
case .denied, .restricted, .notDetermined, .limited:
break
}
})
}
static func saveUIImage(image: UIImage)
{
// Save the image to the Photos app
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image)
}, completionHandler: { success, error in
if success
{
// Image saved successfully
print("Image saved to Photos app.")
}
else
{
// Error saving image
print("Error saving image: \(error?.localizedDescription ?? "Unknown error")")
}
})
}
}

View File

@ -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.screenshotItem].compactMap { $0 }
}
/// Pause Items
@ -28,6 +28,7 @@ class PauseViewController: UIViewController, PauseInfoProviding
var cheatCodesItem: MenuItem?
var fastForwardItem: MenuItem?
var sustainButtonsItem: MenuItem?
var screenshotItem: MenuItem?
/// PauseInfoProviding
var pauseText: String?
@ -160,6 +161,7 @@ private extension PauseViewController
self.cheatCodesItem = nil
self.sustainButtonsItem = nil
self.fastForwardItem = nil
self.screenshotItem = nil
guard let emulatorCore = self.emulatorCore else { return }
@ -180,6 +182,11 @@ 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.gameScreenshots.isEnabled
{
self.screenshotItem = MenuItem(text: NSLocalizedString("Screenshot", comment: ""), image: #imageLiteral(resourceName: "Screenshot"), action: { _ in })
}
if ExperimentalFeatures.shared.variableFastForward.isEnabled
{
let menu = self.makeFastForwardMenu(for: emulatorCore.game)

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "screenshot.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "screenshot@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "screenshot@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB