Compare commits

..

No commits in common. "develop" and "beta7" have entirely different histories.

2227 changed files with 4711 additions and 164498 deletions

1
.gitignore vendored
View File

@ -8,7 +8,6 @@
## Build generated ## Build generated
build/ build/
DerivedData DerivedData
.build
## Various settings ## Various settings
*.pbxuser *.pbxuser

15
.gitmodules vendored
View File

@ -19,18 +19,3 @@
[submodule "Cores/NESDeltaCore"] [submodule "Cores/NESDeltaCore"]
path = Cores/NESDeltaCore path = Cores/NESDeltaCore
url = git@github.com:rileytestut/NESDeltaCore.git url = git@github.com:rileytestut/NESDeltaCore.git
[submodule "Cores/N64DeltaCore"]
path = Cores/N64DeltaCore
url = git@github.com:rileytestut/N64DeltaCore.git
[submodule "Cores/DSDeltaCore"]
path = Cores/DSDeltaCore
url = https://github.com/rileytestut/DSDeltaCore.git
[submodule "Cores/MelonDSDeltaCore"]
path = Cores/MelonDSDeltaCore
url = https://github.com/rileytestut/MelonDSDeltaCore.git
[submodule "Cores/GPGXDeltaCore"]
path = Cores/GPGXDeltaCore
url = https://github.com/rileytestut/GPGXDeltaCore.git
[submodule "External/CheatBase"]
path = External/CheatBase
url = https://github.com/rileytestut/CheatBase.git

@ -1 +0,0 @@
Subproject commit 6c84366b3a76045782905293c9616e33f5da1a35

@ -1 +1 @@
Subproject commit c1db5f51cd455a7033801cc19dc3dbfcb6f2b42c Subproject commit 20dad9d006d8047588461f91e56d5b5757ece658

@ -1 +1 @@
Subproject commit 8ea36dff87bc1f787765de45fa8ccbcc1256a0e3 Subproject commit 21ba9850fb0812148d0826069f8cc3000d476a07

@ -1 +1 @@
Subproject commit 81f8ffba56823637706689fb5c6bc634ee4d9b32 Subproject commit 473474019bd2e3be623cd593da4c1e8c4e51fb80

@ -1 +0,0 @@
Subproject commit 18c595887a12ef23e0d54c63f83c91c99e7f4827

@ -1 +0,0 @@
Subproject commit 697ba731981824f53460f6e0193f159f71f22ba2

@ -1 +0,0 @@
Subproject commit c8816c51f82210a9c4cc62b1a7c53fa21bc705ee

@ -1 +1 @@
Subproject commit 78a092d4e795f83153e98749b5cbeb66cf812d7e Subproject commit d83e51d06020045adc4403b9d77e604cf46e34f5

@ -1 +1 @@
Subproject commit d5717291325578f64d519822aeb2be81217c67f3 Subproject commit 5f341a67ad326829d3557d01df6977d02722a5da

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1020" LastUpgradeVersion = "1010"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "NO"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
@ -14,8 +14,120 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "33C94426DAF58519DC6806AF4C44C9E7" BlueprintIdentifier = "BFADAFF719AE7BB70050CF31"
BuildableName = "libPods-Delta.a" BuildableName = "Roxas.framework"
BlueprintName = "Roxas"
ReferencedContainer = "container:External/Roxas/Roxas.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFA1C8D31ECD01C100DEA99D"
BuildableName = "Harmony.framework"
BlueprintName = "Harmony"
ReferencedContainer = "container:External/Harmony/Harmony.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFECF9F82016982D0012B9FC"
BuildableName = "Harmony_Drive.framework"
BlueprintName = "Harmony-Drive"
ReferencedContainer = "container:External/Harmony/Backends/Drive/Harmony-Drive.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF46895C1AACF36800A2586D"
BuildableName = "DeltaCore.framework"
BlueprintName = "DeltaCore"
ReferencedContainer = "container:Cores/DeltaCore/DeltaCore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF3C12F220438F3F0079A4B5"
BuildableName = "NESDeltaCore.framework"
BlueprintName = "NESDeltaCore"
ReferencedContainer = "container:Cores/NESDeltaCore/NESDeltaCore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF9F4FDB1AAD8070004C9500"
BuildableName = "SNESDeltaCore.framework"
BlueprintName = "SNESDeltaCore"
ReferencedContainer = "container:Cores/SNESDeltaCore/SNESDeltaCore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF8F2AAD1E9C879300F89F15"
BuildableName = "GBCDeltaCore.framework"
BlueprintName = "GBCDeltaCore"
ReferencedContainer = "container:Cores/GBCDeltaCore/GBCDeltaCore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFE8E9C91D010AF7009D623D"
BuildableName = "GBADeltaCore.framework"
BlueprintName = "GBADeltaCore"
ReferencedContainer = "container:Cores/GBADeltaCore/GBADeltaCore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1AC0A77D7D4F472C1693D90C57B90DD5"
BuildableName = "Pods_Delta.framework"
BlueprintName = "Pods-Delta" BlueprintName = "Pods-Delta"
ReferencedContainer = "container:Pods/Pods.xcodeproj"> ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference> </BuildableReference>
@ -29,7 +141,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1" BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
BuildableName = "Retro Game Emulator.app" BuildableName = "Delta.app"
BlueprintName = "Delta" BlueprintName = "Delta"
ReferencedContainer = "container:Delta.xcodeproj"> ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference> </BuildableReference>
@ -41,23 +153,24 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1" BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
BuildableName = "Retro Game Emulator.app" BuildableName = "Delta.app"
BlueprintName = "Delta" BlueprintName = "Delta"
ReferencedContainer = "container:Delta.xcodeproj"> ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables> <AdditionalOptions>
</Testables> </AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
@ -69,7 +182,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1" BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
BuildableName = "Retro Game Emulator.app" BuildableName = "Delta.app"
BlueprintName = "Delta" BlueprintName = "Delta"
ReferencedContainer = "container:Delta.xcodeproj"> ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference> </BuildableReference>
@ -79,10 +192,6 @@
argument = "-com.apple.CoreData.MigrationDebug 1" argument = "-com.apple.CoreData.MigrationDebug 1"
isEnabled = "YES"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument
argument = "-com.rileytestut.Harmony.Debug 1"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "-com.apple.CoreData.ConcurrencyDebug 1" argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES"> isEnabled = "YES">
@ -95,6 +204,8 @@
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@ -107,7 +218,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1" BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
BuildableName = "Retro Game Emulator.app" BuildableName = "Delta.app"
BlueprintName = "Delta" BlueprintName = "Delta"
ReferencedContainer = "container:Delta.xcodeproj"> ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference> </BuildableReference>

View File

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D5D7C1F029E60DFF00663793"
BuildableName = "libDeltaFeatures.a"
BlueprintName = "DeltaFeatures"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D5D7C1F029E60DFF00663793"
BuildableName = "libDeltaFeatures.a"
BlueprintName = "DeltaFeatures"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D539102E29E88B6B0006B350"
BuildableName = "DeltaPreviews.framework"
BlueprintName = "DeltaPreviews"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D539102E29E88B6B0006B350"
BuildableName = "DeltaPreviews.framework"
BlueprintName = "DeltaPreviews"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF6E70B925D2187800E41CD1"
BuildableName = "Systems"
BlueprintName = "Systems"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF6E70B925D2187800E41CD1"
BuildableName = "Systems"
BlueprintName = "Systems"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF14D8941DE7A512002CA1BE"
BuildableName = "mogenerator"
BlueprintName = "mogenerator"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF14D8941DE7A512002CA1BE"
BuildableName = "mogenerator"
BlueprintName = "mogenerator"
ReferencedContainer = "container:Delta.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -19,18 +19,6 @@
<FileRef <FileRef
location = "group:Cores/GBADeltaCore/GBADeltaCore.xcodeproj"> location = "group:Cores/GBADeltaCore/GBADeltaCore.xcodeproj">
</FileRef> </FileRef>
<FileRef
location = "group:Cores/N64DeltaCore/N64DeltaCore.xcodeproj">
</FileRef>
<FileRef
location = "group:Cores/MelonDSDeltaCore/MelonDSDeltaCore.xcodeproj">
</FileRef>
<FileRef
location = "group:Cores/DSDeltaCore/DSDeltaCore.xcodeproj">
</FileRef>
<FileRef
location = "group:Cores/GPGXDeltaCore">
</FileRef>
<FileRef <FileRef
location = "group:External/Harmony/Harmony.xcodeproj"> location = "group:External/Harmony/Harmony.xcodeproj">
</FileRef> </FileRef>

View File

@ -4,7 +4,5 @@
<dict> <dict>
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key> <key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<false/> <false/>
<key>PreviewsEnabled</key>
<false/>
</dict> </dict>
</plist> </plist>

View File

@ -1,34 +0,0 @@
{
"object": {
"pins": [
{
"package": "AltKit",
"repositoryURL": "https://github.com/rileytestut/AltKit.git",
"state": {
"branch": null,
"revision": "2fd376df1c79ec06a5c80cc8933da027f65b3148",
"version": null
}
},
{
"package": "DeltaCore",
"repositoryURL": "https://github.com/rileytestut/DeltaCore.git",
"state": {
"branch": "ios14",
"revision": "74d2a7a6e36035cb5730d0b0cf2456cbeb6faf0c",
"version": null
}
},
{
"package": "ZIPFoundation",
"repositoryURL": "https://github.com/weichsel/ZIPFoundation.git",
"state": {
"branch": null,
"revision": "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0",
"version": "0.9.19"
}
}
]
},
"version": 1
}

View File

@ -9,20 +9,9 @@
import UIKit import UIKit
import DeltaCore import DeltaCore
import Harmony
import AltKit
private extension CFNotificationName import Fabric
{ import Crashlytics
static let altstoreRequestAppState: CFNotificationName = CFNotificationName("com.altstore.RequestAppState.com.rileytestut.Delta" as CFString)
static let altstoreAppIsRunning: CFNotificationName = CFNotificationName("com.altstore.AppState.Running.com.rileytestut.Delta" as CFString)
}
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
appDelegate.receivedApplicationStateRequest()
}
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate class AppDelegate: UIResponder, UIApplicationDelegate
@ -36,22 +25,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate
{ {
Settings.registerDefaults() Settings.registerDefaults()
self.registerCores() System.allCases.forEach { Delta.register($0.deltaCore) }
// Must go AFTER registering cores, or else NESDeltaCore may not work correctly when not connected to debugger 🤷
Fabric.with([Crashlytics.self])
self.configureAppearance() self.configureAppearance()
// Disable system gestures that delay touches on left edge of screen
for gestureRecognizer in self.window?.gestureRecognizers ?? [] where NSStringFromClass(type(of: gestureRecognizer)).contains("GateGesture")
{
gestureRecognizer.delaysTouchesBegan = false
}
// Controllers // Controllers
ExternalGameControllerManager.shared.startMonitoring() ExternalGameControllerManager.shared.startMonitoring()
// JIT
ServerManager.shared.prepare()
// Notifications // Notifications
let center = CFNotificationCenterGetDarwinNotifyCenter()
CFNotificationCenterAddObserver(center, nil, ReceivedApplicationState, CFNotificationName.altstoreRequestAppState.rawValue, nil, .deliverImmediately)
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.databaseManagerDidStart(_:)), name: DatabaseManager.didStartNotification, object: DatabaseManager.shared) NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.databaseManagerDidStart(_:)), name: DatabaseManager.didStartNotification, object: DatabaseManager.shared)
// Deep Links // Deep Links
if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem
{ {
@ -92,58 +84,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate
} }
} }
@available(iOS 13, *)
extension AppDelegate extension AppDelegate
{ {
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
if connectingSceneSession.role == .windowExternalDisplay
{
// External Display
return UISceneConfiguration(name: "External Display", sessionRole: connectingSceneSession.role)
}
else
{
// Default Scene
return UISceneConfiguration(name: "Main", sessionRole: connectingSceneSession.role)
}
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
{
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
private extension AppDelegate
{
func registerCores()
{
#if LITE
#if BETA
Delta.register(System.nes.deltaCore)
Delta.register(System.gbc.deltaCore)
#else
Delta.register(System.nes.deltaCore)
#endif
#else
#if BETA
System.allCases.forEach { Delta.register($0.deltaCore) }
#else
System.allCases.filter { $0 != .genesis }.forEach { Delta.register($0.deltaCore) }
#endif
#endif
}
func configureAppearance() func configureAppearance()
{ {
self.window?.tintColor = UIColor.deltaPurple self.window?.tintColor = UIColor.deltaPurple
@ -170,11 +112,7 @@ extension AppDelegate
return self.importControllerSkin(at: url) return self.importControllerSkin(at: url)
} }
} }
else if url.scheme?.hasPrefix("db-") == true else
{
return DropboxService.shared.handleDropboxURL(url)
}
else if url.scheme?.lowercased() == "delta"
{ {
return self.deepLinkController.handle(.url(url)) return self.deepLinkController.handle(.url(url))
} }
@ -240,11 +178,5 @@ private extension AppDelegate
self.deepLinkController.handle(deepLink) self.deepLinkController.handle(deepLink)
} }
} }
func receivedApplicationStateRequest()
{
let center = CFNotificationCenterGetDarwinNotifyCenter()
CFNotificationCenterPostNotification(center!, CFNotificationName(CFNotificationName.altstoreAppIsRunning.rawValue), nil, nil, true)
}
} }

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="6bq-zy-UZU"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="6bq-zy-UZU">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@ -23,7 +25,7 @@
<navigationItem key="navigationItem" title="Games Database" id="rwF-kd-avR"> <navigationItem key="navigationItem" title="Games Database" id="rwF-kd-avR">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="BnB-5n-Rff"> <barButtonItem key="leftBarButtonItem" systemItem="cancel" id="BnB-5n-Rff">
<connections> <connections>
<segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindToGameCollectionViewController:" id="nzI-4n-kDg"/> <segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindFromGamesDatabaseBrowserWith:" id="zdg-Az-WwQ"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
</navigationItem> </navigationItem>
@ -39,7 +41,7 @@
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="6bq-zy-UZU" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="6bq-zy-UZU" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="uzY-vR-coL"> <navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="uzY-vR-coL">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>

View File

@ -1,9 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="dkK-ii-Bx4"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13528" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="dkK-ii-Bx4">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13526"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
@ -19,32 +23,21 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" insetsLayoutMarginsFromSafeArea="NO" image="LaunchViewC" translatesAutoresizingMaskIntoConstraints="NO" id="5XD-I3-tLg"> <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Delta" translatesAutoresizingMaskIntoConstraints="NO" id="plh-tL-LY0">
<rect key="frame" x="0.0" y="50" width="375" height="378"/> <rect key="frame" x="94" y="250" width="187.5" height="167"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Retro Game Emulator" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vhb-Xd-o6a">
<rect key="frame" x="55.5" y="448" width="264" height="33.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="28"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" alpha="0.44999998807907104" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ENJOY CLASSIC MOMENT" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wWH-Lx-U9x">
<rect key="frame" x="90.5" y="513.5" width="194.5" height="19.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstItem="5XD-I3-tLg" firstAttribute="top" secondItem="qMb-3x-uIu" secondAttribute="bottom" constant="30" id="Aiv-ac-bYx"/> <constraint firstAttribute="width" secondItem="plh-tL-LY0" secondAttribute="height" multiplier="64:57" id="8qM-L2-ASa"/>
<constraint firstItem="wWH-Lx-U9x" firstAttribute="centerX" secondItem="vhb-Xd-o6a" secondAttribute="centerX" id="DEu-U7-qVq"/> </constraints>
<constraint firstAttribute="trailing" secondItem="5XD-I3-tLg" secondAttribute="trailing" id="Gc5-y0-Vsy"/> </imageView>
<constraint firstItem="vhb-Xd-o6a" firstAttribute="top" secondItem="5XD-I3-tLg" secondAttribute="bottom" constant="20" id="Ncn-Yh-ecr"/> </subviews>
<constraint firstItem="5XD-I3-tLg" firstAttribute="leading" secondItem="8Uu-wz-ps8" secondAttribute="leading" id="SSl-CS-XOC"/> <color key="backgroundColor" white="0.14728124936421713" alpha="1" colorSpace="calibratedWhite"/>
<constraint firstItem="5XD-I3-tLg" firstAttribute="height" secondItem="8Uu-wz-ps8" secondAttribute="height" multiplier="1.7:3" id="WGx-z3-vXf"/> <constraints>
<constraint firstItem="wWH-Lx-U9x" firstAttribute="top" secondItem="vhb-Xd-o6a" secondAttribute="bottom" constant="32" id="lVY-5z-fz7"/> <constraint firstItem="plh-tL-LY0" firstAttribute="width" relation="lessThanOrEqual" secondItem="8Uu-wz-ps8" secondAttribute="width" multiplier="0.5" id="8j9-39-Y2s"/>
<constraint firstItem="vhb-Xd-o6a" firstAttribute="centerX" secondItem="8Uu-wz-ps8" secondAttribute="centerX" id="oCf-dA-bJX"/> <constraint firstItem="plh-tL-LY0" firstAttribute="centerY" secondItem="8Uu-wz-ps8" secondAttribute="centerY" id="COW-Co-NFK"/>
<constraint firstItem="plh-tL-LY0" firstAttribute="height" secondItem="8Uu-wz-ps8" secondAttribute="height" multiplier="0.5" priority="900" id="G3L-7B-xVc"/>
<constraint firstItem="plh-tL-LY0" firstAttribute="width" secondItem="8Uu-wz-ps8" secondAttribute="width" multiplier="0.5" priority="950" id="n3i-kS-7eQ"/>
<constraint firstItem="plh-tL-LY0" firstAttribute="centerX" secondItem="8Uu-wz-ps8" secondAttribute="centerX" id="sp5-Kf-N7G"/>
<constraint firstItem="plh-tL-LY0" firstAttribute="height" relation="lessThanOrEqual" secondItem="8Uu-wz-ps8" secondAttribute="height" multiplier="0.5" id="ubN-Qh-I5H"/>
</constraints> </constraints>
</view> </view>
</viewController> </viewController>
@ -54,6 +47,6 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="LaunchViewC" width="375" height="472.5"/> <image name="Delta" width="1342" height="1196"/>
</resources> </resources>
</document> </document>

View File

@ -1,104 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Games View Controller--> <!--Games-->
<scene sceneID="Cd2-Pf-cua"> <scene sceneID="Cd2-Pf-cua">
<objects> <objects>
<viewController id="jeE-WD-wXO" customClass="GamesViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="jeE-WD-wXO" customClass="GamesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides> <layoutGuides>
<viewControllerLayoutGuide type="top" id="WoX-O4-qy5"/> <viewControllerLayoutGuide type="top" id="WoX-O4-qy5"/>
<viewControllerLayoutGuide type="bottom" id="0om-QB-N5a"/> <viewControllerLayoutGuide type="bottom" id="0om-QB-N5a"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="3Bk-k3-7J9"> <view key="view" contentMode="scaleToFill" id="3Bk-k3-7J9">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="tmn-gd-5UN"> <containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="tmn-gd-5UN">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<connections> <connections>
<segue destination="tpK-ou-yEA" kind="embed" identifier="embedPageViewController" id="cjU-nW-cHY"/> <segue destination="tpK-ou-yEA" kind="embed" identifier="embedPageViewController" id="cjU-nW-cHY"/>
</connections> </connections>
</containerView> </containerView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg" translatesAutoresizingMaskIntoConstraints="NO" id="J8K-ZI-4X1">
<rect key="frame" x="0.0" y="56" width="375" height="591"/>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CxB-GP-B6S">
<rect key="frame" x="0.0" y="538" width="375" height="109"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" image="home_tab"/>
<connections>
<segue destination="xMK-Cs-fAS" kind="presentation" modalPresentationStyle="fullScreen" id="bxS-9X-3vh"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="n4H-Kw-HPj">
<rect key="frame" x="143.5" y="517" width="100" height="100"/>
<constraints>
<constraint firstAttribute="height" constant="100" id="SLR-9h-t9y"/>
<constraint firstAttribute="width" constant="100" id="Tdv-6u-nlA"/>
</constraints>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" image="addtwo"/>
<connections>
<action selector="importfilesBtn:" destination="jeE-WD-wXO" eventType="touchUpInside" id="T2w-zr-j8V"/>
</connections>
</button>
</subviews> </subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstAttribute="trailing" secondItem="CxB-GP-B6S" secondAttribute="trailing" id="5sO-AB-YB5"/>
<constraint firstAttribute="trailing" secondItem="J8K-ZI-4X1" secondAttribute="trailing" id="7MY-qA-ANn"/>
<constraint firstAttribute="trailing" secondItem="tmn-gd-5UN" secondAttribute="trailing" id="9Rq-HM-vqk"/> <constraint firstAttribute="trailing" secondItem="tmn-gd-5UN" secondAttribute="trailing" id="9Rq-HM-vqk"/>
<constraint firstItem="J8K-ZI-4X1" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="9kW-o9-BMv"/>
<constraint firstItem="CxB-GP-B6S" firstAttribute="centerX" secondItem="3Bk-k3-7J9" secondAttribute="centerX" id="Dbp-v1-mUp"/>
<constraint firstItem="n4H-Kw-HPj" firstAttribute="centerX" secondItem="3Bk-k3-7J9" secondAttribute="centerX" constant="6" id="H9T-X7-RK8"/>
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="n4H-Kw-HPj" secondAttribute="bottom" constant="30" id="QHB-jA-R0s"/>
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="CxB-GP-B6S" secondAttribute="bottom" id="QzJ-Kr-VZk"/>
<constraint firstItem="CxB-GP-B6S" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="axc-ed-3zE"/>
<constraint firstItem="tmn-gd-5UN" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="f1f-sa-dBA"/> <constraint firstItem="tmn-gd-5UN" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="f1f-sa-dBA"/>
<constraint firstAttribute="bottom" secondItem="tmn-gd-5UN" secondAttribute="bottom" id="ifM-Wa-u9y"/> <constraint firstAttribute="bottom" secondItem="tmn-gd-5UN" secondAttribute="bottom" id="ifM-Wa-u9y"/>
<constraint firstItem="J8K-ZI-4X1" firstAttribute="top" secondItem="WoX-O4-qy5" secondAttribute="bottom" id="jaI-AF-tpn"/>
<constraint firstItem="tmn-gd-5UN" firstAttribute="top" secondItem="3Bk-k3-7J9" secondAttribute="top" id="nhS-aC-rUR"/> <constraint firstItem="tmn-gd-5UN" firstAttribute="top" secondItem="3Bk-k3-7J9" secondAttribute="top" id="nhS-aC-rUR"/>
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="J8K-ZI-4X1" secondAttribute="bottom" id="tvh-Sd-zA1"/>
</constraints> </constraints>
</view> </view>
<navigationItem key="navigationItem" id="pFk-as-3k4"> <navigationItem key="navigationItem" title="Games" id="pFk-as-3k4">
<barButtonItem key="leftBarButtonItem" image="home" id="2gg-lC-FhX"/> <barButtonItem key="leftBarButtonItem" image="SettingsButton" id="2gg-lC-FhX">
<barButtonItem key="rightBarButtonItem" style="plain" id="FeA-O5-xd2"> <connections>
<segue destination="xMK-Cs-fAS" kind="presentation" id="uN5-PN-7FK"/>
</connections>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem systemItem="add" id="FeA-O5-xd2">
<connections> <connections>
<action selector="importFiles" destination="jeE-WD-wXO" id="A1s-kE-NkM"/> <action selector="importFiles" destination="jeE-WD-wXO" id="A1s-kE-NkM"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
</navigationItem> <barButtonItem title="Sync" id="6Fp-px-Dow">
<connections> <connections>
<outlet property="importButton" destination="FeA-O5-xd2" id="A44-3S-Okz"/> <action selector="sync" destination="jeE-WD-wXO" id="FFs-LZ-GEr"/>
</connections> </connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1036" y="1001.649175412294"/> <point key="canvasLocation" x="1036" y="1002"/>
</scene> </scene>
<!--Game Collection View Controller--> <!--Game Collection View Controller-->
<scene sceneID="qNA-NP-TiF"> <scene sceneID="qNA-NP-TiF">
<objects> <objects>
<collectionViewController storyboardIdentifier="gameCollectionViewController" clearsSelectionOnViewWillAppear="NO" id="kqu-75-owz" customClass="GameCollectionViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController"> <collectionViewController storyboardIdentifier="gameCollectionViewController" clearsSelectionOnViewWillAppear="NO" id="kqu-75-owz" customClass="GameCollectionViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" indicatorStyle="white" dataMode="prototypes" id="OIq-Z8-kxO"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" indicatorStyle="white" dataMode="prototypes" id="OIq-Z8-kxO">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="10" id="NKN-dd-bTh" customClass="GridCollectionViewLayout" customModule="Retro_Game_Emulator" customModuleProvider="target"> <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="10" id="NKN-dd-bTh" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
<size key="itemSize" width="100" height="100"/> <size key="itemSize" width="100" height="100"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/> <size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/> <size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="20" maxX="0.0" maxY="20"/> <inset key="sectionInset" minX="0.0" minY="20" maxX="0.0" maxY="20"/>
</collectionViewFlowLayout> </collectionViewFlowLayout>
<cells> <cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="ioT-sh-j8y" customClass="GridCollectionViewCell" customModule="Retro_Game_Emulator" customModuleProvider="target"> <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="ioT-sh-j8y" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="100" height="100"/> <rect key="frame" x="0.0" y="20" width="100" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
@ -114,9 +90,7 @@
</collectionView> </collectionView>
<connections> <connections>
<segue destination="X2o-q6-XD5" kind="unwind" identifier="unwindFromGames" unwindAction="unwindFromGamesViewControllerWith:" id="k8C-Xn-maU"/> <segue destination="X2o-q6-XD5" kind="unwind" identifier="unwindFromGames" unwindAction="unwindFromGamesViewControllerWith:" id="k8C-Xn-maU"/>
<segue destination="MPk-bF-nkj" kind="presentation" identifier="saveStates" customClass="SaveStatesStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="1Xp-2J-0cq"/> <segue destination="MPk-bF-nkj" kind="presentation" identifier="saveStates" customClass="SaveStatesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="1Xp-2J-0cq"/>
<segue destination="qdE-gb-V2e" kind="presentation" identifier="preferredControllerSkins" id="i6y-cP-3WM"/>
<segue destination="V2x-v0-jWm" kind="presentation" identifier="showDSSettings" id="kuV-tY-Y0B"/>
</connections> </connections>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bW1-t8-idm" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="bW1-t8-idm" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -127,7 +101,7 @@
<!--Launch View Controller--> <!--Launch View Controller-->
<scene sceneID="p7y-IT-nlb"> <scene sceneID="p7y-IT-nlb">
<objects> <objects>
<viewController id="SPq-Bk-fQl" customClass="LaunchViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="SPq-Bk-fQl" customClass="LaunchViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides> <layoutGuides>
<viewControllerLayoutGuide type="top" id="Qap-U8-zpQ"/> <viewControllerLayoutGuide type="top" id="Qap-U8-zpQ"/>
<viewControllerLayoutGuide type="bottom" id="dca-QO-wba"/> <viewControllerLayoutGuide type="bottom" id="dca-QO-wba"/>
@ -146,38 +120,12 @@
<containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vf5-Iy-lAb" userLabel="Launch Screen"> <containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vf5-Iy-lAb" userLabel="Launch Screen">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections> <connections>
<segue destination="ibA-aC-X3M" kind="embed" id="fsv-uf-AOE"/> <segue destination="ibA-aC-X3M" kind="embed" id="fsv-uf-AOE"/>
</connections> </connections>
</containerView> </containerView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Mgn-IF-ax7">
<rect key="frame" x="187.5" y="448" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="28"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" alpha="0.44999998807907104" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wnW-MB-udh">
<rect key="frame" x="187.5" y="480" width="0.0" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="50O-wR-1hq">
<rect key="frame" x="0.0" y="50" width="375" height="378"/>
</imageView>
</subviews> </subviews>
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="50O-wR-1hq" secondAttribute="trailing" id="2Xh-ZZ-Dbc"/>
<constraint firstItem="50O-wR-1hq" firstAttribute="leading" secondItem="8jv-0a-ItC" secondAttribute="leading" id="6rJ-Cz-C3Z"/>
<constraint firstItem="Mgn-IF-ax7" firstAttribute="centerX" secondItem="8jv-0a-ItC" secondAttribute="centerX" id="8kV-gZ-4TW"/>
<constraint firstItem="50O-wR-1hq" firstAttribute="height" secondItem="8jv-0a-ItC" secondAttribute="height" multiplier="1.7:3" id="FTt-Ru-HhA"/>
<constraint firstItem="wnW-MB-udh" firstAttribute="top" secondItem="Mgn-IF-ax7" secondAttribute="bottom" constant="32" id="IAR-im-kU7"/>
<constraint firstItem="50O-wR-1hq" firstAttribute="top" secondItem="Qap-U8-zpQ" secondAttribute="bottom" constant="30" id="bo1-Am-BdP"/>
<constraint firstItem="wnW-MB-udh" firstAttribute="centerX" secondItem="8jv-0a-ItC" secondAttribute="centerX" id="djF-KX-NgM"/>
<constraint firstItem="Mgn-IF-ax7" firstAttribute="top" secondItem="50O-wR-1hq" secondAttribute="bottom" constant="20" id="igk-jf-lgM"/>
</constraints>
</view> </view>
<connections> <connections>
<outlet property="gameViewContainerView" destination="oBZ-xU-jeC" id="jMI-iF-JlU"/> <outlet property="gameViewContainerView" destination="oBZ-xU-jeC" id="jMI-iF-JlU"/>
@ -198,7 +146,7 @@
<!--Game View Controller--> <!--Game View Controller-->
<scene sceneID="ASV-Uk-0aP"> <scene sceneID="ASV-Uk-0aP">
<objects> <objects>
<viewController id="yhz-fF-D91" customClass="GameViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController"> <viewController id="yhz-fF-D91" customClass="GameViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides> <layoutGuides>
<viewControllerLayoutGuide type="top" id="ItC-Bu-WRI"/> <viewControllerLayoutGuide type="top" id="ItC-Bu-WRI"/>
<viewControllerLayoutGuide type="bottom" id="g58-HO-6L5"/> <viewControllerLayoutGuide type="bottom" id="g58-HO-6L5"/>
@ -206,23 +154,12 @@
<view key="view" contentMode="scaleToFill" id="skW-1S-YD4"> <view key="view" contentMode="scaleToFill" id="skW-1S-YD4">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg" translatesAutoresizingMaskIntoConstraints="NO" id="0b6-zE-QS0">
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.02214059979" green="0.092763282360000004" blue="0.18196243049999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="0b6-zE-QS0" secondAttribute="trailing" id="5n6-uA-WLa"/>
<constraint firstItem="0b6-zE-QS0" firstAttribute="top" secondItem="ItC-Bu-WRI" secondAttribute="bottom" id="DTE-cg-9BW"/>
<constraint firstItem="0b6-zE-QS0" firstAttribute="leading" secondItem="skW-1S-YD4" secondAttribute="leading" id="Ej3-6D-wcR"/>
<constraint firstItem="g58-HO-6L5" firstAttribute="top" secondItem="0b6-zE-QS0" secondAttribute="bottom" id="ozO-Ni-qpr"/>
</constraints>
</view> </view>
<connections> <connections>
<segue destination="Yrw-9v-Pcr" kind="presentation" identifier="pause" customClass="PauseStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="FLq-Zt-HDv"/> <segue destination="Yrw-9v-Pcr" kind="presentation" identifier="pause" customClass="PauseStoryboardSegue" customModule="Delta" customModuleProvider="target" id="FLq-Zt-HDv"/>
<segue destination="wKV-3d-NIY" kind="presentation" identifier="showGamesViewController" customClass="GamesStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="Tey-6Z-UHp"/> <segue destination="wKV-3d-NIY" kind="presentation" identifier="showGamesViewController" customClass="GamesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="Tey-6Z-UHp"/>
<segue destination="wKV-3d-NIY" kind="presentation" identifier="showInitialGamesViewController" customClass="InitialGamesStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="eR2-0c-0Rv"/> <segue destination="wKV-3d-NIY" kind="presentation" identifier="showInitialGamesViewController" customClass="InitialGamesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="eR2-0c-0Rv"/>
</connections> </connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="gxI-00-NlJ" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="gxI-00-NlJ" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -240,12 +177,10 @@
<!--Settings--> <!--Settings-->
<scene sceneID="L3X-MM-hJL"> <scene sceneID="L3X-MM-hJL">
<objects> <objects>
<viewControllerPlaceholder storyboardName="Settings" id="xMK-Cs-fAS" sceneMemberID="viewController"> <viewControllerPlaceholder storyboardName="Settings" id="xMK-Cs-fAS" sceneMemberID="viewController"/>
<navigationItem key="navigationItem" id="7yR-QM-2bX"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="2N5-3k-beA" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="2N5-3k-beA" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1578" y="774"/> <point key="canvasLocation" x="1036" y="605"/>
</scene> </scene>
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="zJI-sC-1sm"> <scene sceneID="zJI-sC-1sm">
@ -254,14 +189,11 @@
<toolbarItems/> <toolbarItems/>
<nil key="simulatedBottomBarMetrics"/> <nil key="simulatedBottomBarMetrics"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="wj9-1e-eev"> <navigationBar key="navigationBar" contentMode="scaleToFill" id="wj9-1e-eev">
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/> <rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="barTintColor" red="0.032843004910000001" green="0.076728828250000006" blue="0.13726195690000001" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="M4r-sO-G4H"> <toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="M4r-sO-G4H">
<rect key="frame" x="0.0" y="-20" width="0.0" height="0.0"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</toolbar> </toolbar>
<connections> <connections>
@ -283,16 +215,16 @@
<!--Navigation Controller--> <!--Navigation Controller-->
<scene sceneID="nR0-Va-AI1"> <scene sceneID="nR0-Va-AI1">
<objects> <objects>
<navigationController storyboardIdentifier="saveStatesNavigationController" automaticallyAdjustsScrollViewInsets="NO" modalPresentationStyle="fullScreen" id="MPk-bF-nkj" sceneMemberID="viewController"> <navigationController storyboardIdentifier="saveStatesNavigationController" automaticallyAdjustsScrollViewInsets="NO" id="MPk-bF-nkj" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="twH-3X-6DV"> <navigationBar key="navigationBar" contentMode="scaleToFill" id="twH-3X-6DV">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
<connections> <connections>
<segue destination="Eae-Qk-9MI" kind="relationship" relationship="rootViewController" id="1Jh-Zf-ntp"/> <segue destination="Eae-Qk-9MI" kind="relationship" relationship="rootViewController" id="1Jh-Zf-ntp"/>
<segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" unwindAction="unwindToGameCollectionViewController:" id="dwO-iv-XDr"/> <segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Delta" customModuleProvider="target" unwindAction="unwindFromSaveStatesViewControllerWith:" id="dwO-iv-XDr"/>
</connections> </connections>
</navigationController> </navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="htj-tq-2KP" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="htj-tq-2KP" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -300,16 +232,6 @@
</objects> </objects>
<point key="canvasLocation" x="2652" y="1718"/> <point key="canvasLocation" x="2652" y="1718"/>
</scene> </scene>
<!--preferredControllerSkins-->
<scene sceneID="aKY-Ld-et6">
<objects>
<viewControllerPlaceholder storyboardName="Settings" referencedIdentifier="preferredControllerSkins" id="dbc-pQ-iun" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="xth-MV-SHp"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="za6-AO-ZFe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3405" y="2394"/>
</scene>
<!--saveStatesViewController--> <!--saveStatesViewController-->
<scene sceneID="f1R-Kb-FOU"> <scene sceneID="f1R-Kb-FOU">
<objects> <objects>
@ -320,46 +242,11 @@
</objects> </objects>
<point key="canvasLocation" x="3409" y="1716"/> <point key="canvasLocation" x="3409" y="1716"/>
</scene> </scene>
<!--Navigation Controller-->
<scene sceneID="eMh-8N-ZGA">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="qdE-gb-V2e" sceneMemberID="viewController">
<toolbarItems/>
<navigationItem key="navigationItem" id="Dg6-He-v5H"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="35T-4Q-Mmp">
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="dbc-pQ-iun" kind="relationship" relationship="rootViewController" id="oRb-B6-c0J"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="XmB-QY-yA3" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2652" y="2394"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="OW2-zT-pbF">
<objects>
<navigationController id="V2x-v0-jWm" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="pjb-4I-yar">
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Jo9-gl-p5p" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2652" y="3085"/>
</scene>
</scenes> </scenes>
<resources>
<image name="SettingsButton" width="22" height="22"/>
</resources>
<inferredMetricsTieBreakers> <inferredMetricsTieBreakers>
<segue reference="Tey-6Z-UHp"/> <segue reference="Tey-6Z-UHp"/>
</inferredMetricsTieBreakers> </inferredMetricsTieBreakers>
<resources>
<image name="addtwo" width="519" height="519"/>
<image name="bg" width="375" height="812"/>
<image name="home" width="92" height="37"/>
<image name="home_tab" width="384.5" height="109"/>
</resources>
</document> </document>

View File

@ -1,12 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<customFonts key="customFonts">
<array key="Menlo.ttc">
<string>Menlo-Regular</string>
</array>
</customFonts>
<scenes> <scenes>
<!--Pause View Controller--> <!--Pause View Controller-->
<scene sceneID="Wst-Dv-TjM"> <scene sceneID="Wst-Dv-TjM">
@ -33,7 +40,6 @@
</connections> </connections>
</containerView> </containerView>
</subviews> </subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="calibratedRGB"/>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="rqN-NB-jbb" secondAttribute="bottom" id="3XJ-2M-uVD"/> <constraint firstAttribute="bottom" secondItem="rqN-NB-jbb" secondAttribute="bottom" id="3XJ-2M-uVD"/>
<constraint firstAttribute="trailing" secondItem="rqN-NB-jbb" secondAttribute="trailing" id="NQ7-cS-8T5"/> <constraint firstAttribute="trailing" secondItem="rqN-NB-jbb" secondAttribute="trailing" id="NQ7-cS-8T5"/>
@ -68,7 +74,7 @@
<navigationController id="sWv-Ky-VGs" sceneMemberID="viewController"> <navigationController id="sWv-Ky-VGs" sceneMemberID="viewController">
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/> <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" barStyle="black" id="Snh-Z0-9kC"> <navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" barStyle="black" id="Snh-Z0-9kC">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<connections> <connections>
@ -132,7 +138,7 @@
<objects> <objects>
<collectionViewController storyboardIdentifier="saveStatesViewController" id="OOk-k7-INg" customClass="SaveStatesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController"> <collectionViewController storyboardIdentifier="saveStatesViewController" id="OOk-k7-INg" customClass="SaveStatesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="XgF-OF-CVf"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="XgF-OF-CVf">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="tvW-q1-PD8" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target"> <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="tvW-q1-PD8" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
@ -161,28 +167,13 @@
</connections> </connections>
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="Save State" id="BoG-k2-aR2"> <navigationItem key="navigationItem" title="Save State" id="BoG-k2-aR2">
<rightBarButtonItems> <barButtonItem key="rightBarButtonItem" systemItem="add" id="lKg-Ks-hWN">
<barButtonItem systemItem="add" id="lKg-Ks-hWN">
<connections> <connections>
<action selector="addSaveState" destination="OOk-k7-INg" id="xY2-94-EOr"/> <action selector="addSaveState" destination="OOk-k7-INg" id="xY2-94-EOr"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
<barButtonItem style="plain" id="has-I3-HDZ">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="y2a-9f-EFz">
<rect key="frame" x="288.5" y="13" width="30" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="▼"/>
<connections>
<action selector="changeSortOrder:" destination="OOk-k7-INg" eventType="primaryActionTriggered" id="qQn-uw-SN1"/>
</connections>
</button>
</barButtonItem>
</rightBarButtonItems>
</navigationItem> </navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/> <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<connections>
<outlet property="sortButton" destination="y2a-9f-EFz" id="Zbo-Q0-bVL"/>
</connections>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cL5-DH-K3b" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="cL5-DH-K3b" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
@ -193,7 +184,7 @@
<objects> <objects>
<tableViewController id="wb8-5o-1jE" customClass="CheatsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController"> <tableViewController id="wb8-5o-1jE" customClass="CheatsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" id="f5S-hV-1yV"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" id="f5S-hV-1yV">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes> <prototypes>
@ -201,14 +192,14 @@
<rect key="frame" x="0.0" y="28" width="375" height="44"/> <rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="e8g-ZW-5lQ" id="AHE-Jk-ULE"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="e8g-ZW-5lQ" id="AHE-Jk-ULE">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="emc-gw-KkJ" userLabel="Selected Background View"> <visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="emc-gw-KkJ" userLabel="Selected Background View">
<rect key="frame" x="0.0" y="0.0" width="375" height="45"/> <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="9bA-Tg-Bko"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" id="9bA-Tg-Bko">
<rect key="frame" x="0.0" y="0.0" width="375" height="45"/> <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view> </view>
@ -217,9 +208,9 @@
</vibrancyEffect> </vibrancyEffect>
</visualEffectView> </visualEffectView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="R4A-9d-DGb"> <visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="R4A-9d-DGb">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="EdX-fU-x54"> <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="EdX-fU-x54">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view> </view>
<vibrancyEffect> <vibrancyEffect>
@ -267,7 +258,7 @@
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="BV9-ff-x83"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="BV9-ff-x83">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<sections> <sections>
<tableViewSection headerTitle="Name" id="QT6-DZ-g70"> <tableViewSection headerTitle="Name" id="QT6-DZ-g70">
<cells> <cells>
@ -275,11 +266,11 @@
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/> <rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="ZeC-rg-QFa" id="UIF-fK-ApW"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="ZeC-rg-QFa" id="UIF-fK-ApW">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Cheat Name" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="DD1-X0-hg7"> <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Cheat Name" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="DD1-X0-hg7">
<rect key="frame" x="16" y="0.0" width="343" height="44"/> <rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words" autocorrectionType="no" returnKeyType="done"/> <textInputTraits key="textInputTraits" autocapitalizationType="words" autocorrectionType="no" returnKeyType="done"/>
<connections> <connections>
@ -304,11 +295,11 @@
<rect key="frame" x="0.0" y="163" width="375" height="44"/> <rect key="frame" x="0.0" y="163" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Tst-zn-e04" id="gwV-zS-RWQ"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Tst-zn-e04" id="gwV-zS-RWQ">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="xrD-ue-96Q"> <segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="xrD-ue-96Q">
<rect key="frame" x="16" y="6.5" width="343" height="32"/> <rect key="frame" x="16" y="8" width="343" height="29"/>
<segments> <segments>
<segment title="First"/> <segment title="First"/>
<segment title="Second"/> <segment title="Second"/>
@ -333,12 +324,12 @@
<rect key="frame" x="0.0" y="282.5" width="375" height="210"/> <rect key="frame" x="0.0" y="282.5" width="375" height="210"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="xxc-cz-sb7" id="agU-SE-fNa"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="xxc-cz-sb7" id="agU-SE-fNa">
<rect key="frame" x="0.0" y="0.0" width="375" height="210"/> <rect key="frame" x="0.0" y="0.0" width="375" height="209.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="XXXXXXXX YYYYYYYY" translatesAutoresizingMaskIntoConstraints="NO" id="a17-LB-QXD" customClass="CheatTextView" customModule="Delta" customModuleProvider="target"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="XXXXXXXX YYYYYYYY" translatesAutoresizingMaskIntoConstraints="NO" id="a17-LB-QXD" customClass="CheatTextView" customModule="Delta" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="210"/> <rect key="frame" x="0.0" y="0.0" width="375" height="209.5"/>
<color key="textColor" systemColor="labelColor"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="26"/> <fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="26"/>
<textInputTraits key="textInputTraits" autocapitalizationType="allCharacters" autocorrectionType="no" spellCheckingType="no" returnKeyType="done"/> <textInputTraits key="textInputTraits" autocapitalizationType="allCharacters" autocorrectionType="no" spellCheckingType="no" returnKeyType="done"/>
<connections> <connections>
@ -399,9 +390,4 @@
<point key="canvasLocation" x="2385" y="1377"/> <point key="canvasLocation" x="2385" y="1377"/>
</scene> </scene>
</scenes> </scenes>
<resources>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document> </document>

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,8 @@ extension Action
case destructive case destructive
case selected case selected
var alertActionStyle: UIAlertAction.Style { var alertActionStyle: UIAlertAction.Style
{
switch self switch self
{ {
case .default, .selected: return .default case .default, .selected: return .default
@ -26,7 +27,8 @@ extension Action
} }
} }
var previewActionStyle: UIPreviewAction.Style? { var previewActionStyle: UIPreviewAction.Style?
{
switch self switch self
{ {
case .default: return .default case .default: return .default
@ -38,40 +40,11 @@ extension Action
} }
} }
@available(iOS 13, *)
extension Action.Style
{
var menuAttributes: UIMenuElement.Attributes {
switch self
{
case .default, .cancel, .selected: return []
case .destructive: return .destructive
}
}
var menuState: UIMenuElement.State {
switch self
{
case .default, .cancel, .destructive: return .off
case .selected: return .on
}
}
}
struct Action struct Action
{ {
var title: String let title: String
var style: Style let style: Style
var image: UIImage? = nil let action: ((Action) -> Void)?
var action: ((Action) -> Void)?
init(title: String, style: Style = .default, image: UIImage? = nil, action: ((Action) -> Void)? = nil)
{
self.title = title
self.style = style
self.image = image
self.action = action
}
} }
extension UIAlertAction extension UIAlertAction
@ -109,19 +82,6 @@ extension UIAlertController
} }
} }
@available(iOS 13.0, *)
extension UIAction
{
convenience init?(_ action: Action)
{
guard action.style != .cancel else { return nil }
self.init(title: action.title, image: action.image, attributes: action.style.menuAttributes, state: action.style.menuState) { _ in
action.action?(action)
}
}
}
extension RangeReplaceableCollection where Iterator.Element == Action extension RangeReplaceableCollection where Iterator.Element == Action
{ {
var alertActions: [UIAlertAction] { var alertActions: [UIAlertAction] {
@ -133,10 +93,4 @@ extension RangeReplaceableCollection where Iterator.Element == Action
let actions = self.compactMap { UIPreviewAction($0) } let actions = self.compactMap { UIPreviewAction($0) }
return actions return actions
} }
@available(iOS 13.0, *)
var menuActions: [UIAction] {
let actions = self.compactMap { UIAction($0) }
return actions
}
} }

View File

@ -39,7 +39,7 @@ class GridCollectionViewCell: UICollectionViewCell
} }
} }
var maximumImageSize: CGSize = CGSize(width: 150, height: 120) { var maximumImageSize: CGSize = CGSize(width: 100, height: 100) {
didSet { didSet {
self.updateMaximumImageSize() self.updateMaximumImageSize()
} }
@ -109,15 +109,13 @@ class GridCollectionViewCell: UICollectionViewCell
// Image View // Image View
self.imageView.translatesAutoresizingMaskIntoConstraints = false self.imageView.translatesAutoresizingMaskIntoConstraints = false
self.imageView.topAnchor.constraint(equalTo: self.vibrancyView.topAnchor).isActive = true self.imageView.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true
self.imageView.centerXAnchor.constraint(equalTo: self.vibrancyView.centerXAnchor).isActive = true self.imageView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true
self.imageViewWidthConstraint = self.imageView.widthAnchor.constraint(equalToConstant: 150) self.imageViewWidthConstraint = self.imageView.widthAnchor.constraint(equalToConstant: self.maximumImageSize.width)
self.imageViewWidthConstraint.isActive = true self.imageViewWidthConstraint.isActive = true
self.imageViewHeightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: 120) self.imageViewHeightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: self.maximumImageSize.height)
self.imageViewHeightConstraint.priority = UILayoutPriority(999) // Fixes "Unable to simultaneously satisfy constraints" runtime error when inserting new grid row.
self.imageViewHeightConstraint.isActive = true self.imageViewHeightConstraint.isActive = true
@ -183,15 +181,10 @@ private extension GridCollectionViewCell
{ {
func updateMaximumImageSize() func updateMaximumImageSize()
{ {
self.imageViewWidthConstraint.constant = 150 self.imageViewWidthConstraint.constant = self.maximumImageSize.width
self.imageViewHeightConstraint.constant = 120 self.imageViewHeightConstraint.constant = self.maximumImageSize.height
self.textLabelVerticalSpacingConstraint.constant = 8 self.textLabelVerticalSpacingConstraint.constant = 8
self.textLabelFocusedVerticalSpacingConstraint?.constant = self.maximumImageSize.height / 10.0 self.textLabelFocusedVerticalSpacingConstraint?.constant = self.maximumImageSize.height / 10.0
// textLabel 30
// let textLabelFixedHeight: CGFloat = 30
//
// self.textLabelVerticalSpacingConstraint.constant = textLabelFixedHeight
// self.textLabelFocusedVerticalSpacingConstraint?.constant = textLabelFixedHeight / 50
} }
} }

View File

@ -50,8 +50,6 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
return interitemSpacing return interitemSpacing
} }
private var cachedCellLayoutAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
override var estimatedItemSize: CGSize { override var estimatedItemSize: CGSize {
didSet { didSet {
fatalError("GridCollectionViewLayout does not support self-sizing cells.") fatalError("GridCollectionViewLayout does not support self-sizing cells.")
@ -66,18 +64,6 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
self.sectionInset.right = self.interitemSpacing + self.contentInset.right self.sectionInset.right = self.interitemSpacing + self.contentInset.right
} }
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext)
{
super.invalidateLayout(with: context)
if let context = context as? UICollectionViewFlowLayoutInvalidationContext,
context.invalidateFlowLayoutAttributes || context.invalidateFlowLayoutDelegateMetrics || context.invalidateEverything
{
// Clear layout cache to prevent crashing due to returning outdated layout attributes.
self.cachedCellLayoutAttributes = [:]
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{ {
let layoutAttributes = super.layoutAttributesForElements(in: rect)?.map({ $0.copy() }) as! [UICollectionViewLayoutAttributes] let layoutAttributes = super.layoutAttributesForElements(in: rect)?.map({ $0.copy() }) as! [UICollectionViewLayoutAttributes]
@ -151,24 +137,9 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
} }
} }
for attributes in layoutAttributes where attributes.representedElementCategory == .cell
{
// Update cached attributes for layoutAttributesForItem(at:)
self.cachedCellLayoutAttributes[attributes.indexPath] = attributes
}
return layoutAttributes return layoutAttributes
} }
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
{
if let cachedAttributes = self.cachedCellLayoutAttributes[indexPath]
{
return cachedAttributes
}
return super.layoutAttributesForItem(at: indexPath)
}
} }
private extension GridCollectionViewLayout private extension GridCollectionViewLayout

View File

@ -21,8 +21,7 @@ class ListMenuViewController: UITableViewController
override var preferredContentSize: CGSize { override var preferredContentSize: CGSize {
get { get {
// Don't include navigation bar height in calculation (as of iOS 13). let navigationBarHeight = self.navigationController?.navigationBar.bounds.height ?? 0.0
let navigationBarHeight = 0.0 // self.navigationController?.navigationBar.bounds.height ?? 0.0
return CGSize(width: 0, height: (self.tableView.rowHeight * CGFloat(self.items.count)) + navigationBarHeight) return CGSize(width: 0, height: (self.tableView.rowHeight * CGFloat(self.items.count)) + navigationBarHeight)
} }
set {} set {}

View File

@ -17,7 +17,7 @@ extension UINavigationBar
} }
// Make "copy" of self. // Make "copy" of self.
let navigationBar = UINavigationBar(frame: self.bounds) // Use self.bounds to avoid "Unable to simultaneously satisfy constraints" runtime error. let navigationBar = UINavigationBar(frame: .zero)
navigationBar.barStyle = self.barStyle navigationBar.barStyle = self.barStyle
// Set item with title so we can retrieve default text attributes. // Set item with title so we can retrieve default text attributes.
@ -38,33 +38,11 @@ extension UINavigationBar
private var _defaultTitleTextAttributes: [NSAttributedString.Key: Any]? { private var _defaultTitleTextAttributes: [NSAttributedString.Key: Any]? {
guard self.titleTextAttributes == nil else { return self.titleTextAttributes } guard self.titleTextAttributes == nil else { return self.titleTextAttributes }
guard let contentView = self.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("ContentView") || NSStringFromClass(type(of: $0)).contains("ItemView") }) guard
let contentView = self.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("ContentView") || NSStringFromClass(type(of: $0)).contains("ItemView") }),
let titleLabel = contentView.subviews.first(where: { $0 is UILabel }) as? UILabel
else { return nil } else { return nil }
let containerView: UIView
//TODO: Recursively search all subviews for title UILabel instead of hardcoded OS version-specific hierarchy traversals...
if #available(iOS 16, *)
{
guard let titleControl = contentView.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("Title") }) else { return nil }
if #available(iOS 17, *)
{
guard let view = titleControl.subviews.first else { return nil }
containerView = view
}
else
{
containerView = titleControl
}
}
else
{
containerView = contentView
}
guard let titleLabel = containerView.subviews.first(where: { $0 is UILabel }) as? UILabel else { return nil }
let textAttributes = titleLabel.attributedText?.attributes(at: 0, effectiveRange: nil) let textAttributes = titleLabel.attributedText?.attributes(at: 0, effectiveRange: nil)
return textAttributes return textAttributes
} }
@ -85,8 +63,6 @@ class PopoverMenuButton: UIControl
private let arrowLabel: UILabel private let arrowLabel: UILabel
private let stackView: UIStackView private let stackView: UIStackView
private var _didLayoutSubviews = false
private var parentNavigationBar: UINavigationBar? { private var parentNavigationBar: UINavigationBar? {
guard let navigationController = self.parentViewController as? UINavigationController ?? self.parentViewController?.navigationController else { return nil } guard let navigationController = self.parentViewController as? UINavigationController ?? self.parentViewController?.navigationController else { return nil }
guard self.isDescendant(of: navigationController.navigationBar) else { return nil } guard self.isDescendant(of: navigationController.navigationBar) else { return nil }
@ -128,21 +104,6 @@ class PopoverMenuButton: UIControl
{ {
self.updateTextAttributes() self.updateTextAttributes()
} }
override func layoutSubviews()
{
super.layoutSubviews()
if !_didLayoutSubviews
{
_didLayoutSubviews = true
// didMoveToSuperview() can be too early to accurately
// update text attributes, so ensure we also update
// during first layoutSubviews() call.
self.updateTextAttributes()
}
}
} }
private extension PopoverMenuButton private extension PopoverMenuButton

View File

@ -1,143 +0,0 @@
//
// CheatBase.swift
// Delta
//
// Created by Riley Testut on 1/17/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
import SQLite
import Roxas
private extension UserDefaults
{
@NSManaged var previousCheatBaseVersion: Int
}
extension ExpressionType
{
static var cheatID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("cheatID")
}
static var cheatName: SQLite.Expression<String> {
return SQLite.Expression<String>("cheatName")
}
static var cheatDescription: SQLite.Expression<String?> {
return SQLite.Expression<String?>("cheatDescription")
}
static var cheatCode: SQLite.Expression<String> {
return SQLite.Expression<String>("cheatCode")
}
static var cheatDeviceID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("cheatDeviceID")
}
static var cheatActivation: SQLite.Expression<String?> {
return SQLite.Expression<String?>("cheatActivation")
}
static var cheatCategoryID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("cheatCategoryID")
}
static var cheatCategoryName: SQLite.Expression<String> {
return SQLite.Expression<String>("cheatCategory")
}
static var cheatCategoryDescription: SQLite.Expression<String> {
return SQLite.Expression<String>("cheatCategoryDescription")
}
}
extension Table
{
static var cheats: Table {
return Table("CHEATS")
}
static var cheatCategories: Table {
return Table("CHEAT_CATEGORIES")
}
}
@available(iOS 14, *)
class CheatBase: GamesDatabase
{
static let cheatsVersion = 1
static var previousCheatsVersion: Int? {
return UserDefaults.standard.previousCheatBaseVersion
}
private let connection: Connection
override init() throws
{
let fileURL = DatabaseManager.cheatBaseURL
guard FileManager.default.fileExists(atPath: fileURL.path) else { throw GamesDatabase.Error.doesNotExist }
self.connection = try Connection(fileURL.path)
try super.init()
UserDefaults.standard.previousCheatBaseVersion = CheatBase.cheatsVersion
}
func cheats(for game: Game) async throws -> [CheatMetadata]?
{
let metadata = await withCheckedContinuation { continuation in
if let context = game.managedObjectContext
{
context.perform {
let metadata = self.metadata(for: game)
continuation.resume(returning: metadata)
}
}
else
{
let metadata = self.metadata(for: game)
continuation.resume(returning: metadata)
}
}
guard let romIDValue = metadata?.romID else { return nil }
let cheatID = Expression<Any>.cheatID
let cheatName = Expression<Any>.cheatName
let cheatCode = Expression<Any>.cheatCode
let cheatDescription = Expression<Any>.cheatDescription
let cheatActivation = Expression<Any>.cheatActivation
let cheatDeviceID = Expression<Any>.cheatDeviceID
let categoryID = Expression<Any>.cheatCategoryID
let categoryName = Expression<Any>.cheatCategoryName
let categoryDescription = Expression<Any>.cheatCategoryDescription
let romID = Expression<Any>.romID
let query = Table.cheats.select(cheatID, cheatName, cheatCode, cheatDescription, cheatActivation, cheatDeviceID, Table.cheats[categoryID], categoryName, categoryDescription)
.filter(romID == romIDValue)
.join(Table.cheatCategories, on: Table.cheats[categoryID] == Table.cheatCategories[categoryID])
.order(cheatName)
let rows = try self.connection.prepare(query)
let results = rows.compactMap { (row) -> CheatMetadata? in
guard case let deviceID = Int16(row[cheatDeviceID]), let device = CheatDevice(rawValue: deviceID) else { return nil }
let id = row[Table.cheats[categoryID]]
let category = CheatCategory(id: id, name: row[categoryName], categoryDescription: row[categoryDescription])
let metadata = CheatMetadata(id: row[cheatID], name: row[cheatName], code: row[cheatCode], description: row[cheatDescription], activationHint: row[cheatActivation], device: device, category: category)
return metadata
}
return results
}
}

View File

@ -1,273 +0,0 @@
//
// CheatBaseView.swift
// Delta
//
// Created by Riley Testut on 1/17/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import enum SQLite.Result
@available(iOS 14, *)
extension CheatBaseView
{
private class ViewModel: ObservableObject
{
@Published
var allCheats: [CheatMetadata]? {
didSet {
guard let cheats = allCheats else {
self.cheatsByCategory = nil
return
}
let cheatsByCategory = Dictionary(grouping: cheats, by: { $0.category }).sorted { $0.key.id < $1.key.id }
self.cheatsByCategory = cheatsByCategory
}
}
@Published
private(set) var cheatsByCategory: [(CheatCategory, [CheatMetadata])]?
@Published
private(set) var error: Error?
@Published
var searchText: String = "" {
didSet {
self.searchCheats()
}
}
@Published
private(set) var filteredCheats: [CheatMetadata]?
@MainActor
func fetchCheats(for game: Game) async
{
guard self.allCheats == nil else { return }
do
{
let database = try CheatBase()
let cheats = try await database.cheats(for: game) ?? []
self.allCheats = cheats
}
catch
{
self.error = error
}
}
private func searchCheats()
{
if let cheats = self.allCheats, !self.searchText.isEmpty
{
let predicate = NSPredicate(forSearchingForText: self.searchText, inValuesForKeyPaths: [#keyPath(CheatMetadata.name), #keyPath(CheatMetadata.cheatDescription)])
let filteredCheats = cheats.filter { predicate.evaluate(with: $0) }
self.filteredCheats = filteredCheats
}
else
{
self.filteredCheats = nil
}
}
}
}
@available(iOS 14, *)
struct CheatBaseView: View
{
let game: Game?
var cancellationHandler: (() -> Void)?
var selectionHandler: ((CheatMetadata) -> Void)?
@StateObject
private var viewModel = ViewModel()
@State
private var activationHintCheat: CheatMetadata?
var body: some View {
NavigationView {
ZStack {
if let cheats = viewModel.allCheats, !cheats.isEmpty
{
// Only show List if there is at least one cheat for this game.
cheatList()
}
// Place above List
placeholderView()
}
.alert(item: $activationHintCheat) { cheat in
Alert(title: Text("How to Activate"),
message: Text(cheat.activationHint ?? ""),
dismissButton: .default(Text("OK")))
}
.navigationTitle(Text(game?.name ?? "CheatBase"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
cancellationHandler?()
}
}
}
}
.onAppear {
Task {
guard let game = self.game else { return }
await viewModel.fetchCheats(for: game)
}
}
}
private func cheatList() -> some View
{
VStack {
if #unavailable(iOS 15)
{
LegacySearchBar(text: $viewModel.searchText)
}
let listView = List {
if let filteredCheats = viewModel.filteredCheats
{
ForEach(filteredCheats) { cheat in
cell(for: cheat)
}
}
else if let cheats = viewModel.cheatsByCategory
{
ForEach(cheats, id: \.0.id) { (category, cheats) in
Section {
DisclosureGroup {
ForEach(cheats) { cheat in
cell(for: cheat)
}
} label: {
Text(category.name)
}
} footer: {
Text(category.categoryDescription)
}
}
}
}
if #available(iOS 15, *)
{
listView.searchable(text: $viewModel.searchText)
}
else
{
listView
}
}
.listStyle(.insetGrouped)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
}
private func cell(for cheat: CheatMetadata) -> some View
{
ZStack(alignment: .leading) {
Button(action: { choose(cheat) }) {}
HStack {
// Name + Description
VStack(alignment: .leading, spacing: 4) {
Text(cheat.name)
if let description = cheat.cheatDescription
{
Text(description)
.font(.caption)
}
}
// Activation Hint
if cheat.activationHint != nil
{
Spacer()
Button(action: { activationHintCheat = cheat }) {
Image(systemName: "info.circle")
}
.buttonStyle(.borderless)
}
}
.multilineTextAlignment(.leading)
}
}
private func placeholderView() -> some View
{
VStack(spacing: 8) {
if let error = viewModel.error
{
Text("Unable to Load Cheats")
.font(.title)
if let error = error as? SQLite.Result
{
// SQLite.Result implements CustomStringConvertible.description, but not localizedDescription.
Text(String(describing: error))
.font(.callout)
}
else
{
Text(error.localizedDescription)
.font(.callout)
}
}
else if let filteredCheats = viewModel.filteredCheats, filteredCheats.isEmpty
{
Text("Cheat Not Found")
.font(.title)
Text("Please make sure the name is correct, or try searching for another cheat.")
.font(.callout)
}
else if let cheats = viewModel.allCheats, cheats.isEmpty
{
Text("No Cheats")
.font(.title)
Text("There are no cheats for this game in Delta's CheatBase. Please try a different game.")
.font(.callout)
}
else if viewModel.allCheats == nil
{
ProgressView()
.progressViewStyle(.circular)
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.foregroundColor(.gray)
.padding()
}
init(game: Game, cheats: [CheatMetadata]? = nil)
{
self.game = game
let viewModel = ViewModel()
viewModel.allCheats = cheats
self._viewModel = StateObject(wrappedValue: viewModel)
}
}
@available(iOS 14, *)
private extension CheatBaseView
{
func choose(_ cheatMetadata: CheatMetadata)
{
self.selectionHandler?(cheatMetadata)
}
}

View File

@ -1,111 +0,0 @@
//
// CheatDevice.swift
// Delta
//
// Created by Riley Testut on 1/30/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
import DeltaCore
import NESDeltaCore
@objc
enum CheatDevice: Int16
{
case famicomGameGenie = 1
case famicomRaw = 2
case famicomRawCompare = 3
case gbGameGenie = 4
case gbaActionReplayMax = 5
case gbaCodeBreaker = 6
case gbaGameShark = 7
case gbcGameShark = 8
case n64GameShark = 9
case dsActionReplay = 10
case dsCodeBreaker = 11
case nesGameGenie = 12
case nesRaw = 13
case nesRawCompare = 14
case snesActionReplay = 15
case snesGameGenie = 16
case gameGearActionReplay = 17
case gameGearGameGenie = 18
case masterSystemActionReplay = 19
case masterSystemGameGenie = 20
case cdActionReplay10 = 21
case cdActionReplay8 = 22
case genesisActionReplay10 = 23
case genesisActionReplay8 = 24
}
extension CheatDevice
{
var cheatType: CheatType? {
switch self
{
case .snesActionReplay, .gbaActionReplayMax, .dsActionReplay, .gameGearActionReplay, .masterSystemActionReplay, .genesisActionReplay8, .genesisActionReplay10, .cdActionReplay8, .cdActionReplay10:
return .actionReplay
case .n64GameShark, .gbcGameShark, .gbaGameShark:
return .gameShark
case .famicomGameGenie, .snesGameGenie, .gbGameGenie, .gameGearGameGenie, .masterSystemGameGenie:
return .gameGenie
case .nesGameGenie:
return CheatType(rawValue: DeltaCore.CheatType.gameGenie8.rawValue)
case .gbaCodeBreaker, .dsCodeBreaker:
return .codeBreaker
case .famicomRaw, .famicomRawCompare:
return nil
case .nesRaw, .nesRawCompare:
return nil
}
}
var gameType: GameType? {
switch self
{
case .famicomGameGenie, .famicomRaw, .famicomRawCompare: return .nes
case .nesGameGenie, .nesRaw, .nesRawCompare: return .nes
case .snesActionReplay, .snesGameGenie: return .snes
case .n64GameShark: return .n64
case .gbGameGenie, .gbcGameShark: return .gbc
case .gbaActionReplayMax, .gbaGameShark, .gbaCodeBreaker: return .gba
case .dsActionReplay, .dsCodeBreaker: return .ds
case .genesisActionReplay8, .genesisActionReplay10: return .genesis
case .cdActionReplay8, .cdActionReplay10: return .genesis
// Not yet supported
case .gameGearActionReplay, .gameGearGameGenie: return nil
case .masterSystemActionReplay, .masterSystemGameGenie: return nil
}
}
var cheatFormat: CheatFormat? {
guard
let cheatType = self.cheatType,
let gameType = self.gameType,
let deltaCore = Delta.core(for: gameType)
else { return nil }
let cheatFormat = deltaCore.supportedCheatFormats.first { $0.type == cheatType }
return cheatFormat
}
}

View File

@ -1,45 +0,0 @@
//
// CheatMetadata.swift
// Delta
//
// Created by Riley Testut on 1/17/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import DeltaCore
struct CheatCategory: Identifiable, Hashable
{
var id: Int
var name: String
var categoryDescription: String
}
@objcMembers // @objcMembers required for NSPredicate-based filtering.
final class CheatMetadata: NSObject, Identifiable
{
let id: Int
let name: String
let code: String
let cheatDescription: String?
let activationHint: String?
let device: CheatDevice
let category: CheatCategory
init(id: Int, name: String, code: String, description: String?, activationHint: String?, device: CheatDevice, category: CheatCategory)
{
self.id = id
self.name = name
self.code = code
self.cheatDescription = description
self.activationHint = activationHint
self.device = device
self.category = category
}
}

View File

@ -1,51 +0,0 @@
//
// LegacySearchBar.swift
// Delta
//
// Created by Riley Testut on 1/25/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import SwiftUI
@available(iOS 13, *)
struct LegacySearchBar: UIViewRepresentable
{
class Coordinator: NSObject, UISearchBarDelegate
{
@Binding
var text: String
init(text: Binding<String>)
{
self._text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
{
self.text = searchText
}
}
@Binding
var text: String
func makeUIView(context: Context) -> UISearchBar
{
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = NSLocalizedString("Search", comment: "")
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: Context)
{
uiView.text = self.text
}
func makeCoordinator() -> Coordinator
{
return Coordinator(text: $text)
}
}

View File

@ -14,7 +14,6 @@ import DeltaCore
import Harmony import Harmony
import Roxas import Roxas
import ZIPFoundation import ZIPFoundation
import MelonDSDeltaCore
extension DatabaseManager extension DatabaseManager
{ {
@ -23,7 +22,7 @@ extension DatabaseManager
extension DatabaseManager extension DatabaseManager
{ {
enum ImportError: LocalizedError, Hashable, Equatable enum ImportError: Error, Hashable
{ {
case doesNotExist(URL) case doesNotExist(URL)
case invalid(URL) case invalid(URL)
@ -31,14 +30,31 @@ extension DatabaseManager
case unknown(URL, NSError) case unknown(URL, NSError)
case saveFailed(Set<URL>, NSError) case saveFailed(Set<URL>, NSError)
var errorDescription: String? { var hashValue: Int {
switch self switch self
{ {
case .doesNotExist: return NSLocalizedString("The file does not exist.", comment: "") case .doesNotExist: return 0
case .invalid: return NSLocalizedString("The file is invalid.", comment: "") case .invalid: return 1
case .unsupported: return NSLocalizedString("This file is not supported.", comment: "") case .unsupported: return 2
case .unknown(_, let error): return error.localizedDescription case .unknown: return 3
case .saveFailed(_, let error): return error.localizedDescription case .saveFailed: return 4
}
}
static func ==(lhs: ImportError, rhs: ImportError) -> Bool
{
switch (lhs, rhs)
{
case (let .doesNotExist(url1), let .doesNotExist(url2)) where url1 == url2: return true
case (let .invalid(url1), let .invalid(url2)) where url1 == url2: return true
case (let .unsupported(url1), let .unsupported(url2)) where url1 == url2: return true
case (let .unknown(url1, error1), let .unknown(url2, error2)) where url1 == url2 && error1 == error2: return true
case (let .saveFailed(urls1, error1), let .saveFailed(urls2, error2)) where urls1 == urls2 && error1 == error2: return true
case (.doesNotExist, _): return false
case (.invalid, _): return false
case (.unsupported, _): return false
case (.unknown, _): return false
case (.saveFailed, _): return false
} }
} }
} }
@ -54,8 +70,6 @@ final class DatabaseManager: RSTPersistentContainer
private var validationManagedObjectContext: NSManagedObjectContext? private var validationManagedObjectContext: NSManagedObjectContext?
private let importController = ImportController(documentTypes: [])
private init() private init()
{ {
guard guard
@ -76,10 +90,11 @@ extension DatabaseManager
{ {
guard !self.isStarted else { return } guard !self.isStarted else { return }
for description in self.persistentStoreDescriptions do
{ {
// Set configuration so RSTPersistentContainer can determine how to migrate this and Harmony's database independently. if !FileManager.default.fileExists(atPath: DatabaseManager.backupDirectoryURL.path)
description.configuration = NSManagedObjectModel.Configuration.external.rawValue {
try FileManager.default.copyItem(at: DatabaseManager.defaultDirectoryURL(), to: DatabaseManager.backupDirectoryURL)
} }
self.loadPersistentStores { (description, error) in self.loadPersistentStores { (description, error) in
@ -94,112 +109,9 @@ extension DatabaseManager
} }
} }
} }
func prepare(_ core: DeltaCoreProtocol, in context: NSManagedObjectContext)
{
guard let system = System(gameType: core.gameType) else { return }
if let skin = ControllerSkin(system: system, context: context)
{
print("Updated default skin (\(skin.identifier)) for system:", system)
}
else
{
print("Failed to update default skin for system:", system)
}
switch system
{
case .ds where core == MelonDS.core:
// Returns nil if game already exists.
func makeBIOS(name: String, identifier: String) -> Game?
{
let predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), identifier)
if let _ = Game.instancesWithPredicate(predicate, inManagedObjectContext: context, type: Game.self).first
{
// BIOS already exists, so don't do anything.
return nil
}
let filename: String
switch identifier
{
case Game.melonDSBIOSIdentifier:
guard
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios7URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios9URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.firmwareURL.path)
else { return nil }
filename = "nds.bios"
case Game.melonDSDSiBIOSIdentifier:
#if BETA
guard
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS7URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS9URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiFirmwareURL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiNANDURL.path)
else { return nil }
filename = "dsi.bios"
#else
return nil
#endif
default: filename = "system.bios"
}
let bios = Game(context: context)
bios.name = name
bios.identifier = identifier
bios.type = .ds
bios.filename = filename
if let artwork = UIImage(named: "DS Home Screen"), let artworkData = artwork.pngData()
{
do
{
let destinationURL = DatabaseManager.artworkURL(for: bios)
try artworkData.write(to: destinationURL, options: .atomic)
bios.artworkURL = destinationURL
}
catch catch
{ {
print("Failed to copy default DS home screen artwork.", error) completionHandler(error)
}
}
return bios
}
let insertedGames = [
(name: NSLocalizedString("Home Screen", comment: ""), identifier: Game.melonDSBIOSIdentifier),
(name: NSLocalizedString("Home Screen (DSi)", comment: ""), identifier: Game.melonDSDSiBIOSIdentifier)
].compactMap(makeBIOS)
// Break if we didn't create any new Games.
guard !insertedGames.isEmpty else { break }
let gameCollection = GameCollection(context: context)
gameCollection.identifier = GameType.ds.rawValue
gameCollection.index = Int16(System.ds.year)
gameCollection.games.formUnion(insertedGames)
case .ds:
let predicate = NSPredicate(format: "%K IN %@", #keyPath(Game.identifier), [Game.melonDSBIOSIdentifier, Game.melonDSDSiBIOSIdentifier])
let games = Game.instancesWithPredicate(predicate, inManagedObjectContext: context, type: Game.self)
for game in games
{
context.delete(game)
}
default: break
} }
} }
} }
@ -241,7 +153,13 @@ private extension DatabaseManager
for system in System.allCases for system in System.allCases
{ {
self.prepare(system.deltaCore, in: context) guard let deltaControllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: system.gameType) else { continue }
let controllerSkin = ControllerSkin(context: context)
controllerSkin.isStandard = true
controllerSkin.filename = deltaControllerSkin.fileURL.lastPathComponent
controllerSkin.configure(with: deltaControllerSkin)
} }
do do
@ -255,27 +173,10 @@ private extension DatabaseManager
do do
{ {
if !FileManager.default.fileExists(atPath: DatabaseManager.gamesDatabaseURL.path) || GamesDatabase.version != GamesDatabase.previousVersion if !FileManager.default.fileExists(atPath: DatabaseManager.gamesDatabaseURL.path)
{ {
guard let bundleURL = Bundle.main.url(forResource: "openvgdb", withExtension: "sqlite") else { throw GamesDatabase.Error.doesNotExist } guard let bundleURL = Bundle.main.url(forResource: "openvgdb", withExtension: "sqlite") else { throw GamesDatabase.Error.doesNotExist }
try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.gamesDatabaseURL, shouldReplace: true) try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.gamesDatabaseURL)
}
if #available(iOS 14, *), !FileManager.default.fileExists(atPath: DatabaseManager.cheatBaseURL.path) || CheatBase.cheatsVersion != CheatBase.previousCheatsVersion
{
guard let archiveURL = Bundle.main.url(forResource: "cheatbase", withExtension: "zip") else { throw GamesDatabase.Error.doesNotExist }
let temporaryDirectoryURL = FileManager.default.uniqueTemporaryURL()
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true)
defer {
try? FileManager.default.removeItem(at: temporaryDirectoryURL)
}
// Unzip to temporaryDirectoryURL first to ensure we don't accidentally unzip other items into DatabaseManager.cheatBaseURL directory (e.g. __MACOSX directory).
try FileManager.default.unzipItem(at: archiveURL, to: temporaryDirectoryURL, skipCRC32: true) // skipCRC32 to avoid ~10 second extraction.
let extractedDatabaseURL = temporaryDirectoryURL.appendingPathComponent("cheatbase.sqlite")
try FileManager.default.copyItem(at: extractedDatabaseURL, to: DatabaseManager.cheatBaseURL, shouldReplace: true)
} }
self.gamesDatabase = try GamesDatabase() self.gamesDatabase = try GamesDatabase()
@ -296,20 +197,7 @@ extension DatabaseManager
{ {
func importGames(at urls: Set<URL>, completion: ((Set<Game>, Set<ImportError>) -> Void)?) func importGames(at urls: Set<URL>, completion: ((Set<Game>, Set<ImportError>) -> Void)?)
{ {
let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) } var errors = Set<ImportError>()
guard externalFileURLs.isEmpty else {
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
availableFileURLs.formUnion(importedURLs)
self.importGames(at: Set(availableFileURLs)) { (importedGames, importErrors) in
let allErrors = importErrors.union(externalImportErrors)
completion?(importedGames, allErrors)
}
}
return
}
let zipFileURLs = urls.filter { $0.pathExtension.lowercased() == "zip" } let zipFileURLs = urls.filter { $0.pathExtension.lowercased() == "zip" }
if zipFileURLs.count > 0 if zipFileURLs.count > 0
@ -327,7 +215,6 @@ extension DatabaseManager
self.performBackgroundTask { (context) in self.performBackgroundTask { (context) in
var errors = Set<ImportError>()
var identifiers = Set<String>() var identifiers = Set<String>()
for url in urls for url in urls
@ -342,11 +229,6 @@ extension DatabaseManager
continue continue
} }
guard System.registeredSystems.contains(system) else {
errors.insert(.unsupported(url))
continue
}
let identifier: String let identifier: String
do do
@ -423,24 +305,10 @@ extension DatabaseManager
func importControllerSkins(at urls: Set<URL>, completion: ((Set<ControllerSkin>, Set<ImportError>) -> Void)?) func importControllerSkins(at urls: Set<URL>, completion: ((Set<ControllerSkin>, Set<ImportError>) -> Void)?)
{ {
let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) } var errors = Set<ImportError>()
guard externalFileURLs.isEmpty else {
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
availableFileURLs.formUnion(importedURLs)
self.importControllerSkins(at: Set(availableFileURLs)) { (importedSkins, importErrors) in
let allErrors = importErrors.union(externalImportErrors)
completion?(importedSkins, allErrors)
}
}
return
}
self.performBackgroundTask { (context) in self.performBackgroundTask { (context) in
var errors = Set<ImportError>()
var identifiers = Set<String>() var identifiers = Set<String>()
for url in urls for url in urls
@ -545,7 +413,7 @@ extension DatabaseManager
try FileManager.default.removeItem(at: outputURL) try FileManager.default.removeItem(at: outputURL)
} }
_ = try archive.extract(entry, to: outputURL, skipCRC32: true) _ = try archive.extract(entry, to: outputURL)
outputURLs.insert(outputURL) outputURLs.insert(outputURL)
} }
@ -579,32 +447,6 @@ extension DatabaseManager
completion(outputURLs, errors) completion(outputURLs, errors)
} }
} }
private func importExternalFiles(at urls: Set<URL>, completion: @escaping ((Set<URL>, Set<ImportError>) -> Void))
{
var outputURLs = Set<URL>()
var errors = Set<ImportError>()
let dispatchGroup = DispatchGroup()
for url in urls
{
dispatchGroup.enter()
self.importController.importExternalFile(at: url) { (result) in
switch result
{
case .failure(let error): errors.insert(.unknown(url, error as NSError))
case .success(let fileURL): outputURLs.insert(fileURL)
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
completion(outputURLs, errors)
}
}
} }
//MARK: - File URLs - //MARK: - File URLs -
@ -636,12 +478,6 @@ extension DatabaseManager
return gamesDatabaseURL return gamesDatabaseURL
} }
class var cheatBaseURL: URL
{
let gamesDatabaseURL = self.defaultDirectoryURL().appendingPathComponent("cheatbase.sqlite")
return gamesDatabaseURL
}
class var gamesDirectoryURL: URL class var gamesDirectoryURL: URL
{ {
let gamesDirectoryURL = DatabaseManager.defaultDirectoryURL().appendingPathComponent("Games") let gamesDirectoryURL = DatabaseManager.defaultDirectoryURL().appendingPathComponent("Games")
@ -686,9 +522,15 @@ extension DatabaseManager
{ {
let gameURL = game.fileURL let gameURL = game.fileURL
let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("png") let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("jpg")
return artworkURL return artworkURL
} }
class var backupDirectoryURL: URL
{
let backupDirectoryURL = FileManager.default.documentsDirectory.appendingPathComponent("Database-Backup")
return backupDirectoryURL
}
} }
//MARK: - Notifications - //MARK: - Notifications -

View File

@ -3,6 +3,6 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>_XCCurrentVersionName</key> <key>_XCCurrentVersionName</key>
<string>Delta 7.xcdatamodel</string> <string>Delta 4.xcdatamodel</string>
</dict> </dict>
</plist> </plist>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14492.1" systemVersion="18G95" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0"> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
<entity name="Cheat" representedClassName="Cheat" syncable="YES"> <entity name="Cheat" representedClassName="Cheat" syncable="YES">
<attribute name="code" attributeType="String" syncable="YES"/> <attribute name="code" attributeType="String" syncable="YES"/>
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/> <attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
@ -7,7 +7,7 @@
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/> <attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/> <attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/> <attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES"> <attribute name="type" attributeType="Transformable" syncable="YES">
<userInfo> <userInfo>
<entry key="attributeValueClassName" value="CheatType"/> <entry key="attributeValueClassName" value="CheatType"/>
</userInfo> </userInfo>
@ -21,7 +21,7 @@
</entity> </entity>
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES"> <entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
<attribute name="filename" attributeType="String" syncable="YES"/> <attribute name="filename" attributeType="String" syncable="YES"/>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES"> <attribute name="gameType" attributeType="Transformable" syncable="YES">
<userInfo> <userInfo>
<entry key="attributeValueClassName" value="GameType"/> <entry key="attributeValueClassName" value="GameType"/>
</userInfo> </userInfo>
@ -42,7 +42,7 @@
</uniquenessConstraints> </uniquenessConstraints>
</entity> </entity>
<entity name="Game" representedClassName="Game" syncable="YES"> <entity name="Game" representedClassName="Game" syncable="YES">
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES"> <attribute name="artworkURL" optional="YES" attributeType="Transformable" syncable="YES">
<userInfo> <userInfo>
<entry key="attributeValueClassName" value="URL"/> <entry key="attributeValueClassName" value="URL"/>
</userInfo> </userInfo>
@ -55,7 +55,7 @@
<attribute name="identifier" attributeType="String" syncable="YES"/> <attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/> <attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/> <attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES"> <attribute name="type" attributeType="Transformable" syncable="YES">
<userInfo> <userInfo>
<entry key="attributeValueClassName" value="GameType"/> <entry key="attributeValueClassName" value="GameType"/>
</userInfo> </userInfo>
@ -87,12 +87,12 @@
<entry key="attributeValueClassName" value="Any"/> <entry key="attributeValueClassName" value="Any"/>
</userInfo> </userInfo>
</attribute> </attribute>
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES"> <attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="" syncable="YES">
<userInfo> <userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/> <entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo> </userInfo>
</attribute> </attribute>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES"> <attribute name="gameType" attributeType="Transformable" syncable="YES">
<userInfo> <userInfo>
<entry key="attributeValueClassName" value="GameType"/> <entry key="attributeValueClassName" value="GameType"/>
</userInfo> </userInfo>
@ -146,7 +146,7 @@
<elements> <elements>
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/> <element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="135"/> <element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="135"/>
<element name="Game" positionX="-378" positionY="-54" width="128" height="210"/> <element name="Game" positionX="-378" positionY="-54" width="128" height="30"/>
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/> <element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/> <element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/> <element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>

View File

@ -1,159 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
<attribute name="code" attributeType="String" syncable="YES"/>
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="CheatType"/>
</userInfo>
</attribute>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
<attribute name="filename" attributeType="String" syncable="YES"/>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
<userInfo>
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
</userInfo>
</attribute>
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="gameType"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Game" representedClassName="Game" syncable="YES">
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="URL"/>
</userInfo>
</attribute>
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollection" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="Any"/>
</userInfo>
</attribute>
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo>
</attribute>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="gameControllerInputType"/>
<constraint value="gameType"/>
<constraint value="playerIndex"/>
</uniquenessConstraint>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
<userInfo>
<entry key="attributeValueScalarType" value="SaveStateType"/>
</userInfo>
</attribute>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="163"/>
<element name="Game" positionX="-378" positionY="-54" width="128" height="238"/>
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
</elements>
</model>

View File

@ -1,160 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E287" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
<attribute name="code" attributeType="String" syncable="YES"/>
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="CheatType"/>
</userInfo>
</attribute>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
<attribute name="filename" attributeType="String" syncable="YES"/>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
<userInfo>
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
</userInfo>
</attribute>
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="gameType"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Game" representedClassName="Game" syncable="YES">
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="URL"/>
</userInfo>
</attribute>
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollection" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="Any"/>
</userInfo>
</attribute>
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo>
</attribute>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="gameControllerInputType"/>
<constraint value="gameType"/>
<constraint value="playerIndex"/>
</uniquenessConstraint>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
<attribute name="coreIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
<userInfo>
<entry key="attributeValueScalarType" value="SaveStateType"/>
</userInfo>
</attribute>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="163"/>
<element name="Game" positionX="-378" positionY="-54" width="128" height="238"/>
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
<element name="SaveState" positionX="-198" positionY="113" width="128" height="178"/>
</elements>
</model>

View File

@ -1,152 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22158.8" systemVersion="22F66" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
<attribute name="code" attributeType="String" syncable="YES"/>
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="CheatType"/>
</userInfo>
</attribute>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
<attribute name="filename" attributeType="String" syncable="YES"/>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
<userInfo>
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
</userInfo>
</attribute>
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
<constraint value="gameType"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Game" representedClassName="Game" syncable="YES">
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="URL"/>
</userInfo>
</attribute>
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollection" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="Any"/>
</userInfo>
</attribute>
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameControllerInputType"/>
</userInfo>
</attribute>
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="GameType"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="gameControllerInputType"/>
<constraint value="gameType"/>
<constraint value="playerIndex"/>
</uniquenessConstraint>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="sha1" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
<attribute name="coreIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
<userInfo>
<entry key="attributeValueScalarType" value="SaveStateType"/>
</userInfo>
</attribute>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View File

@ -53,9 +53,4 @@ extension Cheat: Syncable
public var syncableLocalizedName: String? { public var syncableLocalizedName: String? {
return self.name return self.name
} }
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
{
return .newest
}
} }

View File

@ -13,27 +13,16 @@ import Harmony
extension ControllerSkinConfigurations extension ControllerSkinConfigurations
{ {
init?(traits: DeltaCore.ControllerSkin.Traits) init(traits: DeltaCore.ControllerSkin.Traits)
{ {
switch (traits.device, traits.displayType, traits.orientation) switch (traits.displayType, traits.orientation)
{ {
case (.iphone, .standard, .portrait): self = .iphoneStandardPortrait case (.standard, .portrait): self = .standardPortrait
case (.iphone, .standard, .landscape): self = .iphoneStandardLandscape case (.standard, .landscape): self = .standardLandscape
case (.iphone, .edgeToEdge, .portrait): self = .iphoneEdgeToEdgePortrait case (.edgeToEdge, .portrait): self = .edgeToEdgePortrait
case (.iphone, .edgeToEdge, .landscape): self = .iphoneEdgeToEdgeLandscape case (.edgeToEdge, .landscape): self = .edgeToEdgeLandscape
case (.iphone, .splitView, _): return nil case (.splitView, .portrait): self = .splitViewPortrait
case (.splitView, .landscape): self = .splitViewLandscape
case (.ipad, .standard, .portrait): self = .ipadStandardPortrait
case (.ipad, .standard, .landscape): self = .ipadStandardLandscape
case (.ipad, .edgeToEdge, .portrait): self = .ipadEdgeToEdgePortrait
case (.ipad, .edgeToEdge, .landscape): self = .ipadEdgeToEdgeLandscape
case (.ipad, .splitView, .portrait): self = .ipadSplitViewPortrait
case (.ipad, .splitView, .landscape): self = .ipadSplitViewLandscape
case (.tv, .standard, .portrait): self = .tvStandardPortrait
case (.tv, .standard, .landscape): self = .tvStandardLandscape
case (.tv, .edgeToEdge, _): return nil
case (.tv, .splitView, _): return nil
} }
} }
} }
@ -77,9 +66,9 @@ extension ControllerSkin: ControllerSkinProtocol
return self.controllerSkin?.image(for: traits, preferredSize: preferredSize) return self.controllerSkin?.image(for: traits, preferredSize: preferredSize)
} }
public func thumbstick(for item: DeltaCore.ControllerSkin.Item, traits: DeltaCore.ControllerSkin.Traits, preferredSize: DeltaCore.ControllerSkin.Size) -> (UIImage, CGSize)? public func inputs(for traits: DeltaCore.ControllerSkin.Traits, at point: CGPoint) -> [Input]?
{ {
return self.controllerSkin?.thumbstick(for: item, traits: traits, preferredSize: preferredSize) return self.controllerSkin?.inputs(for: traits, at: point)
} }
public func items(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Item]? public func items(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Item]?
@ -92,20 +81,15 @@ extension ControllerSkin: ControllerSkinProtocol
return self.controllerSkin?.isTranslucent(for: traits) return self.controllerSkin?.isTranslucent(for: traits)
} }
public func screens(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Screen]? public func gameScreenFrame(for traits: DeltaCore.ControllerSkin.Traits) -> CGRect?
{ {
return self.controllerSkin?.screens(for: traits) return self.controllerSkin?.gameScreenFrame(for: traits)
} }
public func aspectRatio(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize? public func aspectRatio(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize?
{ {
return self.controllerSkin?.aspectRatio(for: traits) return self.controllerSkin?.aspectRatio(for: traits)
} }
public func contentSize(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize?
{
return self.controllerSkin?.contentSize(for: traits)
}
} }
extension ControllerSkin: Syncable extension ControllerSkin: Syncable
@ -129,9 +113,4 @@ extension ControllerSkin: Syncable
public var syncableLocalizedName: String? { public var syncableLocalizedName: String? {
return self.name return self.name
} }
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
{
return .newest
}
} }

View File

@ -9,22 +9,14 @@
import Foundation import Foundation
import DeltaCore import DeltaCore
import MelonDSDeltaCore
import Harmony import Harmony
public extension Game
{
static let melonDSBIOSIdentifier = "com.rileytestut.MelonDSDeltaCore.BIOS"
static let melonDSDSiBIOSIdentifier = "com.rileytestut.MelonDSDeltaCore.DSiBIOS"
}
@objc(Game) @objc(Game)
public class Game: _Game, GameProtocol public class Game: _Game, GameProtocol
{ {
public var fileURL: URL { public var fileURL: URL {
var fileURL: URL! var fileURL: URL!
// self URL
self.managedObjectContext?.performAndWait { self.managedObjectContext?.performAndWait {
fileURL = DatabaseManager.gamesDirectoryURL.appendingPathComponent(self.filename) fileURL = DatabaseManager.gamesDirectoryURL.appendingPathComponent(self.filename)
} }
@ -38,29 +30,11 @@ public class Game: _Game, GameProtocol
var artworkURL = self.primitiveValue(forKey: #keyPath(Game.artworkURL)) as? URL var artworkURL = self.primitiveValue(forKey: #keyPath(Game.artworkURL)) as? URL
self.didAccessValue(forKey: #keyPath(Game.artworkURL)) self.didAccessValue(forKey: #keyPath(Game.artworkURL))
if let unwrappedArtworkURL = artworkURL if let unwrappedArtworkURL = artworkURL, unwrappedArtworkURL.isFileURL
{
if unwrappedArtworkURL.isFileURL
{ {
// Recreate the stored URL relative to current sandbox location. // Recreate the stored URL relative to current sandbox location.
artworkURL = URL(fileURLWithPath: unwrappedArtworkURL.relativePath, relativeTo: DatabaseManager.gamesDirectoryURL) artworkURL = URL(fileURLWithPath: unwrappedArtworkURL.relativePath, relativeTo: DatabaseManager.gamesDirectoryURL)
} }
else if let host = unwrappedArtworkURL.host?.lowercased(), host == "img.gamefaqs.net" || host == "gamefaqs1.cbsistatic.com",
var components = URLComponents(url: unwrappedArtworkURL, resolvingAgainstBaseURL: false)
{
// Quick fix for broken album artwork URLs due to host change.
components.host = "gamefaqs.gamespot.com"
components.scheme = "https"
let updatedPath = "/a" + components.path
components.path = updatedPath
if let url = components.url
{
artworkURL = url
}
}
}
return artworkURL return artworkURL
} }
@ -102,17 +76,11 @@ extension Game
guard let managedObjectContext = self.managedObjectContext else { return } guard let managedObjectContext = self.managedObjectContext else { return }
// If filename == empty string (e.g. during merge), ignore this deletion.
// Otherwise, we may accidentally delete the entire Games directory!
guard !self.filename.isEmpty else { return }
// If a game with the same identifier is also currently being inserted, Core Data is more than likely resolving a conflict by deleting the previous instance // If a game with the same identifier is also currently being inserted, Core Data is more than likely resolving a conflict by deleting the previous instance
// In this case, we make sure we DON'T delete the game file + misc other Core Data relationships, or else we'll just lose all that data // In this case, we make sure we DON'T delete the game file + misc other Core Data relationships, or else we'll just lose all that data
guard !managedObjectContext.insertedObjects.contains(where: { ($0 as? Game)?.identifier == self.identifier }) else { return } guard !managedObjectContext.insertedObjects.contains(where: { ($0 as? Game)?.identifier == self.identifier }) else { return }
// Double-check fileURL is NOT actually a directory, which we should never delete. guard FileManager.default.fileExists(atPath: self.fileURL.path) else { return }
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: self.fileURL.path, isDirectory: &isDirectory), !isDirectory.boolValue else { return }
do do
{ {
@ -154,42 +122,12 @@ extension Game: Syncable
} }
public var syncableFiles: Set<File> { public var syncableFiles: Set<File> {
let artworkURL: URL let gameFile = File(identifier: "game", fileURL: self.fileURL)
if let fileURL = self.artworkURL, fileURL.isFileURL
{
artworkURL = fileURL
}
else
{
artworkURL = DatabaseManager.artworkURL(for: self)
}
let artworkURL = DatabaseManager.artworkURL(for: self)
let artworkFile = File(identifier: "artwork", fileURL: artworkURL) let artworkFile = File(identifier: "artwork", fileURL: artworkURL)
switch self.identifier return [gameFile, artworkFile]
{
case Game.melonDSBIOSIdentifier:
let bios7File = File(identifier: "bios7", fileURL: MelonDSEmulatorBridge.shared.bios7URL)
let bios9File = File(identifier: "bios9", fileURL: MelonDSEmulatorBridge.shared.bios9URL)
let firmwareFile = File(identifier: "firmware", fileURL: MelonDSEmulatorBridge.shared.firmwareURL)
return [artworkFile, bios7File, bios9File, firmwareFile]
case Game.melonDSDSiBIOSIdentifier:
let bios7File = File(identifier: "bios7", fileURL: MelonDSEmulatorBridge.shared.dsiBIOS7URL)
let bios9File = File(identifier: "bios9", fileURL: MelonDSEmulatorBridge.shared.dsiBIOS9URL)
let firmwareFile = File(identifier: "firmware", fileURL: MelonDSEmulatorBridge.shared.dsiFirmwareURL)
// DSi NAND is ~240MB, so don't sync for now until Harmony can selectively download files.
// let nandFile = File(identifier: "nand", fileURL: MelonDSEmulatorBridge.shared.dsiNANDURL)
return [artworkFile, bios7File, bios9File, firmwareFile]
default:
let gameFile = File(identifier: "game", fileURL: self.fileURL)
return [artworkFile, gameFile]
}
} }
public var syncableRelationships: Set<AnyKeyPath> { public var syncableRelationships: Set<AnyKeyPath> {
@ -199,14 +137,4 @@ extension Game: Syncable
public var syncableLocalizedName: String? { public var syncableLocalizedName: String? {
return self.name return self.name
} }
public func awakeFromSync(_ record: AnyRecord) throws
{
guard let gameCollection = self.gameCollection else { throw SyncValidationError.incorrectGameCollection(nil) }
if gameCollection.identifier != self.type.rawValue
{
throw SyncValidationError.incorrectGameCollection(gameCollection.name)
}
}
} }

View File

@ -43,9 +43,4 @@ extension GameCollection: Syncable
public var syncableLocalizedName: String? { public var syncableLocalizedName: String? {
return self.name return self.name
} }
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
{
return .newest
}
} }

View File

@ -50,7 +50,7 @@ extension GameControllerInputMapping
{ {
let inputMappings = try managedObjectContext.fetch(fetchRequest) let inputMappings = try managedObjectContext.fetch(fetchRequest)
let inputMapping = inputMappings.first(where: { !$0.isDeleted }) let inputMapping = inputMappings.first
return inputMapping return inputMapping
} }
catch catch
@ -100,9 +100,4 @@ extension GameControllerInputMapping: Syncable
public var syncableLocalizedName: String? { public var syncableLocalizedName: String? {
return self.name return self.name
} }
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
{
return .newest
}
} }

View File

@ -8,8 +8,6 @@
import Foundation import Foundation
import GBCDeltaCore
import Harmony import Harmony
@objc(GameSave) @objc(GameSave)
@ -30,7 +28,7 @@ extension GameSave: Syncable
} }
public var syncableKeys: Set<AnyKeyPath> { public var syncableKeys: Set<AnyKeyPath> {
return [\GameSave.modifiedDate, \GameSave.sha1] return [\GameSave.modifiedDate]
} }
public var syncableRelationships: Set<AnyKeyPath> { public var syncableRelationships: Set<AnyKeyPath> {
@ -53,64 +51,10 @@ extension GameSave: Syncable
public var syncableMetadata: [HarmonyMetadataKey : String] { public var syncableMetadata: [HarmonyMetadataKey : String] {
guard let game = self.game else { return [:] } guard let game = self.game else { return [:] }
return [.gameID: game.identifier, .gameName: game.name]
// Use self.identifier to always link with exact matching game.
return [.gameID: self.identifier, .gameName: game.name]
} }
public var syncableLocalizedName: String? { public var syncableLocalizedName: String? {
return self.game?.name return self.game?.name
} }
public var isSyncingEnabled: Bool {
// self.game may be nil if being downloaded, so don't enforce it.
// guard let identifier = self.game?.identifier else { return false }
return self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier
}
public func awakeFromSync(_ record: AnyRecord) throws
{
do
{
guard let game = self.game else { throw SyncValidationError.incorrectGame(nil) }
if game.identifier != self.identifier
{
let fetchRequest = GameSave.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(GameSave.identifier), game.identifier)
if let misplacedGameSave = try self.managedObjectContext?.fetch(fetchRequest).first, misplacedGameSave.game == nil
{
// Relink game with its correct gameSave, in case we accidentally misplaced it.
// Otherwise, corrupted records might displace already-downloaded GameSaves
// due to automatic Core Data relationship propagation, despite us throwing error.
game.gameSave = misplacedGameSave
}
else
{
// Either there is no misplacedGameSave, or there is but it's linked to another game somehow.
game.gameSave = nil
}
throw SyncValidationError.incorrectGame(game.name)
}
}
catch let error as SyncValidationError
{
guard SyncManager.shared.ignoredCorruptedRecordIDs.contains(record.recordID) else { throw error }
let fetchRequest = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), self.identifier)
if let correctGame = try self.managedObjectContext?.fetch(fetchRequest).first
{
self.game = correctGame
}
else
{
throw ValidationError.nilRelationshipObjects(keys: [#keyPath(GameSave.game)])
}
}
}
} }

View File

@ -11,8 +11,6 @@ import Foundation
import DeltaCore import DeltaCore
import Harmony import Harmony
import struct DSDeltaCore.DS
@objc public enum SaveStateType: Int16 @objc public enum SaveStateType: Int16
{ {
case auto case auto
@ -113,7 +111,7 @@ extension SaveState: Syncable
} }
public var syncableKeys: Set<AnyKeyPath> { public var syncableKeys: Set<AnyKeyPath> {
return [\SaveState.creationDate, \SaveState.filename, \SaveState.modifiedDate, \SaveState.name, \SaveState.type, \SaveState.coreIdentifier] return [\SaveState.creationDate, \SaveState.filename, \SaveState.modifiedDate, \SaveState.name, \SaveState.type]
} }
public var syncableFiles: Set<File> { public var syncableFiles: Set<File> {
@ -125,70 +123,15 @@ extension SaveState: Syncable
} }
public var isSyncingEnabled: Bool { public var isSyncingEnabled: Bool {
// self.game may be nil if being downloaded, so don't enforce it. return self.type != .auto && self.type != .quick
// 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)
return isSyncingEnabled
} }
public var syncableMetadata: [HarmonyMetadataKey : String] { public var syncableMetadata: [HarmonyMetadataKey : String] {
guard let game = self.game else { return [:] } guard let game = self.game else { return [:] }
return [.gameID: game.identifier, .gameName: game.name, .coreID: self.coreIdentifier, .verifiedGameID: game.identifier].compactMapValues { $0 } return [.gameID: game.identifier, .gameName: game.name]
} }
public var syncableLocalizedName: String? { public var syncableLocalizedName: String? {
return self.localizedName return self.localizedName
} }
public func awakeFromSync(_ record: AnyRecord) throws
{
let verifiedGameID = record.remoteMetadata?[.verifiedGameID]
do
{
guard let game = self.game else { return }
if let system = System(gameType: game.type), self.coreIdentifier == nil
{
if let coreIdentifier = record.remoteMetadata?[.coreID]
{
// SaveState was synced to older version of Delta and lost its coreIdentifier,
// but it remains in the remote metadata so we can reassign it.
self.coreIdentifier = coreIdentifier
}
else
{
switch system
{
case .ds: self.coreIdentifier = DS.core.identifier // Assume DS save state with nil coreIdentifier is from DeSmuME core.
default: self.coreIdentifier = system.deltaCore.identifier
}
}
}
if let verifiedGameID, verifiedGameID != game.identifier
{
// Game does not match verified game ID, which most likely means
// this SaveState was reviewed + fixed on another device, but not uploaded.
throw SyncValidationError.incorrectGame(game.name)
}
}
catch let error as SyncValidationError
{
guard let verifiedGameID, SyncManager.shared.ignoredCorruptedRecordIDs.contains(record.recordID) else { throw error }
let fetchRequest = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), verifiedGameID)
if let correctGame = try self.managedObjectContext?.fetch(fetchRequest).first
{
self.game = correctGame
}
else
{
throw ValidationError.nilRelationshipObjects(keys: [#keyPath(GameSave.game)])
}
}
}
} }

View File

@ -28,9 +28,5 @@ public class _ControllerSkin: NSManagedObject
// MARK: - Relationships // MARK: - Relationships
@NSManaged public var preferredLandscapeSkinByGames: Set<Game>
@NSManaged public var preferredPortraitSkinByGames: Set<Game>
} }

View File

@ -34,10 +34,6 @@ public class _Game: NSManagedObject
@NSManaged public var gameSave: GameSave? @NSManaged public var gameSave: GameSave?
@NSManaged public var preferredLandscapeSkin: ControllerSkin?
@NSManaged public var preferredPortraitSkin: ControllerSkin?
@NSManaged public var previewSaveState: SaveState? @NSManaged public var previewSaveState: SaveState?
@NSManaged public var saveStates: Set<SaveState> @NSManaged public var saveStates: Set<SaveState>

View File

@ -18,8 +18,6 @@ public class _GameSave: NSManagedObject
@NSManaged public var modifiedDate: Date @NSManaged public var modifiedDate: Date
@NSManaged public var sha1: String?
// MARK: - Relationships // MARK: - Relationships
@NSManaged public var game: Game? @NSManaged public var game: Game?

View File

@ -14,8 +14,6 @@ public class _SaveState: NSManagedObject
// MARK: - Properties // MARK: - Properties
@NSManaged public var coreIdentifier: String?
@NSManaged public var creationDate: Date @NSManaged public var creationDate: Date
@NSManaged public var filename: String @NSManaged public var filename: String

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,10 +8,6 @@
import UIKit import UIKit
import DeltaCore
import struct DSDeltaCore.DS
@objc(SaveStateToSaveStateMigrationPolicy) @objc(SaveStateToSaveStateMigrationPolicy)
class SaveStateToSaveStateMigrationPolicy: NSEntityMigrationPolicy class SaveStateToSaveStateMigrationPolicy: NSEntityMigrationPolicy
{ {
@ -27,19 +23,3 @@ class SaveStateToSaveStateMigrationPolicy: NSEntityMigrationPolicy
} }
} }
} }
// Delta5 to Delta6
extension SaveStateToSaveStateMigrationPolicy
{
@objc(defaultCoreIdentifierForGameType:)
func defaultCoreIdentifier(for gameType: GameType) -> String?
{
guard let system = System(gameType: gameType) else { return nil }
switch system
{
case .ds: return DS.core.identifier // Assume any existing save state is from DeSmuME.
default: return system.deltaCore.identifier
}
}
}

View File

@ -9,35 +9,16 @@
#ifndef ControllerSkinConfigurations_h #ifndef ControllerSkinConfigurations_h
#define ControllerSkinConfigurations_h #define ControllerSkinConfigurations_h
// Every possible (supported) combination of traits.
typedef NS_OPTIONS(int16_t, ControllerSkinConfigurations) typedef NS_OPTIONS(int16_t, ControllerSkinConfigurations)
{ {
/* iPhone */ ControllerSkinConfigurationStandardPortrait = 1 << 0,
ControllerSkinConfigurationiPhoneStandardPortrait NS_SWIFT_NAME(iphoneStandardPortrait) = 1 << 0, ControllerSkinConfigurationStandardLandscape = 1 << 1,
ControllerSkinConfigurationiPhoneStandardLandscape NS_SWIFT_NAME(iphoneStandardLandscape) = 1 << 1,
// iPhone doesn't support Split View ControllerSkinConfigurationSplitViewPortrait = 1 << 2,
// ControllerSkinConfigurationiPhoneSplitViewPortrait = 1 << 2, ControllerSkinConfigurationSplitViewLandscape = 1 << 3,
// ControllerSkinConfigurationiPhoneSplitViewLandscape = 1 << 3,
ControllerSkinConfigurationiPhoneEdgeToEdgePortrait NS_SWIFT_NAME(iphoneEdgeToEdgePortrait) = 1 << 4, ControllerSkinConfigurationEdgeToEdgePortrait = 1 << 4,
ControllerSkinConfigurationiPhoneEdgeToEdgeLandscape NS_SWIFT_NAME(iphoneEdgeToEdgeLandscape) = 1 << 5, ControllerSkinConfigurationEdgeToEdgeLandscape = 1 << 5,
/* iPad */
ControllerSkinConfigurationiPadStandardPortrait NS_SWIFT_NAME(ipadStandardPortrait) = 1 << 6,
ControllerSkinConfigurationiPadStandardLandscape NS_SWIFT_NAME(ipadStandardLandscape) = 1 << 7,
ControllerSkinConfigurationiPadSplitViewPortrait NS_SWIFT_NAME(ipadSplitViewPortrait) = 1 << 2, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewPortrait
ControllerSkinConfigurationiPadSplitViewLandscape NS_SWIFT_NAME(ipadSplitViewLandscape) = 1 << 3, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewLandscape
ControllerSkinConfigurationiPadEdgeToEdgePortrait NS_SWIFT_NAME(ipadEdgeToEdgePortrait) = 1 << 8,
ControllerSkinConfigurationiPadEdgeToEdgeLandscape NS_SWIFT_NAME(ipadEdgeToEdgeLandscape) = 1 << 9,
/* TV */
ControllerSkinConfigurationTVStandardPortrait = 1 << 10,
ControllerSkinConfigurationTVStandardLandscape = 1 << 11,
}; };
#endif /* ControllerSkinConfigurations_h */ #endif /* ControllerSkinConfigurations_h */

View File

@ -1,83 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.78" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="LocalRecord" representedClassName="LocalRecord" syncable="YES">
<attribute name="modificationDate" attributeType="Date" syncable="YES"/>
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectURI" attributeType="URI" syncable="YES"/>
<attribute name="status" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="versionDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="versionIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="managedRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedRecord" inverseName="localRecord" inverseEntity="ManagedRecord" syncable="YES"/>
<relationship name="remoteFiles" toMany="YES" deletionRule="Cascade" destinationEntity="RemoteFile" inverseName="localRecord" inverseEntity="RemoteFile" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="recordedObjectType"/>
<constraint value="recordedObjectIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="ManagedRecord" representedClassName="ManagedRecord" syncable="YES">
<attribute name="isConflicted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="isSyncingEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
<relationship name="localRecord" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="LocalRecord" inverseName="managedRecord" inverseEntity="LocalRecord" syncable="YES"/>
<relationship name="remoteRecord" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="RemoteRecord" inverseName="managedRecord" inverseEntity="RemoteRecord" syncable="YES"/>
<fetchIndex name="byRecordedObject">
<fetchIndexElement property="recordedObjectType" type="Binary" order="ascending"/>
<fetchIndexElement property="recordedObjectIdentifier" type="Binary" order="ascending"/>
</fetchIndex>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="recordedObjectType"/>
<constraint value="recordedObjectIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="RemoteFile" representedClassName="RemoteFile" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="remoteIdentifier" attributeType="String" syncable="YES"/>
<attribute name="sha1Hash" attributeType="String" syncable="YES"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
<attribute name="versionIdentifier" attributeType="String" syncable="YES"/>
<relationship name="localRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LocalRecord" inverseName="remoteFiles" inverseEntity="LocalRecord" syncable="YES"/>
</entity>
<entity name="RemoteRecord" representedClassName="RemoteRecord" syncable="YES">
<attribute name="author" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isLocked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="localizedName" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="metadata" optional="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="previousVersionDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="previousVersionIdentifier" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
<attribute name="status" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="versionIdentifier" attributeType="String" syncable="YES"/>
<relationship name="managedRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedRecord" inverseName="remoteRecord" inverseEntity="ManagedRecord" syncable="YES"/>
<fetchIndex name="byIdentifier">
<fetchIndexElement property="identifier" type="Binary" order="ascending"/>
</fetchIndex>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="recordedObjectType"/>
<constraint value="recordedObjectIdentifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<configuration name="External"/>
<configuration name="Harmony">
<memberEntity name="ManagedRecord"/>
<memberEntity name="LocalRecord"/>
<memberEntity name="RemoteRecord"/>
<memberEntity name="RemoteFile"/>
</configuration>
<elements>
<element name="LocalRecord" positionX="93.01953125" positionY="273.0234375" width="128" height="180"/>
<element name="ManagedRecord" positionX="298.4375" positionY="225.49609375" width="128" height="133"/>
<element name="RemoteFile" positionX="288" positionY="378" width="128" height="135"/>
<element name="RemoteRecord" positionX="488.2578125" positionY="272.56640625" width="128" height="240"/>
</elements>
</model>

View File

@ -11,17 +11,15 @@ import Foundation
// Must be an NSObject subclass so it can be used with RSTCellContentDataSource. // Must be an NSObject subclass so it can be used with RSTCellContentDataSource.
class GameMetadata: NSObject class GameMetadata: NSObject
{ {
let releaseID: Int let identifier: Int
let romID: Int
let name: String? let name: String?
let artworkURL: URL? let artworkURL: URL?
init(releaseID: Int, romID: Int, name: String?, artworkURL: URL?) init(identifier: Int, name: String?, artworkURL: URL?)
{ {
self.releaseID = releaseID
self.romID = romID
self.name = name self.name = name
self.identifier = identifier
self.artworkURL = artworkURL self.artworkURL = artworkURL
} }
} }
@ -29,13 +27,13 @@ class GameMetadata: NSObject
extension GameMetadata extension GameMetadata
{ {
override var hash: Int { override var hash: Int {
return self.releaseID.hashValue ^ self.romID.hashValue return self.identifier.hashValue
} }
override func isEqual(_ object: Any?) -> Bool override func isEqual(_ object: Any?) -> Bool
{ {
guard let metadata = object as? GameMetadata else { return false } guard let metadata = object as? GameMetadata else { return false }
return self.releaseID == metadata.releaseID && self.romID == metadata.romID return self.identifier == metadata.identifier
} }
} }

View File

@ -57,26 +57,16 @@ extension VirtualTable
extension GamesDatabase extension GamesDatabase
{ {
enum Error: LocalizedError enum Error: Swift.Error
{ {
case doesNotExist case doesNotExist
case connection(Swift.Error)
var errorDescription: String? {
switch self
{
case .doesNotExist:
return NSLocalizedString("The SQLite database could not be found.", comment: "")
}
}
} }
} }
class GamesDatabase class GamesDatabase
{ {
static let version = 3 static let version = -1
static var previousVersion: Int? {
return UserDefaults.standard.previousGamesDatabaseVersion
}
private let connection: Connection private let connection: Connection
@ -90,7 +80,7 @@ class GamesDatabase
} }
catch catch
{ {
throw error throw Error.connection(error)
} }
self.invalidateVirtualTableIfNeeded() self.invalidateVirtualTableIfNeeded()
@ -99,11 +89,10 @@ class GamesDatabase
func metadataResults(forGameName gameName: String) -> [GameMetadata] func metadataResults(forGameName gameName: String) -> [GameMetadata]
{ {
let releaseID = Expression<Any>.releaseID let releaseID = Expression<Any>.releaseID
let romID = Expression<Any>.romID
let name = Expression<Any>.name let name = Expression<Any>.name
let artworkAddress = Expression<Any>.artworkAddress let artworkAddress = Expression<Any>.artworkAddress
let query = VirtualTable.search.select(releaseID, romID, name, artworkAddress).filter(name.match(gameName + "*")) let query = VirtualTable.search.select(releaseID, name, artworkAddress).filter(name.match(gameName + "*"))
do do
{ {
@ -122,7 +111,7 @@ class GamesDatabase
} }
let metadata = GameMetadata(releaseID: row[releaseID], romID: row[romID], name: row[name], artworkURL: artworkURL) let metadata = GameMetadata(identifier: row[releaseID], name: row[name], artworkURL: artworkURL)
return metadata return metadata
} }
@ -156,7 +145,7 @@ class GamesDatabase
let romID = Expression<Any>.romID let romID = Expression<Any>.romID
let gameHash = game.identifier.uppercased() let gameHash = game.identifier.uppercased()
let query = Table.roms.select(releaseID, name, artworkAddress, Table.roms[romID]).filter(sha1Hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID]) let query = Table.roms.select(releaseID, name, artworkAddress).filter(sha1Hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID])
do do
{ {
@ -172,7 +161,7 @@ class GamesDatabase
artworkURL = nil artworkURL = nil
} }
let metadata = GameMetadata(releaseID: row[releaseID], romID: row[Table.roms[romID]], name: row[name], artworkURL: artworkURL) let metadata = GameMetadata(identifier: row[releaseID], name: row[name], artworkURL: artworkURL)
return metadata return metadata
} }
} }
@ -208,13 +197,12 @@ private extension GamesDatabase
let name = Expression<Any>.name let name = Expression<Any>.name
let artworkAddress = Expression<Any>.artworkAddress let artworkAddress = Expression<Any>.artworkAddress
let releaseID = Expression<Any>.releaseID let releaseID = Expression<Any>.releaseID
let romID = Expression<Any>.romID
do do
{ {
try self.connection.run(VirtualTable.search.create(.FTS4([releaseID, romID, name, artworkAddress], tokenize: .Unicode61()))) try self.connection.run(VirtualTable.search.create(.FTS4([releaseID, name, artworkAddress], tokenize: .Unicode61())))
let update = VirtualTable.search.insert(Table.releases.select(releaseID, romID, name, artworkAddress)) let update = VirtualTable.search.insert(Table.releases.select(releaseID, name, artworkAddress))
_ = try self.connection.run(update) _ = try self.connection.run(update)
} }
catch catch

View File

@ -71,13 +71,6 @@ class GamesDatabaseBrowserViewController: UITableViewController
self.updatePlaceholderView() self.updatePlaceholderView()
} }
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
self.dataSource.searchController.isActive = true
}
override func didReceiveMemoryWarning() override func didReceiveMemoryWarning()
{ {
super.didReceiveMemoryWarning() super.didReceiveMemoryWarning()
@ -204,13 +197,6 @@ extension GamesDatabaseBrowserViewController
extension GamesDatabaseBrowserViewController: UISearchControllerDelegate extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
{ {
func didPresentSearchController(_ searchController: UISearchController)
{
DispatchQueue.main.async {
searchController.searchBar.becomeFirstResponder()
}
}
func willDismissSearchController(_ searchController: UISearchController) func willDismissSearchController(_ searchController: UISearchController)
{ {
// Manually set items to empty array to prevent crash if user dismissses searchController while scrolling // Manually set items to empty array to prevent crash if user dismissses searchController while scrolling

View File

@ -1,126 +0,0 @@
//
// GamePickerViewController.swift
// Delta
//
// Created by Riley Testut on 8/4/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class GamePickerViewController: UITableViewController
{
private lazy var dataSource = self.makeDataSource()
var gameHandler: ((Game?) -> Void)?
init()
{
super.init(style: .insetGrouped)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.navigationController?.delegate = self
self.dataSource.proxy = self
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.navigationItem.title = NSLocalizedString("Choose Game", comment: "")
self.navigationItem.searchController = self.dataSource.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
}
}
private extension GamePickerViewController
{
func makeDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource<Game, UIImage>
{
let fetchRequest = Game.fetchRequest()
fetchRequest.propertiesToFetch = [#keyPath(Game.name), #keyPath(Game.identifier), #keyPath(Game.artworkURL)]
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Game.gameCollection?.index, ascending: true), NSSortDescriptor(keyPath: \Game.name, ascending: true)]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(Game.gameCollection.name), cacheName: nil)
let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<Game, UIImage>(fetchedResultsController: fetchedResultsController)
dataSource.cellConfigurationHandler = { (cell, game, indexPath) in
var configuration = UIListContentConfiguration.valueCell()
configuration.prefersSideBySideTextAndSecondaryText = false
configuration.text = game.name
configuration.secondaryText = game.identifier
configuration.secondaryTextProperties.font = .preferredFont(forTextStyle: .caption1)
configuration.image = UIImage(named: "BoxArt")
configuration.imageProperties.maximumSize = CGSize(width: 48, height: 48)
configuration.imageProperties.reservedLayoutSize = CGSize(width: 48, height: 48)
configuration.imageProperties.cornerRadius = 4
cell.contentConfiguration = configuration
}
dataSource.prefetchHandler = { (game, indexPath, completionHandler) in
guard let artworkURL = game.artworkURL else {
completionHandler(nil, nil)
return nil
}
let imageOperation = LoadImageURLOperation(url: artworkURL)
imageOperation.resultHandler = { (image, error) in
completionHandler(image, error)
}
return imageOperation
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
guard let image = image, var config = cell.contentConfiguration as? UIListContentConfiguration else { return }
config.image = image
cell.contentConfiguration = config
}
dataSource.searchController.searchableKeyPaths = [#keyPath(Game.name), #keyPath(Game.identifier)]
return dataSource
}
}
extension GamePickerViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let game = self.dataSource.item(at: indexPath)
self.gameHandler?(game)
self.navigationController?.delegate = nil // Prevent calling navigationController(_:willShow:)
self.navigationController?.popViewController(animated: true)
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
guard let section = self.dataSource.fetchedResultsController.sections?[section], !section.name.isEmpty else {
return NSLocalizedString("Unknown System", comment: "")
}
return section.name
}
}
extension GamePickerViewController: UINavigationControllerDelegate
{
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool)
{
guard viewController != self else { return }
self.gameHandler?(nil)
}
}

View File

@ -1,479 +0,0 @@
//
// RepairDatabaseViewController.swift
// Delta
//
// Created by Riley Testut on 8/4/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import OSLog
import DeltaCore
import Roxas
import Harmony
private extension String
{
func sanitizedFilePath() -> String
{
let sanitizedFilePath = self.components(separatedBy: .urlFilenameAllowed.inverted).joined()
return sanitizedFilePath
}
}
class RepairDatabaseViewController: UIViewController
{
var completionHandler: (() -> Void)?
private var _viewDidAppear = false
private lazy var managedObjectContext = DatabaseManager.shared.newBackgroundSavingViewContext()
private lazy var gameSavesContext = DatabaseManager.shared.newBackgroundContext(withParent: self.managedObjectContext)
private var gamesByID: [String: Game]?
private lazy var backupsDirectory = FileManager.default.documentsDirectory.appendingPathComponent("Backups")
private lazy var gameSavesDirectory = DatabaseManager.gamesDirectoryURL
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = .systemBackground
self.isModalInPresentation = true
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.text = NSLocalizedString("Verifying Database…", comment: "")
placeholderView.detailTextLabel.text = nil
placeholderView.activityIndicatorView.startAnimating()
placeholderView.stackView.spacing = 15
self.view.addSubview(placeholderView, pinningEdgesWith: .zero)
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
if !_viewDidAppear
{
self.repairDatabase()
}
_viewDidAppear = true
}
}
private extension RepairDatabaseViewController
{
func repairDatabase()
{
Logger.database.info("Begin repairing database...")
self.repairGames { result in
switch result
{
case .failure(let error):
DispatchQueue.main.async {
let alertController = UIAlertController(title: "Unable to Repair Games", error: error)
self.present(alertController, animated: true)
}
case .success:
self.repairGameSaves { result in
DispatchQueue.main.async {
switch result
{
case .failure(let error):
let alertController = UIAlertController(title: "Unable to Repair Save Files", error: error)
self.present(alertController, animated: true)
case .success:
self.showReviewViewController()
}
}
}
}
}
}
func repairGames(completion: @escaping (Result<Void, Error>) -> Void)
{
self.managedObjectContext.perform {
do
{
let fetchRequest = Game.fetchRequest()
fetchRequest.propertiesToFetch = [#keyPath(Game.type)]
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(Game.gameCollection)]
let allGames = try self.managedObjectContext.fetch(fetchRequest)
let affectedGames = allGames.filter { $0.type.rawValue != $0.gameCollection?.identifier }
let gameCollections = try self.managedObjectContext.fetch(GameCollection.fetchRequest())
let gameCollectionsByID = gameCollections.reduce(into: [:]) { $0[$1.identifier] = $1 }
for game in affectedGames
{
let gameCollection = gameCollectionsByID[game.type.rawValue]
game.gameCollection = gameCollection
Logger.database.notice("Re-associating “\(game.name, privacy: .public)” with GameCollection: \(gameCollection?.identifier ?? "nil", privacy: .public)")
}
try self.managedObjectContext.save()
completion(.success)
}
catch
{
completion(.failure(error))
}
}
}
func repairGameSaves(completion: @escaping (Result<Void, Error>) -> Void)
{
self.managedObjectContext.perform {
do
{
// Fetch GameSaves that don't have same identifier as their Game,
// OR GameSaves that have a non-nil SHA1 hash.
//
// This covers GameSaves connected to wrong games and GameSaves with nil Games,
// as well as any GameSaves modified since last beta (which we assume are corrupted).
let fetchRequest = GameSave.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "(%K == nil) OR (%K != %K) OR (%K != nil)",
#keyPath(GameSave.game),
#keyPath(GameSave.identifier), #keyPath(GameSave.game.identifier),
#keyPath(GameSave.sha1))
let gameSaves = try self.managedObjectContext.fetch(fetchRequest)
let gameSavesByID = gameSaves.reduce(into: [:]) { $0[$1.identifier] = $1 }
let gamesFetchRequest = Game.fetchRequest()
gamesFetchRequest.predicate = NSPredicate(format: "%K IN %@", #keyPath(Game.identifier), Set(gameSavesByID.keys))
let games = try self.managedObjectContext.fetch(gamesFetchRequest)
self.gamesByID = games.reduce(into: [:]) { $0[$1.identifier] = $1 }
let savesBackupsDirectory = self.backupsDirectory.appendingPathComponent("Saves")
try FileManager.default.createDirectory(at: savesBackupsDirectory, withIntermediateDirectories: true)
var conflictedGames = Set<Game>()
for gameSave in gameSaves
{
let expectedGame = self.repair(gameSave, backupsDirectory: savesBackupsDirectory)
// At this point, gameSave is only updated in gameSavesContext,
// so gameSave here still points to previous game,
if let game = gameSave.game
{
Logger.database.notice("The save file for “\(game.name, privacy: .public)” is potentially corrupted, writing to conflicts.txt")
conflictedGames.insert(game)
}
if let expectedGame
{
Logger.database.notice("The save file for “\(expectedGame.name, privacy: .public)” is potentially corrupted, writing to conflicts.txt")
conflictedGames.insert(expectedGame)
}
}
try self.gameSavesContext.performAndWait {
try self.gameSavesContext.save()
}
try self.managedObjectContext.save()
let outputURL = self.backupsDirectory.appendingPathComponent("conflicts.txt")
let conflictsLog = conflictedGames.map { $0.name + " (" + $0.identifier + ")" }.sorted().joined(separator: "\n")
try conflictsLog.write(to: outputURL, atomically: true, encoding: .utf8)
completion(.success)
}
catch
{
completion(.failure(error))
}
}
}
// Returns expectedGame, but in managedObjectContext (not gameSavesContext)
func repair(_ gameSave: GameSave, backupsDirectory: URL) -> Game?
{
Logger.database.notice("Repairing GameSave \(gameSave.identifier, privacy: .public)...")
guard let expectedGame = self.gamesByID?[gameSave.identifier] else {
// Game doesn't exist, so we'll back up save file and delete record.
Logger.database.warning("Orphaning GameSave \(gameSave.identifier, privacy: .public) due to no matching game.")
do
{
try self.backup(gameSave, for: nil, to: backupsDirectory)
}
catch
{
Logger.database.error("Failed to back up save file for orphaned GameSave \(gameSave.identifier, privacy: .public). \(error, privacy: .public)")
}
self.gameSavesContext.performAndWait {
let gameSave = self.gameSavesContext.object(with: gameSave.objectID) as! GameSave
gameSave.game = nil
}
return nil
}
let misplacedGameSave: GameSave?
if let otherGameSave = expectedGame.gameSave, otherGameSave != gameSave
{
misplacedGameSave = otherGameSave
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public) will misplace \(otherGameSave.identifier, privacy: .public)")
}
else
{
misplacedGameSave = nil
}
do
{
// Back up the save file gameSave (incorrectly) refers to, but name it after the _expected_ game.
try self.backup(gameSave, for: expectedGame, to: backupsDirectory)
}
catch
{
Logger.database.error("Failed to back up save file for GameSave \(gameSave.identifier, privacy: .public). Expected Game: \(expectedGame.identifier). \(error, privacy: .public)")
}
// Ignore error if we can't hash file, not that big a deal.
let hash = try? RSTHasher.sha1HashOfFile(at: expectedGame.gameSaveURL)
// Make changes on separate context so we don't change any relationships until we're finished.
// This allows us to refer to previous relationships.
self.gameSavesContext.performAndWait {
let gameSave = self.gameSavesContext.object(with: gameSave.objectID) as! GameSave
let expectedGame = self.gameSavesContext.object(with: expectedGame.objectID) as! Game
let misplacedGameSave: GameSave? = misplacedGameSave.map { self.gameSavesContext.object(with: $0.objectID) as! GameSave }
if hash == gameSave.sha1
{
// .sav has same hash as GameSave SHA1,
// so we can relink without changes.
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public)'s hash matches .sav, relinking without changes.")
}
else if let misplacedGameSave
{
// GameSave data differs from actual .sav file,
// so copy metadata from misplacedGameSave.
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public)'s hash does NOT match .sav, ignoring misplaced GameSave \(misplacedGameSave.identifier, privacy: .public).")
// Not worth potential conflicts.
// gameSave.sha1 = misplacedGameSave.sha1
// gameSave.modifiedDate = misplacedGameSave.modifiedDate
}
else
{
// GameSave data differs from actual .sav file,
// so copy metadata from disk.
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public)'s hash does NOT match .sav, ignoring.")
// Not worth potential conflicts.
// let modifiedDate = try? FileManager.default.attributesOfItem(atPath: expectedGame.gameSaveURL.path)[.modificationDate] as? Date
// gameSave.sha1 = hash
// gameSave.modifiedDate = modifiedDate ?? Date()
}
gameSave.game = expectedGame
}
return expectedGame
}
func backup(_ gameSave: GameSave, for expectedGame: Game?, to backupsDirectory: URL) throws
{
Logger.database.notice("Backing up GameSave \(gameSave.identifier, privacy: .public). Expected Game: \(expectedGame?.name ?? "nil", privacy: .public)")
if let game = gameSave.game
{
// GameSave is linked with incorrect game.
// Prefer using expectedGame's saveFileExtension over game's.
let saveFileExtension: String
if let deltaCore = Delta.core(for: expectedGame?.type ?? game.type)
{
saveFileExtension = deltaCore.gameSaveFileExtension
}
else
{
saveFileExtension = "sav"
}
// 1. Backup existing file at `game`'s expected save file location
if FileManager.default.fileExists(atPath: game.gameSaveURL.path)
{
// Filename = expectedGame.name? + game.identifier
let filename: String
if let expectedGame
{
filename = expectedGame.name + "_" + game.identifier
}
else
{
filename = game.identifier
}
let sanitizedFilename = filename.sanitizedFilePath()
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension(saveFileExtension)
try FileManager.default.copyItem(at: game.gameSaveURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up save file \(game.gameSaveURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
let rtcFileURL = game.gameSaveURL.deletingPathExtension().appendingPathExtension("rtc")
if FileManager.default.fileExists(atPath: rtcFileURL.path)
{
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension("rtc")
try FileManager.default.copyItem(at: rtcFileURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up RTC save file \(rtcFileURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
}
}
// 2. Backup existing file at `expectedGame`'s save file location
if let expectedGame, FileManager.default.fileExists(atPath: expectedGame.gameSaveURL.path)
{
// Filename = expectedGame.name + (misplacedGameSave.identifier ?? expectedGame.identifier)
let filename = expectedGame.name + "_" + (expectedGame.gameSave?.identifier ?? expectedGame.identifier)
let sanitizedFilename = filename.sanitizedFilePath()
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension(saveFileExtension)
try FileManager.default.copyItem(at: expectedGame.gameSaveURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up expected save file \(expectedGame.gameSaveURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
let rtcFileURL = expectedGame.gameSaveURL.deletingPathExtension().appendingPathExtension("rtc")
if FileManager.default.fileExists(atPath: rtcFileURL.path)
{
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension("rtc")
try FileManager.default.copyItem(at: rtcFileURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up expected RTC save file \(rtcFileURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
}
}
}
else
{
@discardableResult
func backUp(_ saveFileURL: URL) throws -> Bool
{
guard FileManager.default.fileExists(atPath: saveFileURL.path) else { return false }
// Filename = expectedGame.name? + gameSave.identifier
let filename: String
if let expectedGame
{
filename = expectedGame.name + "_" + gameSave.identifier
}
else
{
filename = gameSave.identifier
}
let sanitizedFilename = filename.sanitizedFilePath()
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension(saveFileURL.pathExtension)
try FileManager.default.copyItem(at: saveFileURL, to: destinationURL, shouldReplace: true)
Logger.database.notice("Backed up discovered save file \(saveFileURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
return true
}
// GameSave is _not_ linked to a Game, so instead we iterate through all save files on disk to find match.
let savURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("sav")
let srmURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("srm")
let dsvURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("dsv")
let saveFileURLs = [savURL, srmURL, dsvURL]
for saveFileURL in saveFileURLs
{
if try backUp(saveFileURL)
{
break
}
}
// ALWAYS attempt to back up RTC file.
let rtcURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("rtc")
try backUp(rtcURL)
}
}
func showReviewViewController()
{
Logger.database.info("Finished repairing Games and GameSaves, reviewing recent SaveStates...")
let viewController = ReviewSaveStatesViewController()
viewController.filter = .sinceLastBeta
viewController.completionHandler = { [weak self] in
self?.finish()
}
self.navigationController?.pushViewController(viewController, animated: true)
}
func finish()
{
Logger.database.info("Finished repairing database!")
DispatchQueue.global(qos: .userInitiated).async {
if #available(iOS 15, *)
{
do
{
let store = try OSLogStore(scope: .currentProcessIdentifier)
// All logs since the app launched.
let position = store.position(timeIntervalSinceLatestBoot: 0)
let entries = try store.getEntries(at: position)
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == Logger.deltaSubsystem || $0.subsystem == Logger.harmonySubsystem }
.map { "[\($0.date.formatted())] [\($0.level.localizedName)] \($0.composedMessage)" }
let outputURL = self.backupsDirectory.appendingPathComponent("repair.log")
try FileManager.default.createDirectory(at: self.backupsDirectory, withIntermediateDirectories: true)
let outputText = entries.joined(separator: "\n")
try outputText.write(to: outputURL, atomically: true, encoding: .utf8)
}
catch
{
print("Failed to export Harmony logs.", error)
}
}
DispatchQueue.main.async {
let alertController = UIAlertController(title: NSLocalizedString("Database Repaired", comment: ""),
message: NSLocalizedString("Some save files may still be corrupted and require you to restore an older version from the Delta Sync settings.\n\nA text file listing all affected games has been saved to “On My Device/Delta/Backups/conflicts.txt” in the Files app, alongside backups of any conflicted save files.", comment: ""),
preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.ok.title, style: UIAlertAction.ok.style) { _ in
self.completionHandler?()
})
self.present(alertController, animated: true)
}
}
}
}

View File

@ -1,377 +0,0 @@
//
// ReviewSaveStatesViewController.swift
// Delta
//
// Created by Riley Testut on 8/4/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import OSLog
import Harmony
import Roxas
extension ReviewSaveStatesViewController
{
enum Filter
{
case recent
case all
case sinceLastBeta
}
}
extension RecordFlags
{
static let isGameRelationshipVerified = RecordFlags(rawValue: 1 << 0)
}
class ReviewSaveStatesViewController: UITableViewController
{
var filter: Filter = .recent {
didSet {
self.updateDataSource()
}
}
var completionHandler: (() -> Void)?
private lazy var managedObjectContext = DatabaseManager.shared.newBackgroundSavingViewContext()
private lazy var dataSource = self.makeDataSource()
private lazy var descriptionDataSource = self.makeDescriptionDataSource()
private lazy var saveStatesDataSource = self.makeSaveStatesDataSource()
private weak var _parentNavigationController: UINavigationController?
init()
{
super.init(style: .insetGrouped)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.dataSource.proxy = self
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(ReviewSaveStatesViewController.finish))
self.navigationItem.rightBarButtonItem = doneButton
self.navigationItem.title = NSLocalizedString("Review Save States", comment: "")
// Disable going back to RepairDatabaseViewController.
self.navigationItem.setHidesBackButton(true, animated: false)
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if let parent = self.parent, parent.navigationItem.title == nil
{
// Must change parent's navigationItem when we're contained in SwiftUI View.
parent.navigationItem.title = NSLocalizedString("Review Save States", comment: "")
parent.navigationItem.rightBarButtonItem = self.makeFilterButton()
}
}
override func viewWillDisappear(_ animated: Bool)
{
super.viewWillDisappear(animated)
_parentNavigationController = self.parent?.navigationController
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
switch self.filter
{
case .all, .recent:
if self.parent == nil || self.parent?.parent == nil
{
// Only finish if we're popped off navigation controller.
self.finish()
}
case .sinceLastBeta: break
}
}
}
private extension ReviewSaveStatesViewController
{
func makeDataSource() -> RSTCompositeTableViewPrefetchingDataSource<SaveState, UIImage>
{
let dataSource = RSTCompositeTableViewPrefetchingDataSource<SaveState, UIImage>(dataSources: [self.descriptionDataSource, self.saveStatesDataSource])
return dataSource
}
func makeDescriptionDataSource() -> RSTDynamicTableViewPrefetchingDataSource<SaveState, UIImage>
{
let dataSource = RSTDynamicTableViewPrefetchingDataSource<SaveState, UIImage>()
dataSource.numberOfSectionsHandler = { 1 }
dataSource.numberOfItemsHandler = { _ in 0 }
return dataSource
}
func makeSaveStatesDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource<SaveState, UIImage>
{
let fetchedResultsController = self.makeSaveStatesFetchedResultsController()
let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<SaveState, UIImage>(fetchedResultsController: fetchedResultsController)
dataSource.cellConfigurationHandler = { (cell, saveState, indexPath) in
var configuration = UIListContentConfiguration.valueCell()
configuration.prefersSideBySideTextAndSecondaryText = false
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold) ?? .preferredFontDescriptor(withTextStyle: .body)
configuration.text = saveState.name ?? NSLocalizedString("Untitled", comment: "")
configuration.textProperties.font = UIFont(descriptor: fontDescriptor, size: 0)
configuration.secondaryText = SaveState.localizedDateFormatter.string(from: saveState.modifiedDate)
configuration.secondaryTextProperties.font = .preferredFont(forTextStyle: .caption1)
configuration.image = nil
configuration.imageProperties.maximumSize = CGSize(width: 80, height: 80)
configuration.imageProperties.reservedLayoutSize = CGSize(width: 80, height: 80)
configuration.imageProperties.cornerRadius = 6
cell.contentConfiguration = configuration
cell.accessoryType = .disclosureIndicator
}
dataSource.prefetchHandler = { (saveState, indexPath, completionHandler) in
guard saveState.game != nil else {
completionHandler(nil, nil)
return nil
}
let imageOperation = LoadImageURLOperation(url: saveState.imageFileURL)
imageOperation.resultHandler = { (image, error) in
completionHandler(image, error)
}
if self.isAppearing
{
imageOperation.start()
imageOperation.waitUntilFinished()
return nil
}
return imageOperation
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
guard let image = image, var config = cell.contentConfiguration as? UIListContentConfiguration else { return }
config.image = image
cell.contentConfiguration = config
}
return dataSource
}
func makeSaveStatesFetchedResultsController() -> NSFetchedResultsController<SaveState>
{
let fetchRequest = SaveState.fetchRequest()
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \SaveState.game?.name, ascending: true), NSSortDescriptor(keyPath: \SaveState.modifiedDate, ascending: false)]
let predicate = NSPredicate(format: "%K != %@", #keyPath(SaveState.type), SaveStateType.auto.rawValue as NSNumber)
switch self.filter
{
case .recent:
let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) ?? Date().addingTimeInterval(-1 * 60 * 60 * 24 * 30)
let recentPredicate = NSPredicate(format: "%K > %@", #keyPath(SaveState.modifiedDate), oneMonthAgo as NSDate)
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, recentPredicate])
case .all:
fetchRequest.predicate = predicate
case .sinceLastBeta:
let dateComponents = DateComponents(year: 2023, month: 7, day: 18, hour: 0, minute: 0, second: 0)
let lastBetaDate = Calendar.current.date(from: dateComponents) ?? Date().addingTimeInterval(-1 * 60 * 60 * 24 * 45)
let sinceLastBetaPredicate = NSPredicate(format: "%K > %@", #keyPath(SaveState.modifiedDate), lastBetaDate as NSDate)
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, sinceLastBetaPredicate])
}
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: #keyPath(SaveState.game.name), cacheName: nil)
return fetchedResultsController
}
func updateDataSource()
{
let fetchedResultsController = self.makeSaveStatesFetchedResultsController()
self.saveStatesDataSource.fetchedResultsController = fetchedResultsController
}
func makeFilterButton() -> UIBarButtonItem
{
let recentAction = UIAction(title: NSLocalizedString("Past Month", comment: ""), image: UIImage(systemName: "calendar")) { [weak self] _ in
self?.filter = .recent
}
let allAction = UIAction(title: NSLocalizedString("All Time", comment: ""), image: UIImage(systemName: "clock")) { [weak self] _ in
self?.filter = .all
}
var options: UIMenu.Options = []
if #available(iOS 15, *)
{
options = .singleSelection
recentAction.state = self.filter == .recent ? .on : .off
allAction.state = self.filter == .all ? .on : .off
}
let filterMenu = UIMenu(options: options, children: [recentAction, allAction])
let filterButton = UIBarButtonItem(title: NSLocalizedString("Filter", comment: ""), image: UIImage(systemName: "calendar.badge.clock"), menu: filterMenu)
return filterButton
}
}
private extension ReviewSaveStatesViewController
{
func pickGame(for saveState: SaveState)
{
let gamePickerViewController = GamePickerViewController()
gamePickerViewController.gameHandler = { game in
guard let game else { return }
let previousGame = saveState.game
if previousGame != nil
{
// Move files to new location.
let destinationDirectory = DatabaseManager.saveStatesDirectoryURL(for: game)
for fileURL in [saveState.fileURL, saveState.imageFileURL]
{
guard FileManager.default.fileExists(atPath: fileURL.path) else { continue }
let destinationURL = destinationDirectory.appendingPathComponent(fileURL.lastPathComponent)
do
{
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true) // Copy, don't move, in case app quits before user confirms.
}
catch
{
Logger.database.error("Failed to copy SaveState “\(saveState.localizedName, privacy: .public)” from \(fileURL, privacy: .public) to \(destinationURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
}
}
}
let tempGame = self.managedObjectContext.object(with: game.objectID) as! Game
saveState.game = tempGame
Logger.database.notice("Re-associated SaveState “\(saveState.localizedName, privacy: .public)” with game “\(tempGame.name, privacy: .public)”. Previously: \(previousGame?.name ?? "nil", privacy: .public)")
}
self.navigationController?.pushViewController(gamePickerViewController, animated: true)
}
@objc func finish()
{
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = true
self.managedObjectContext.perform {
do
{
let saveStates: [SaveState]?
switch self.filter
{
case .recent, .all:
// Only upload metadata for changed SaveStates.
saveStates = self.managedObjectContext.updatedObjects.compactMap { $0 as? SaveState }
case .sinceLastBeta:
// Upload metadata for _all_ SaveStates.
saveStates = self.saveStatesDataSource.fetchedResultsController.fetchedObjects
}
try self.managedObjectContext.save()
if let saveStates = saveStates, let coordinator = SyncManager.shared.coordinator
{
let records = try coordinator.recordController.fetchRecords(for: saveStates)
if let context = records.first?.recordedObject?.managedObjectContext
{
try context.performAndWait {
for record in records
{
record.perform { managedRecord in
managedRecord.flags.insert(.isGameRelationshipVerified)
managedRecord.setNeedsMetadataUpdate()
let saveState = record.recordedObject
Logger.database.notice("Flagged SaveState “\(saveState?.localizedName ?? record.recordID.identifier, privacy: .public)” for metadata update.")
}
}
try context.save()
}
}
}
DispatchQueue.main.async {
self.completionHandler?()
}
}
catch
{
DispatchQueue.main.async {
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
let alertController = UIAlertController(title: NSLocalizedString("Unable to Save Changes", comment: ""), error: error)
(self._parentNavigationController ?? self).present(alertController, animated: true)
}
}
}
}
}
extension ReviewSaveStatesViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let saveState = self.dataSource.item(at: indexPath)
self.pickGame(for: saveState)
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
if section == 0
{
return nil
}
else
{
let section = section - 1
guard let gameName = self.saveStatesDataSource.fetchedResultsController.sections?[section].name else { return NSLocalizedString("Unknown Game", comment: "") }
return gameName
}
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{
guard section == 0 else { return nil }
return NSLocalizedString("These save states have been modified recently and may be associated with the wrong game.\n\nPlease change any incorrectly associated save states to the correct game by tapping them.", comment: "")
}
}

View File

@ -1,67 +0,0 @@
//
// CopyDeepLinkActivity.swift
// Delta
//
// Created by Riley Testut on 8/5/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
extension UIActivity.ActivityType
{
static let copyDeepLink = UIActivity.ActivityType("com.rileytestut.Delta.CopyDeepLink")
}
class CopyDeepLinkActivity: UIActivity
{
private var deepLink: URL?
override class var activityCategory: UIActivity.Category {
return .action
}
override var activityType: UIActivity.ActivityType? {
return .copyDeepLink
}
override var activityTitle: String? {
return NSLocalizedString("Copy Deep Link", comment: "")
}
override var activityImage: UIImage? {
return UIImage(symbolNameIfAvailable: "link") ?? UIImage(named: "Link")
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool
{
if activityItems.contains(where: { $0 is Game })
{
return true
}
else
{
return false
}
}
override func prepare(withActivityItems activityItems: [Any])
{
guard let game = activityItems.first(where: { $0 is Game }) as? Game else { return }
self.deepLink = URL(action: .launchGame(identifier: game.identifier))
}
override func perform()
{
if let deepLink = self.deepLink
{
UIPasteboard.general.url = deepLink
self.activityDidFinish(true)
}
else
{
self.activityDidFinish(false)
}
}
}

View File

@ -12,12 +12,16 @@ extension URL
{ {
init(action: DeepLink.Action) init(action: DeepLink.Action)
{ {
var components = URLComponents()
components.host = action.type.rawValue
switch action switch action
{ {
case .launchGame(let identifier): case .launchGame(let identifier): components.path = identifier
let deepLinkURL = URL(string: "delta://\(action.type.rawValue)/\(identifier)")!
self = deepLinkURL
} }
let url = components.url!
self = url
} }
} }

View File

@ -23,17 +23,9 @@ extension UIViewController
struct DeepLinkController struct DeepLinkController
{ {
private var window: UIWindow? { private var window: UIWindow? {
if #available(iOS 13, *)
{
guard let delegate = UIApplication.shared.connectedScenes.lazy.compactMap({ $0.delegate as? UIWindowSceneDelegate }).first, let window = delegate.window else { return nil }
return window
}
else
{
guard let delegate = UIApplication.shared.delegate, let window = delegate.window else { return nil } guard let delegate = UIApplication.shared.delegate, let window = delegate.window else { return nil }
return window return window
} }
}
private var topViewController: UIViewController? { private var topViewController: UIViewController? {
guard let window = self.window else { return nil } guard let window = self.window else { return nil }

View File

@ -18,7 +18,6 @@ enum ActionInput: String
case quickSave case quickSave
case quickLoad case quickLoad
case fastForward case fastForward
case toggleFastForward
} }
extension ActionInput: Input extension ActionInput: Input

File diff suppressed because it is too large Load Diff

View File

@ -15,31 +15,19 @@ private var kvoContext = 0
class PreviewGameViewController: DeltaCore.GameViewController class PreviewGameViewController: DeltaCore.GameViewController
{ {
// If non-nil, will override the default preview action items returned in previewActionItems() // If non-nil, will override the default preview action items returned in previewActionItems()
// nilpreviewActionItems()
var overridePreviewActionItems: [UIPreviewActionItem]? var overridePreviewActionItems: [UIPreviewActionItem]?
// Save state to be loaded upon preview // Save state to be loaded upon preview
//
var previewSaveState: SaveStateProtocol? var previewSaveState: SaveStateProtocol?
// Initial image to be shown while loading // Initial image to be shown while loading
//
var previewImage: UIImage? { var previewImage: UIImage? {
didSet { didSet {
self.updatePreviewImage() self.updatePreviewImage()
} }
} }
var isLivePreview: Bool = true
private var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated) private var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated)
private var copiedSaveFiles = [(originalURL: URL, copyURL: URL)]()
private lazy var temporaryDirectoryURL: URL = {
let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent("preview-" + UUID().uuidString)
try? FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
return directoryURL
}()
override var game: GameProtocol? { override var game: GameProtocol? {
willSet { willSet {
@ -53,8 +41,7 @@ class PreviewGameViewController: DeltaCore.GameViewController
emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext) emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext)
let size = CGSize(width: emulatorCore.preferredRenderingSize.width * 2.0, height: emulatorCore.preferredRenderingSize.height * 2.0) self.preferredContentSize = emulatorCore.preferredRenderingSize
self.preferredContentSize = size
} }
} }
@ -63,28 +50,8 @@ class PreviewGameViewController: DeltaCore.GameViewController
return previewActionItems return previewActionItems
} }
public required init()
{
super.init()
self.delegate = self
}
public required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.delegate = self
}
deinit deinit
{ {
// Explicitly stop emulatorCore _before_ we remove ourselves as observer
// so we can wait until stopped before restoring save files (again).
// emulatorCore
//
self.emulatorCore?.stop()
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext) self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
} }
} }
@ -98,26 +65,17 @@ extension PreviewGameViewController
super.viewDidLoad() super.viewDidLoad()
self.controllerView.isHidden = true self.controllerView.isHidden = true
self.controllerView.controllerSkin = nil // Skip loading controller skin from disk, which may be slow.
// Temporarily prevent emulatorCore from updating gameView to prevent flicker of black, or other visual glitches // Temporarily prevent emulatorCore from updating gameView to prevent flicker of black, or other visual glitches
// emulatorCore gameView
self.emulatorCore?.remove(self.gameView) self.emulatorCore?.remove(self.gameView)
} }
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.copySaveFiles()
}
override func viewDidAppear(_ animated: Bool) override func viewDidAppear(_ animated: Bool)
{ {
super.viewDidAppear(animated) super.viewDidAppear(animated)
self.emulatorCoreQueue.async { self.emulatorCoreQueue.async {
self.startEmulation() self.emulatorCore?.resume()
} }
} }
@ -126,33 +84,14 @@ extension PreviewGameViewController
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
// Pause in viewWillDisappear and not viewDidDisappear like DeltaCore.GameViewController so the audio cuts off earlier if being dismissed // Pause in viewWillDisappear and not viewDidDisappear like DeltaCore.GameViewController so the audio cuts off earlier if being dismissed
// viewWillDisappear DeltaCore.GameViewController viewDidDisappear
self.emulatorCore?.pause() self.emulatorCore?.pause()
} }
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
// Already stopped = we've already restored save files and removed directory.
// =
if self.emulatorCore?.state != .stopped
{
// Pre-emptively restore save files in case something goes wrong while stopping emulation.
// This also ensures if the core is never stopped (for some reason), saves are still restored.
// 仿
//
self.restoreSaveFiles(removeCopyDirectory: false)
}
}
override func viewDidLayoutSubviews() override func viewDidLayoutSubviews()
{ {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
// Need to update in viewDidLayoutSubviews() to ensure bounds of gameView are not CGRect.zero // Need to update in viewDidLayoutSubviews() to ensure bounds of gameView are not CGRect.zero
// viewDidLayoutSubviews() gameView CGRect.zero
self.updatePreviewImage() self.updatePreviewImage()
} }
@ -160,7 +99,6 @@ extension PreviewGameViewController
{ {
super.didReceiveMemoryWarning() super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated. // Dispose of any resources that can be recreated.
//
} }
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
@ -173,24 +111,17 @@ extension PreviewGameViewController
let state = self.emulatorCore?.state let state = self.emulatorCore?.state
else { return } else { return }
switch state if previousState == .stopped, state == .running
{
self.emulatorCoreQueue.sync {
if self.isAppearing
{ {
case .running where previousState == .stopped:
self.emulatorCoreQueue.async {
// Pause to prevent it from starting before visible (in case user peeked slowly) // Pause to prevent it from starting before visible (in case user peeked slowly)
//
self.emulatorCore?.pause() self.emulatorCore?.pause()
self.preparePreview()
} }
case .stopped: self.preparePreview()
// Emulation has stopped, so we can safely restore save files, }
// and also remove the directory they were copied to.
//
//
self.restoreSaveFiles(removeCopyDirectory: true)
default: break
} }
} }
} }
@ -240,69 +171,6 @@ private extension PreviewGameViewController
self.emulatorCore?.updateCheats() self.emulatorCore?.updateCheats()
// Re-enable emulatorCore to update gameView again // Re-enable emulatorCore to update gameView again
// emulatorCoregameView
self.emulatorCore?.add(self.gameView) self.emulatorCore?.add(self.gameView)
self.emulatorCore?.resume()
}
func copySaveFiles()
{
guard let game = self.game as? Game, let gameSave = game.gameSave else { return }
self.copiedSaveFiles.removeAll()
let fileURLs = gameSave.syncableFiles.lazy.map { $0.fileURL }
for fileURL in fileURLs
{
do
{
let destinationURL = self.temporaryDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
self.copiedSaveFiles.append((fileURL, destinationURL))
print("Copied save file:", fileURL.lastPathComponent)
}
catch
{
print("Failed to back up save file \(fileURL.lastPathComponent).", error)
}
}
}
func restoreSaveFiles(removeCopyDirectory: Bool)
{
for (originalURL, copyURL) in self.copiedSaveFiles
{
do
{
try FileManager.default.copyItem(at: copyURL, to: originalURL, shouldReplace: true)
print("Restored save file:", originalURL.lastPathComponent)
}
catch
{
print("Failed to restore copied save file \(copyURL.lastPathComponent).", error)
}
}
if removeCopyDirectory
{
do
{
try FileManager.default.removeItem(at: self.temporaryDirectoryURL)
}
catch
{
print("Failed to remove preview temporary directory.", error)
}
}
}
}
extension PreviewGameViewController: GameViewControllerDelegate
{
func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool
{
return self.isLivePreview
} }
} }

View File

@ -1,53 +0,0 @@
//
// ExperimentalFeatures.swift
// Delta
//
// Created by Riley Testut on 4/6/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import DeltaFeatures
struct ExperimentalFeatures: FeatureContainer
{
static let shared = ExperimentalFeatures()
@Feature(name: "AirPlay Skins",
description: "Customize the appearance of games when AirPlaying to your TV.",
options: AirPlaySkinsOptions())
var airPlaySkins
@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
@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
@Feature(name: "Review Save States",
description: "Review recent Save States to make sure they are associated with the correct game.",
options: ReviewSaveStatesOptions())
var reviewSaveStates
@Feature(name: "Alternate App Icon",
description: "Change the app icon.",
options: AlternateAppIconOptions())
var alternateAppIcons
private init()
{
self.prepareFeatures()
}
}

View File

@ -1,167 +0,0 @@
//
// AirPlaySkins.swift
// Delta
//
// Created by Riley Testut on 4/20/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import DeltaFeatures
import DeltaCore
extension Feature where Options == AirPlaySkinsOptions
{
func preferredAirPlayControllerSkin(for gameType: GameType) -> ControllerSkin?
{
guard let identifier = self[gameType] else { return nil }
let predicate = NSPredicate(format: "%K == %@", #keyPath(ControllerSkin.identifier), identifier)
let controllerSkin = ControllerSkin.instancesWithPredicate(predicate, inManagedObjectContext: DatabaseManager.shared.viewContext, type: ControllerSkin.self).first
return controllerSkin
}
}
struct AirPlaySkinsOptions
{
@Option(name: "Manage Skins", detailView: { _ in SkinManager() })
private var skinManager: String = "" // Hack until I figure out how to support Void properties...
@Option(name: LocalizedStringKey(System.nes.localizedName), description: "The controller skin used when AirPlaying NES games.", detailView: { SkinPicker(gameType: .nes, controllerSkinID: $0) })
var nes: String?
@Option(name: LocalizedStringKey(System.snes.localizedName), description: "The controller skin used when AirPlaying SNES games.", detailView: { SkinPicker(gameType: .snes, controllerSkinID: $0) })
var snes: String?
@Option(name: LocalizedStringKey(System.genesis.localizedName), description: "The controller skin used when AirPlaying Genesis games.", detailView: { SkinPicker(gameType: .genesis, controllerSkinID: $0) })
var genesis: String?
@Option(name: LocalizedStringKey(System.n64.localizedName), description: "The controller skin used when AirPlaying N64 games.", detailView: { SkinPicker(gameType: .n64, controllerSkinID: $0) })
var n64: String?
@Option(name: LocalizedStringKey(System.gbc.localizedName), description: "The controller skin used when AirPlaying GBC games.", detailView: { SkinPicker(gameType: .gbc, controllerSkinID: $0) })
var gbc: String?
@Option(name: LocalizedStringKey(System.gba.localizedName), description: "The controller skin used when AirPlaying GBA games.", detailView: { SkinPicker(gameType: .gba, controllerSkinID: $0) })
var gba: String?
@Option(name: LocalizedStringKey(System.ds.localizedName), description: "The controller skin used when AirPlaying DS games.", detailView: { SkinPicker(gameType: .ds, controllerSkinID: $0) })
var ds: String?
subscript(gameType: GameType) -> String? {
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
}
}
}
fileprivate extension AirPlaySkinsOptions
{
struct SkinPicker: View
{
let gameType: GameType
@Binding
var controllerSkinID: String?
@FetchRequest
private var controllerSkins: FetchedResults<ControllerSkin>
@Environment(\.featureOption)
private var option
var body: some View {
Picker(option.name ?? "", selection: $controllerSkinID) {
ForEach(controllerSkins, id: \.identifier) { controllerSkin in
Text(controllerSkin.name)
.tag(Optional<String>(controllerSkin.identifier)) // Must be Optional<String> in order for selection to work.
// .tag(controllerSkin.identifier)
}
Text("None")
.tag(String?.none)
}
.pickerStyle(.menu)
.displayInline()
}
init(gameType: GameType, controllerSkinID: Binding<String?>)
{
self.gameType = gameType
self._controllerSkinID = controllerSkinID
let configuration = ControllerSkinConfigurations.tvStandardLandscape
let predicate = NSPredicate(format: "%K == %@ AND (%K & %d) != 0 AND %K == NO",
#keyPath(ControllerSkin.gameType), self.gameType.rawValue,
#keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue,
#keyPath(ControllerSkin.isStandard))
self._controllerSkins = FetchRequest(entity: ControllerSkin.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)], predicate: predicate)
}
}
struct SkinManager: View
{
@FetchRequest(entity: ControllerSkin.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)],
predicate: {
let configuration = ControllerSkinConfigurations.tvStandardLandscape
return NSPredicate(format: "(%K & %d) != 0 AND %K == NO",
#keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue,
#keyPath(ControllerSkin.isStandard))
}())
private var controllerSkins: FetchedResults<ControllerSkin>
var body: some View {
if controllerSkins.isEmpty
{
Text("No AirPlay Skins")
.foregroundColor(.gray)
}
else
{
List {
ForEach(controllerSkins, id: \.identifier) { controllerSkin in
HStack {
Text(controllerSkin.name)
Spacer()
if let system = System(gameType: controllerSkin.gameType)
{
Text(system.localizedShortName)
.foregroundColor(.gray)
}
}
}
.onDelete(perform: deleteAirPlaySkins)
}
}
}
private func deleteAirPlaySkins(at indexes: IndexSet)
{
let objectIDs = indexes.map { controllerSkins[$0].objectID }
DatabaseManager.shared.performBackgroundTask { context in
let controllerSkins = objectIDs.compactMap { context.object(with: $0) as? ControllerSkin }
for controllerSkin in controllerSkins
{
context.delete(controllerSkin)
}
context.saveWithErrorLogging()
}
}
}
}

View File

@ -1,124 +0,0 @@
//
// AlternateAppIcons.swift
// Delta
//
// Created by Chris Rittenhouse on 5/2/23.
// Copyright © 2023 LitRitt. All rights reserved.
//
import SwiftUI
import DeltaFeatures
enum AppIcon: String, CaseIterable, CustomStringConvertible, Identifiable
{
case normal = "Default"
case gba4ios = "GBA4iOS"
case inverted = "Inverted"
case pixelated = "Pixelated"
case skin = "Controller Skin"
var description: String {
return self.rawValue
}
var id: String {
return self.rawValue
}
var author: String {
switch self
{
case .normal: return "Caroline Moore"
case .gba4ios: return "Paul Thorsen"
case .inverted, .skin, .pixelated: return "LitRitt"
}
}
var assetName: String {
switch self
{
case .normal: return "AppIcon"
case .gba4ios: return "IconGBA4iOS"
case .inverted: return "IconInverted"
case .pixelated: return "IconPixelated"
case .skin: return "IconSkin"
}
}
}
extension AppIcon: Equatable
{
static func == (lhs: AppIcon, rhs: AppIcon) -> Bool
{
return lhs.description == rhs.description
}
}
extension AppIcon: LocalizedOptionValue
{
var localizedDescription: Text {
Text(self.description)
}
}
struct AlternateAppIconOptions
{
@Option(name: "Alternate App Icon",
description: "Choose from alternate app icons created by the community.",
detailView: { value in
List {
ForEach(AppIcon.allCases) { icon in
HStack {
if icon == value.wrappedValue
{
Text("")
}
icon.localizedDescription
Text("- by \(icon.author)")
.font(.system(size: 15))
.foregroundColor(.gray)
Spacer()
Image(uiImage: Bundle.appIcon(for: icon) ?? UIImage())
.cornerRadius(13)
}
.contentShape(Rectangle())
.onTapGesture {
value.wrappedValue = icon
}
}
}
.onChange(of: value.wrappedValue) { _ in
updateAppIcon()
}
.displayInline()
})
var icon: AppIcon = .normal
}
extension AlternateAppIconOptions
{
static func updateAppIcon()
{
// Get current icon
let currentIcon = UIApplication.shared.alternateIconName
// Apply chosen icon if feature is enabled
if ExperimentalFeatures.shared.alternateAppIcons.isEnabled
{
let icon = ExperimentalFeatures.shared.alternateAppIcons.icon
// Only apply new icon if it's not already the current icon
switch icon
{
case .normal: if currentIcon != nil { UIApplication.shared.setAlternateIconName(nil) } // Default app icon
default: if currentIcon != icon.assetName { UIApplication.shared.setAlternateIconName(icon.assetName) } // Alternate app icon
}
}
else
{
// Remove alternate icons if feature is disabled
if currentIcon != nil { UIApplication.shared.setAlternateIconName(nil) }
}
}
}

View File

@ -1,54 +0,0 @@
//
// 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

@ -1,30 +0,0 @@
//
// LinkSaveStatesOptions.swift
// Delta
//
// Created by Riley Testut on 8/7/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import DeltaFeatures
struct ReviewSaveStatesView: UIViewControllerRepresentable
{
func makeUIViewController(context: Context) -> ReviewSaveStatesViewController
{
let viewController = ReviewSaveStatesViewController()
return viewController
}
func updateUIViewController(_ uiViewController: ReviewSaveStatesViewController, context: Context)
{
}
}
struct ReviewSaveStatesOptions
{
@Option(name: "View Save States", detailView: { _ in ReviewSaveStatesView() })
private var reviewSaveStates: String = "" // Hack until I figure out how to support Void properties...
}

View File

@ -1,38 +0,0 @@
//
// 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

@ -1,129 +0,0 @@
//
// VariableFastForward.swift
// Delta
//
// Created by Riley Testut on 4/5/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import SwiftUI
import DeltaCore
import DeltaFeatures
struct FastForwardSpeed: RawRepresentable
{
let rawValue: Double
init(rawValue: Double)
{
self.rawValue = rawValue
}
static func speeds(in range: ClosedRange<Double>) -> [FastForwardSpeed]
{
var range = range
if ExperimentalFeatures.shared.variableFastForward.allowUnrestrictedSpeeds
{
range = 1.0...8.0
}
// .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, *)
{
let formattedText = self.rawValue.formatted(.number.decimalSeparator(strategy: .automatic))
return "\(formattedText)x"
}
else
{
return "\(self.rawValue)x"
}
}
var localizedDescription: Text {
Text(self.description)
}
static var localizedNilDescription: Text {
Text("Maximum")
}
}
struct VariableFastForwardOptions
{
// Alternatively, this feature could be implemented with single hidden dictionary @Option mapping preferred speeds to systems,
// because we support changing these values by long-pressing the Fast Forward button in the pause menu.
// However, we want to also show these options in Delta's settings, which requires us to explicitly define them one-by-one.
//
// @Option // No name = hidden
// var preferredSpeedsBySystem: [String: Double] = [:]
@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.speeds(in: System.snes.deltaCore.supportedRates))
var snes: FastForwardSpeed?
@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.speeds(in: System.n64.deltaCore.supportedRates))
var n64: FastForwardSpeed?
@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.speeds(in: System.gba.deltaCore.supportedRates))
var gba: FastForwardSpeed?
@Option(name: "Nintendo DS", description: "Preferred DS fast forward speed.", values: FastForwardSpeed.speeds(in: System.ds.deltaCore.supportedRates))
var ds: FastForwardSpeed?
@Option(name: "Allow Unrestricted Speeds", description: "Allow choosing speeds that exceed the maximum supported speed of a system.\n\nThis can be used to test the performance of new iOS devices.")
var allowUnrestrictedSpeeds: Bool = false
}
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

@ -1,34 +0,0 @@
//
// Bundle+AppIconImage.swift
// Delta
//
// Created by Chris Rittenhouse on 7/20/23.
// Copyright © 2023 LitRitt. All rights reserved.
//
import UIKit
extension Bundle
{
static func appIcon(for icon: AppIcon = .normal) -> UIImage? {
guard let appIcons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any] else { return nil }
switch icon
{
case .normal:
guard let primaryAppIcon = appIcons["CFBundlePrimaryIcon"] as? [String: Any],
let appIconFiles = primaryAppIcon["CFBundleIconFiles"] as? [String],
let appIcon = appIconFiles.first else { return nil }
return UIImage(named:appIcon)
default:
guard let alternateAppIcons = appIcons["CFBundleAlternateIcons"] as? [String: Any],
let alternateAppIcon = alternateAppIcons[icon.assetName] as? [String: Any],
let appIconFiles = alternateAppIcon["CFBundleIconFiles"] as? [String],
let appIcon = appIconFiles.first else { return nil }
return UIImage(named:appIcon)
}
}
}

View File

@ -1,56 +0,0 @@
//
// Bundle+SwizzleBundleID.swift
// Delta
//
// Created by Riley Testut on 8/7/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import ObjectiveC.runtime
extension Bundle
{
@objc private var swizzled_infoDictionary: [String : Any]? {
var infoDictionary = self.swizzled_infoDictionary
#if LITE
#if BETA
infoDictionary?[kCFBundleIdentifierKey as String] = "com.rileytestut.Delta.Lite.Beta"
#else
infoDictionary?[kCFBundleIdentifierKey as String] = "com.rileytestut.Delta.Lite"
#endif
#else
#if BETA
infoDictionary?[kCFBundleIdentifierKey as String] = "com.rileytestut.Delta.AltStore.Beta"
#else
infoDictionary?[kCFBundleIdentifierKey as String] = "com.rileytestut.Delta.AltStore"
#endif
#endif
return infoDictionary
}
public static func swizzleBundleID(handler: () -> Void)
{
let bundleClass: AnyClass = Bundle.self
guard
let originalMethod = class_getInstanceMethod(bundleClass, #selector(getter: Bundle.infoDictionary)),
let swizzledMethod = class_getInstanceMethod(bundleClass, #selector(getter: Bundle.swizzled_infoDictionary))
else {
print("Failed to swizzle Bundle.infoDictionary.")
return
}
method_exchangeImplementations(originalMethod, swizzledMethod)
handler()
method_exchangeImplementations(swizzledMethod, originalMethod)
}
}

View File

@ -1,22 +0,0 @@
//
// CharacterSet+Filename.swift
// Delta
//
// Created by Riley Testut on 4/28/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
extension CharacterSet
{
// Different than .urlPathAllowed
// Copied from https://stackoverflow.com/a/39443252
static var urlFilenameAllowed: CharacterSet {
var illegalCharacters = CharacterSet(charactersIn: ":/")
illegalCharacters.formUnion(.newlines)
illegalCharacters.formUnion(.illegalCharacters)
illegalCharacters.formUnion(.controlCharacters)
return illegalCharacters.inverted
}
}

View File

@ -6,24 +6,10 @@
// Copyright © 2016 Riley Testut. All rights reserved. // Copyright © 2016 Riley Testut. All rights reserved.
// //
import UIKit
import DeltaCore import DeltaCore
extension ControllerSkin extension ControllerSkin
{ {
convenience init?(system: System, context: NSManagedObjectContext)
{
guard let deltaControllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: system.gameType) else { return nil }
self.init(context: context)
self.isStandard = true
self.filename = deltaControllerSkin.fileURL.lastPathComponent
self.configure(with: deltaControllerSkin)
}
func configure(with skin: DeltaCore.ControllerSkin) func configure(with skin: DeltaCore.ControllerSkin)
{ {
// Manually copy values to be stored in database. // Manually copy values to be stored in database.
@ -34,19 +20,20 @@ extension ControllerSkin
var configurations = ControllerSkinConfigurations() var configurations = ControllerSkinConfigurations()
let allTraitCombinations = DeltaCore.ControllerSkin.Device.allCases.flatMap { device in let device: DeltaCore.ControllerSkin.Device = (UIDevice.current.userInterfaceIdiom == .pad) ? .ipad : .iphone
DeltaCore.ControllerSkin.DisplayType.allCases.flatMap { displayType in
DeltaCore.ControllerSkin.Orientation.allCases.map { orientation in
DeltaCore.ControllerSkin.Traits(device: device, displayType: displayType, orientation: orientation)
}
}
}
for traits in allTraitCombinations let traitCollections: [(displayType: DeltaCore.ControllerSkin.DisplayType, orientation: DeltaCore.ControllerSkin.Orientation)] =
[(.standard, .portrait), (.standard, .landscape), (.edgeToEdge, .portrait), (.edgeToEdge, .landscape), (.splitView, .portrait), (.splitView, .landscape)]
for collection in traitCollections
{ {
guard let configuration = ControllerSkinConfigurations(traits: traits), skin.supports(traits) else { continue } let traits = DeltaCore.ControllerSkin.Traits(device: device, displayType: collection.displayType, orientation: collection.orientation)
if skin.supports(traits)
{
let configuration = ControllerSkinConfigurations(traits: traits)
configurations.formUnion(configuration) configurations.formUnion(configuration)
} }
}
self.supportedConfigurations = configurations self.supportedConfigurations = configurations
} }

View File

@ -0,0 +1,24 @@
//
// DeltaCoreProtocol+Delta.swift
// Delta
//
// Created by Riley Testut on 4/30/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import DeltaCore
extension DeltaCoreProtocol
{
var supportedRates: ClosedRange<Double> {
guard let system = System(gameType: self.gameType) else { return 1...1 }
switch system
{
case .nes: return 1...4
case .snes: return 1...4
case .gbc: return 1...4
case .gba: return 1...3
}
}
}

View File

@ -1,25 +0,0 @@
//
// 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

@ -12,8 +12,4 @@ extension HarmonyMetadataKey
{ {
static let gameID = HarmonyMetadataKey("gameID") static let gameID = HarmonyMetadataKey("gameID")
static let gameName = HarmonyMetadataKey("gameName") static let gameName = HarmonyMetadataKey("gameName")
static let verifiedGameID = HarmonyMetadataKey("verifiedGameID")
// Backwards compatibility
static let coreID = HarmonyMetadataKey("coreID")
} }

View File

@ -124,8 +124,6 @@ extension Input
case .leftTrigger: return NSLocalizedString("L2", comment: "") case .leftTrigger: return NSLocalizedString("L2", comment: "")
case .rightShoulder: return NSLocalizedString("R1", comment: "") case .rightShoulder: return NSLocalizedString("R1", comment: "")
case .rightTrigger: return NSLocalizedString("R2", comment: "") case .rightTrigger: return NSLocalizedString("R2", comment: "")
case .start: return NSLocalizedString("Start", comment: "")
case .select: return NSLocalizedString("Select", comment: "")
} }
case .controller(.keyboard): case .controller(.keyboard):

View File

@ -23,29 +23,4 @@ extension NSManagedObjectContext
print("Error saving NSManagedObjectContext: ", error, error.userInfo) print("Error saving NSManagedObjectContext: ", error, error.userInfo)
} }
} }
// MARK: - Perform -
func performAndWait<T>(_ block: @escaping () -> T) -> T
{
var result: T! = nil
self.performAndWait {
result = block()
}
return result
}
func performAndWait<T>(_ block: @escaping () throws -> T) throws -> T
{
var result: Result<T, Error>! = nil
self.performAndWait {
result = Result { try block() }
}
let value = try result.get()
return value
}
} }

View File

@ -1,38 +0,0 @@
//
// OSLog+Delta.swift
// Delta
//
// Created by Riley Testut on 8/10/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import OSLog
extension OSLog.Category
{
static let database = "Database"
}
extension Logger
{
static let deltaSubsystem = "com.rileytestut.Delta"
static let database = Logger(subsystem: deltaSubsystem, category: OSLog.Category.database)
}
@available(iOS 15, *)
extension OSLogEntryLog.Level
{
var localizedName: String {
switch self
{
case .undefined: return NSLocalizedString("Undefined", comment: "")
case .debug: return NSLocalizedString("Debug", comment: "")
case .info: return NSLocalizedString("Info", comment: "")
case .notice: return NSLocalizedString("Notice", comment: "")
case .error: return NSLocalizedString("Error", comment: "")
case .fault: return NSLocalizedString("Fault", comment: "")
@unknown default: return NSLocalizedString("Unknown", comment: "")
}
}
}

View File

@ -1,46 +0,0 @@
//
// 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, .limited:
code()
case .denied, .restricted, .notDetermined: break
@unknown default: break
}
})
}
static func saveImageData(_ data: Data)
{
// Save the image to the Photos app
PHPhotoLibrary.shared().performChanges({
PHAssetCreationRequest.forAsset().addResource(with: .photo, data: data, options: nil)
}, 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

@ -1,41 +0,0 @@
//
// ProcessInfo+JIT.swift
// Delta
//
// Created by Riley Testut on 9/14/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
private let CS_OPS_STATUS: UInt32 = 0 /* OK */
private let CS_DEBUGGED: UInt32 = 0x10000000 /* Process is or has been attached to debugger. */
@_silgen_name("csops")
func csops(_ pid: pid_t, _ ops: UInt32, _ useraddr: UnsafeMutableRawPointer?, _ usersize: Int) -> Int
extension ProcessInfo
{
static var isJITDisabled = false
var isDebugging: Bool {
var flags: UInt32 = 0
let result = csops(getpid(), CS_OPS_STATUS, &flags, MemoryLayout<UInt32>.size)
let isDebugging = result == 0 && (flags & CS_DEBUGGED == CS_DEBUGGED)
return isDebugging
}
var isJITAvailable: Bool {
guard UIDevice.current.supportsJIT && !ProcessInfo.isJITDisabled else { return false }
let ios14_4 = OperatingSystemVersion(majorVersion: 14, minorVersion: 4, patchVersion: 0)
if #available(iOS 14.2, *), !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14_4)
{
// JIT is always available on supported devices running iOS 14.2 - 14.3.
return true
}
return self.isDebugging
}
}

View File

@ -1,104 +0,0 @@
//
// ServerManager+Delta.swift
// Delta
//
// Created by Riley Testut on 9/15/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import AltKit
extension ServerManager
{
static let didEnableJITNotification = Notification.Name("didEnableJITNotification")
}
extension ServerManager
{
func prepare()
{
NotificationCenter.default.addObserver(self, selector: #selector(ServerManager.didChangeJITMode(_:)), name: Settings.didChangeNotification, object: nil)
#if DEBUG
if ProcessInfo.processInfo.isDebugging
{
// Debugger is attached at app launch, so we assume
// we're connected to Xcode for debugging purposes.
// In that case, we manually treat JIT as unavailable
// until AltServer is discovered to simulate real-world use.
ProcessInfo.isJITDisabled = true
}
#endif
self.start()
}
}
private extension ServerManager
{
func start()
{
guard Settings.isAltJITEnabled && !ProcessInfo.processInfo.isJITAvailable else { return }
self.startDiscovering()
self.autoconnect()
}
func autoconnect()
{
self.autoconnect { result in
switch result
{
case .failure(let error):
print("Could not auto-connect to server.", error)
self.autoconnect()
case .success(let connection):
func finish(result: Result<Void, Error>)
{
switch result
{
case .failure(ALTServerError.unknownRequest), .failure(ALTServerError.deviceNotFound):
// Try connecting to a different server.
self.autoconnect()
case .failure(let error):
print("Could not enable JIT compilation.", error)
case .success:
print("Successfully enabled JIT compilation!")
NotificationCenter.default.post(name: ServerManager.didEnableJITNotification, object: nil)
self.stopDiscovering()
}
connection.disconnect()
}
if ProcessInfo.isJITDisabled
{
ProcessInfo.isJITDisabled = false
finish(result: .success(()))
}
else
{
connection.enableUnsignedCodeExecution(completion: finish)
}
}
}
}
@objc func didChangeJITMode(_ notification: Notification)
{
guard let name = notification.userInfo?[Settings.NotificationUserInfoKey.name] as? Settings.Name, name == Settings.Name.isAltJITEnabled else { return }
if Settings.isAltJITEnabled
{
self.start()
}
else
{
self.stopDiscovering()
}
}
}

View File

@ -20,6 +20,16 @@ extension UIAlertController
class func alertController(for importType: ImportType, with errors: Set<DatabaseManager.ImportError>) -> UIAlertController class func alertController(for importType: ImportType, with errors: Set<DatabaseManager.ImportError>) -> UIAlertController
{ {
let title: String
switch importType
{
case .games: title = NSLocalizedString("Error Importing Games", comment: "")
case .controllerSkins: title = NSLocalizedString("Error Importing Controller Skins", comment: "")
}
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
var urls = Set<URL>() var urls = Set<URL>()
for error in errors for error in errors
@ -34,39 +44,24 @@ extension UIAlertController
} }
} }
let title: String let filenames = urls.map{ $0.lastPathComponent }.sorted()
let message: String
if let fileURL = urls.first, let error = errors.first, errors.count == 1 if filenames.count > 0
{ {
title = String(format: NSLocalizedString("Could not import “%@”.", comment: ""), fileURL.lastPathComponent) var message: String
message = error.localizedDescription
}
else
{
switch importType
{
case .games: title = NSLocalizedString("Error Importing Games", comment: "")
case .controllerSkins: title = NSLocalizedString("Error Importing Controller Skins", comment: "")
}
if urls.count > 0
{
var tempMessage: String
switch importType switch importType
{ {
case .games: tempMessage = NSLocalizedString("The following game files could not be imported:", comment: "") + "\n" case .games: message = NSLocalizedString("The following game files could not be imported:", comment: "") + "\n"
case .controllerSkins: tempMessage = NSLocalizedString("The following controller skin files could not be imported:", comment: "") + "\n" case .controllerSkins: message = NSLocalizedString("The following controller skin files could not be imported:", comment: "") + "\n"
} }
let filenames = urls.map { $0.lastPathComponent }.sorted()
for filename in filenames for filename in filenames
{ {
tempMessage += "\n" + filename message += "\n" + filename
} }
message = tempMessage alertController.message = message
} }
else else
{ {
@ -74,14 +69,13 @@ extension UIAlertController
switch importType switch importType
{ {
case .games: message = NSLocalizedString("Delta was unable to import games. Please try again later.", comment: "") case .games: alertController.message = NSLocalizedString("Delta was unable to import games. Please try again later.", comment: "")
case .controllerSkins: message = NSLocalizedString("Delta was unable to import controller skins. Please try again later.", comment: "") case .controllerSkins: alertController.message = NSLocalizedString("Delta was unable to import controller skins. Please try again later.", comment: "")
}
} }
} }
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .cancel, handler: nil)) alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .cancel, handler: nil))
return alertController return alertController
} }
} }

View File

@ -10,6 +10,6 @@ import UIKit
extension UIColor extension UIColor
{ {
static let deltaPurple = UIColor.purple static let deltaPurple = UIColor(named: "Purple")!
static let deltaDarkGray = #colorLiteral(red: 0.07139974087, green: 0.08217515796, blue: 0.1083263531, alpha: 1) static let deltaDarkGray = UIColor(named: "DarkGray")!
} }

View File

@ -1,35 +0,0 @@
//
// UIDevice+Processor.swift
// Delta
//
// Created by Riley Testut on 9/21/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import ARKit
import Metal
extension UIDevice
{
private static var mtlDevice: MTLDevice? = MTLCreateSystemDefaultDevice()
var hasA9ProcessorOrBetter: Bool {
// ARKit is only supported by devices with an A9 processor or better, according to the documentation.
// https://developer.apple.com/documentation/arkit/arconfiguration/2923553-issupported
return ARConfiguration.isSupported
}
var hasA11ProcessorOrBetter: Bool {
guard let mtlDevice = UIDevice.mtlDevice else { return false }
return mtlDevice.supportsFeatureSet(.iOS_GPUFamily4_v1) // iOS GPU Family 4 = A11 GPU
}
var supportsJIT: Bool {
guard #available(iOS 14.0, *) else { return false }
// JIT is supported on devices with an A12 processor or better running iOS 14.0 or later.
// ARKit 3 is only supported by devices with an A12 processor or better, according to the documentation.
return ARBodyTrackingConfiguration.isSupported
}
}

View File

@ -1,24 +0,0 @@
//
// UIImage+SymbolFallback.swift
// Delta
//
// Created by Riley Testut on 2/5/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
extension UIImage
{
convenience init?(symbolNameIfAvailable name: String)
{
if #available(iOS 13, *)
{
self.init(systemName: name)
}
else
{
return nil
}
}
}

View File

@ -18,7 +18,7 @@ internal extension UILabel
context.minimumScaleFactor = self.minimumScaleFactor context.minimumScaleFactor = self.minimumScaleFactor
// Using self.attributedString returns incorrect calculations, so we create our own attributed string // Using self.attributedString returns incorrect calculations, so we create our own attributed string
let attributedString = NSAttributedString(string: text, attributes: [.font: self.font!]) let attributedString = NSAttributedString(string: text, attributes: [.font: self.font])
attributedString.boundingRect(with: self.bounds.size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: context) attributedString.boundingRect(with: self.bounds.size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: context)
let scaleFactor = context.actualScaleFactor let scaleFactor = context.actualScaleFactor

View File

@ -1,14 +0,0 @@
//
// UserDefaults+Delta.swift
// Delta
//
// Created by Riley Testut on 8/10/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import Foundation
extension UserDefaults
{
@NSManaged var shouldRepairDatabase: Bool
}

View File

@ -8,10 +8,8 @@
import UIKit import UIKit
import MobileCoreServices import MobileCoreServices
import AVFoundation
import DeltaCore import DeltaCore
import MelonDSDeltaCore
import Roxas import Roxas
import Harmony import Harmony
@ -24,7 +22,6 @@ extension GameCollectionViewController
{ {
case alreadyRunning case alreadyRunning
case downloadingGameSave case downloadingGameSave
case biosNotFound
} }
} }
@ -43,8 +40,6 @@ class GameCollectionViewController: UICollectionViewController
// Calling reloadData sometimes will not update the cells correctly if an insertion/deletion animation is in progress // Calling reloadData sometimes will not update the cells correctly if an insertion/deletion animation is in progress
// As a workaround, we manually iterate over and configure each cell ourselves // As a workaround, we manually iterate over and configure each cell ourselves
// / reloadData
//
for cell in self.collectionView?.visibleCells ?? [] for cell in self.collectionView?.visibleCells ?? []
{ {
if let indexPath = self.collectionView?.indexPath(for: cell) if let indexPath = self.collectionView?.indexPath(for: cell)
@ -64,16 +59,11 @@ class GameCollectionViewController: UICollectionViewController
private let prototypeCell = GridCollectionViewCell() private let prototypeCell = GridCollectionViewCell()
private var _performingPreviewTransition = false private var _performing3DTouchTransition = false
private weak var _previewTransitionViewController: PreviewGameViewController? private weak var _destination3DTouchTransitionViewController: UIViewController?
private weak var _previewTransitionDestinationViewController: UIViewController?
private weak var _popoverSourceView: UIView?
private var _renameAction: UIAlertAction? private var _renameAction: UIAlertAction?
private var _changingArtworkGame: Game? private var _changingArtworkGame: Game?
private var _importingSaveFileGame: Game?
private var _exportedSaveFileURL: URL?
required init?(coder aDecoder: NSCoder) required init?(coder aDecoder: NSCoder)
{ {
@ -97,33 +87,31 @@ extension GameCollectionViewController
self.collectionView?.prefetchDataSource = self.dataSource self.collectionView?.prefetchDataSource = self.dataSource
self.collectionView?.delegate = self self.collectionView?.delegate = self
if #available(iOS 13, *) {} let layout = self.collectionViewLayout as! GridCollectionViewLayout
else layout.itemWidth = 90
{ layout.minimumInteritemSpacing = 12
self.registerForPreviewing(with: self, sourceView: self.collectionView!) self.registerForPreviewing(with: self, sourceView: self.collectionView!)
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:))) let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:)))
self.collectionView?.addGestureRecognizer(longPressGestureRecognizer) self.collectionView?.addGestureRecognizer(longPressGestureRecognizer)
} }
self.update()
}
override func viewWillDisappear(_ animated: Bool) override func viewWillDisappear(_ animated: Bool)
{ {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
if _performingPreviewTransition if _performing3DTouchTransition
{ {
_performingPreviewTransition = false _performing3DTouchTransition = false
// Unlike our custom transitions, 3D Touch transition doesn't manually call appearance methods for us // Unlike our custom transitions, 3D Touch transition doesn't manually call appearance methods for us
// To compensate, we call them ourselves // To compensate, we call them ourselves
_previewTransitionDestinationViewController?.beginAppearanceTransition(true, animated: true) _destination3DTouchTransitionViewController?.beginAppearanceTransition(true, animated: true)
self.transitionCoordinator?.animate(alongsideTransition: nil, completion: { (context) in self.transitionCoordinator?.animate(alongsideTransition: nil, completion: { (context) in
self._previewTransitionDestinationViewController?.endAppearanceTransition() self._destination3DTouchTransitionViewController?.endAppearanceTransition()
self._previewTransitionDestinationViewController = nil self._destination3DTouchTransitionViewController = nil
}) })
} }
} }
@ -133,13 +121,6 @@ extension GameCollectionViewController
super.didReceiveMemoryWarning() super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated. // Dispose of any resources that can be recreated.
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
{
super.traitCollectionDidChange(previousTraitCollection)
self.update()
}
} }
//MARK: - Segues - //MARK: - Segues -
@ -161,12 +142,6 @@ extension GameCollectionViewController
saveStatesViewController.mode = .loading saveStatesViewController.mode = .loading
saveStatesViewController.theme = self.theme saveStatesViewController.theme = self.theme
case "preferredControllerSkins":
let game = sender as! Game
let preferredControllerSkinsViewController = (segue.destination as! UINavigationController).topViewController as! PreferredControllerSkinsViewController
preferredControllerSkinsViewController.game = game
case "unwindFromGames": case "unwindFromGames":
let destinationViewController = segue.destination as! GameViewController let destinationViewController = segue.destination as! GameViewController
let cell = sender as! UICollectionViewCell let cell = sender as! UICollectionViewCell
@ -176,28 +151,9 @@ extension GameCollectionViewController
destinationViewController.game = game destinationViewController.game = game
if let emulatorBridge = destinationViewController.emulatorCore?.deltaCore.emulatorBridge as? MelonDSEmulatorBridge
{
//TODO: Update this to work with multiple processes by retrieving emulatorBridge directly from emulatorCore.
//TODO emulatorCore emulatorBridge 使
if game.identifier == Game.melonDSDSiBIOSIdentifier
{
emulatorBridge.systemType = .dsi
}
else
{
emulatorBridge.systemType = .ds
}
emulatorBridge.isJITEnabled = ProcessInfo.processInfo.isJITAvailable
}
if let saveState = self.activeSaveState if let saveState = self.activeSaveState
{ {
// Must be synchronous or else there will be a flash of black // Must be synchronous or else there will be a flash of black
//
destinationViewController.emulatorCore?.start() destinationViewController.emulatorCore?.start()
destinationViewController.emulatorCore?.pause() destinationViewController.emulatorCore?.pause()
@ -219,16 +175,20 @@ extension GameCollectionViewController
self.activeSaveState = nil self.activeSaveState = nil
if _performingPreviewTransition if _performing3DTouchTransition
{ {
_previewTransitionDestinationViewController = destinationViewController _destination3DTouchTransitionViewController = destinationViewController
} }
default: break default: break
} }
} }
@IBAction private func unwindToGameCollectionViewController(_ segue: UIStoryboardSegue) @IBAction private func unwindFromSaveStatesViewController(with segue: UIStoryboardSegue)
{
}
@IBAction private func unwindFromGamesDatabaseBrowser(with segue: UIStoryboardSegue)
{ {
} }
} }
@ -236,27 +196,6 @@ extension GameCollectionViewController
//MARK: - Private Methods - //MARK: - Private Methods -
private extension GameCollectionViewController private extension GameCollectionViewController
{ {
func update()
{
let layout = self.collectionViewLayout as! GridCollectionViewLayout
switch self.traitCollection.horizontalSizeClass
{
case .regular:
layout.itemWidth = 150
layout.minimumInteritemSpacing = 25 // 30 == only 3 games per line for iPad mini 6 in portrait
// 30 == iPad mini 6 3
case .unspecified, .compact:
layout.itemWidth = 90
layout.minimumInteritemSpacing = 12
@unknown default: break
}
self.collectionView.reloadData()
}
//MARK: - Data Source //MARK: - Data Source
func prepareDataSource() func prepareDataSource()
{ {
@ -315,24 +254,11 @@ private extension GameCollectionViewController
cell.isImageViewVibrancyEnabled = true cell.isImageViewVibrancyEnabled = true
} }
cell.imageView.image = #imageLiteral(resourceName: "zw") cell.imageView.image = #imageLiteral(resourceName: "BoxArt")
if self.traitCollection.horizontalSizeClass == .regular
{
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)!
cell.textLabel.font = UIFont(descriptor: fontDescriptor, size: 0)
}
else
{
cell.textLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
}
let layout = self.collectionViewLayout as! GridCollectionViewLayout
cell.maximumImageSize = CGSize(width: layout.itemWidth, height: layout.itemWidth)
cell.maximumImageSize = CGSize(width: 90, height: 90)
cell.textLabel.text = game.name cell.textLabel.text = game.name
cell.textLabel.textColor = UIColor.gray cell.textLabel.textColor = UIColor.gray
cell.tintColor = cell.textLabel.textColor
} }
//MARK: - Emulation //MARK: - Emulation
@ -375,13 +301,11 @@ private extension GameCollectionViewController
} }
// Disable videoManager to prevent flash of black // Disable videoManager to prevent flash of black
// videoManager
self.activeEmulatorCore?.videoManager.isEnabled = false self.activeEmulatorCore?.videoManager.isEnabled = false
launchGame(ignoringErrors: [LaunchError.alreadyRunning]) launchGame(ignoringErrors: [LaunchError.alreadyRunning])
// The game hasn't changed, so the activeEmulatorCore is the same as before, so we need to enable videoManager it again // The game hasn't changed, so the activeEmulatorCore is the same as before, so we need to enable videoManager it again
// activeEmulatorCorevideoManager
self.activeEmulatorCore?.videoManager.isEnabled = true self.activeEmulatorCore?.videoManager.isEnabled = true
})) }))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Restart", comment: ""), style: .destructive, handler: { (action) in alertController.addAction(UIAlertAction(title: NSLocalizedString("Restart", comment: ""), style: .destructive, handler: { (action) in
@ -395,16 +319,6 @@ private extension GameCollectionViewController
alertController.addAction(.ok) alertController.addAction(.ok)
self.present(alertController, animated: true, completion: nil) self.present(alertController, animated: true, completion: nil)
} }
catch LaunchError.biosNotFound
{
let alertController = UIAlertController(title: NSLocalizedString("Missing Required DS Files", comment: ""), message: NSLocalizedString("Delta requires certain files to play Nintendo DS games. Please import them to launch this game.", comment: ""), preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Import Files", comment: ""), style: .default) { _ in
self.performSegue(withIdentifier: "showDSSettings", sender: nil)
})
alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
catch catch
{ {
let alertController = UIAlertController(title: NSLocalizedString("Unable to Launch Game", comment: ""), error: error) let alertController = UIAlertController(title: NSLocalizedString("Unable to Launch Game", comment: ""), error: error)
@ -421,7 +335,7 @@ private extension GameCollectionViewController
launchGame(ignoringErrors: []) launchGame(ignoringErrors: [])
} }
} }
//
func validateLaunchingGame(_ game: Game, ignoringErrors ignoredErrors: [Error]) throws func validateLaunchingGame(_ game: Game, ignoringErrors ignoredErrors: [Error]) throws
{ {
let ignoredErrors = ignoredErrors.map { $0 as NSError } let ignoredErrors = ignoredErrors.map { $0 as NSError }
@ -431,13 +345,13 @@ private extension GameCollectionViewController
guard game.fileURL != self.activeEmulatorCore?.game.fileURL else { throw LaunchError.alreadyRunning } guard game.fileURL != self.activeEmulatorCore?.game.fileURL else { throw LaunchError.alreadyRunning }
} }
if let coordinator = SyncManager.shared.coordinator, coordinator.isSyncing if SyncManager.shared.syncCoordinator.isSyncing
{ {
if let gameSave = game.gameSave if let gameSave = game.gameSave
{ {
do do
{ {
if let record = try coordinator.recordController.fetchRecords(for: [gameSave]).first if let record = try SyncManager.shared.recordController.fetchRecords(for: [gameSave]).first
{ {
if record.isSyncingEnabled && !record.isConflicted && (record.localStatus == nil || record.remoteStatus == .updated) if record.isSyncingEnabled && !record.isConflicted && (record.localStatus == nil || record.remoteStatus == .updated)
{ {
@ -455,27 +369,6 @@ private extension GameCollectionViewController
} }
} }
} }
if game.type == .ds && Settings.preferredCore(for: .ds) == MelonDS.core
{
if game.identifier == Game.melonDSDSiBIOSIdentifier
{
guard
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS7URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS9URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiFirmwareURL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiNANDURL.path)
else { throw LaunchError.biosNotFound }
}
else
{
guard
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios7URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios9URL.path) &&
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.firmwareURL.path)
else { throw LaunchError.biosNotFound }
}
}
} }
} }
@ -486,54 +379,36 @@ private extension GameCollectionViewController
{ {
let cancelAction = Action(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, action: nil) let cancelAction = Action(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, action: nil)
let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "pencil.and.ellipsis.rectangle"), action: { [unowned self] action in let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, action: { [unowned self] action in
self.rename(game) self.rename(game)
}) })
let changeArtworkAction = Action(title: NSLocalizedString("Change Artwork", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "photo")) { [unowned self] action in let changeArtworkAction = Action(title: NSLocalizedString("Change Artwork", comment: ""), style: .default) { [unowned self] action in
self.changeArtwork(for: game) self.changeArtwork(for: game)
} }
let changeControllerSkinAction = Action(title: NSLocalizedString("Change Controller Skin", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "gamecontroller")) { [unowned self] _ in let shareAction = Action(title: NSLocalizedString("Share", comment: ""), style: .default, action: { [unowned self] action in
self.changePreferredControllerSkin(for: game)
}
let shareAction = Action(title: NSLocalizedString("Share", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "square.and.arrow.up"), action: { [unowned self] action in
self.share(game) self.share(game)
}) })
let saveStatesAction = Action(title: NSLocalizedString("Save States", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "doc.on.doc"), action: { [unowned self] action in let saveStatesAction = Action(title: NSLocalizedString("Save States", comment: ""), style: .default, action: { [unowned self] action in
self.viewSaveStates(for: game) self.viewSaveStates(for: game)
}) })
let importSaveFile = Action(title: NSLocalizedString("Import Save File", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "tray.and.arrow.down")) { [unowned self] _ in let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, action: { [unowned self] action in
self.importSaveFile(for: game)
}
let exportSaveFile = Action(title: NSLocalizedString("Export Save File", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "tray.and.arrow.up")) { [unowned self] _ in
self.exportSaveFile(for: game)
}
let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, image: UIImage(symbolNameIfAvailable: "trash"), action: { [unowned self] action in
self.delete(game) self.delete(game)
}) })
switch game.type switch game.type
{ {
case GameType.unknown: case GameType.unknown: return [cancelAction, renameAction, changeArtworkAction, shareAction, deleteAction]
return [cancelAction, renameAction, changeArtworkAction, shareAction, deleteAction] default: return [cancelAction, renameAction, changeArtworkAction, shareAction, saveStatesAction, deleteAction]
case .ds where game.identifier == Game.melonDSBIOSIdentifier || game.identifier == Game.melonDSDSiBIOSIdentifier:
return [cancelAction, renameAction, changeArtworkAction, changeControllerSkinAction, saveStatesAction]
default:
return [cancelAction, renameAction, changeArtworkAction, changeControllerSkinAction, shareAction, saveStatesAction, importSaveFile, exportSaveFile, deleteAction]
} }
} }
func delete(_ game: Game) func delete(_ game: Game)
{ {
let confirmationAlertController = UIAlertController(title: NSLocalizedString("Are you sure you want to delete this game?", comment: ""), let confirmationAlertController = UIAlertController(title: NSLocalizedString("Are you sure you want to delete this game? All associated data, such as saves, save states, and cheat codes, will also be deleted.", comment: ""), message: nil, preferredStyle: .actionSheet)
message: NSLocalizedString("All associated data, such as saves, save states, and cheat codes, will also be deleted.", comment: ""),
preferredStyle: .alert)
confirmationAlertController.addAction(UIAlertAction(title: NSLocalizedString("Delete Game", comment: ""), style: .destructive, handler: { action in confirmationAlertController.addAction(UIAlertAction(title: NSLocalizedString("Delete Game", comment: ""), style: .destructive, handler: { action in
DatabaseManager.shared.performBackgroundTask { (context) in DatabaseManager.shared.performBackgroundTask { (context) in
@ -603,148 +478,29 @@ private extension GameCollectionViewController
let importController = ImportController(documentTypes: [kUTTypeImage as String]) let importController = ImportController(documentTypes: [kUTTypeImage as String])
importController.delegate = self importController.delegate = self
importController.importOptions = [clipboardImportOption, photoLibraryImportOption, gamesDatabaseImportOption] importController.importOptions = [clipboardImportOption, photoLibraryImportOption, gamesDatabaseImportOption]
importController.sourceView = self._popoverSourceView
self.present(importController, animated: true, completion: nil) self.present(importController, animated: true, completion: nil)
} }
func changeArtwork(for game: Game, toImageAt url: URL?, errors: [Error])
{
defer {
if let temporaryImageURL = url
{
try? FileManager.default.removeItem(at: temporaryImageURL)
}
}
var errors = errors
var imageURL: URL?
if let url = url
{
if url.isFileURL
{
do
{
let imageData = try Data(contentsOf: url)
if
let image = UIImage(data: imageData),
let resizedImage = image.resizing(toFit: CGSize(width: 300, height: 300)),
let rotatedImage = resizedImage.rotatedToIntrinsicOrientation(), // in case image was imported directly from Files
let resizedData = rotatedImage.pngData()
{
let destinationURL = DatabaseManager.artworkURL(for: game)
try resizedData.write(to: destinationURL, options: .atomic)
imageURL = destinationURL
}
}
catch
{
errors.append(error)
}
}
else
{
imageURL = url
}
}
for error in errors
{
print(error)
}
if let imageURL = imageURL
{
self.dataSource.prefetchItemCache.removeObject(forKey: game)
if let cacheManager = SDWebImageManager.shared()
{
let cacheKey = cacheManager.cacheKey(for: imageURL)
cacheManager.imageCache.removeImage(forKey: cacheKey)
}
DatabaseManager.shared.performBackgroundTask { (context) in
let temporaryGame = context.object(with: game.objectID) as! Game
temporaryGame.artworkURL = imageURL
context.saveWithErrorLogging()
// Local image URLs may not change despite being a different image, so manually mark record as updated.
SyncManager.shared.recordController?.updateRecord(for: temporaryGame)
DispatchQueue.main.async {
if let indexPath = self.dataSource.fetchedResultsController.indexPath(forObject: game)
{
// Manually reload item because collection view may not be in window hierarchy,
// which means it won't automatically update when we save the context.
//
//
self.collectionView.reloadItems(at: [indexPath])
}
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
}
else
{
DispatchQueue.main.async {
func presentAlertController()
{
let alertController = UIAlertController(title: NSLocalizedString("Unable to Change Artwork", comment: ""), message: NSLocalizedString("The image might be corrupted or in an unsupported format.", comment: ""), preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
if let presentedViewController = self.presentedViewController
{
presentedViewController.dismiss(animated: true) {
presentAlertController()
}
}
else
{
presentAlertController()
}
}
}
}
func share(_ game: Game) func share(_ game: Game)
{ {
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
let symbolicURL = temporaryDirectory.appendingPathComponent(game.name + "." + game.fileURL.pathExtension)
let sanitizedName = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined()
let temporaryURL = temporaryDirectory.appendingPathComponent(sanitizedName + "." + game.fileURL.pathExtension, isDirectory: false)
do do
{ {
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
// Make a temporary copy so we can control the filename used when sharing. // Create a symbolic link so we can control the file name used when sharing.
// Otherwise, if we just passed in game.fileURL to UIActivityViewController, the file name would be the game's SHA1 hash. // Otherwise, if we just passed in game.fileURL to UIActivityViewController, the file name would be the game's SHA1 hash.
try FileManager.default.copyItem(at: game.fileURL, to: temporaryURL, shouldReplace: true) try FileManager.default.createSymbolicLink(at: symbolicURL, withDestinationURL: game.fileURL)
} }
catch catch
{ {
let alertController = UIAlertController(title: NSLocalizedString("Could Not Share Game", comment: ""), error: error) print(error)
self.present(alertController, animated: true, completion: nil)
return
} }
let copyDeepLinkActivity = CopyDeepLinkActivity() let activityViewController = UIActivityViewController(activityItems: [symbolicURL], applicationActivities: nil)
let activityViewController = UIActivityViewController(activityItems: [temporaryURL, game], applicationActivities: [copyDeepLinkActivity])
activityViewController.popoverPresentationController?.sourceView = self._popoverSourceView?.superview
activityViewController.popoverPresentationController?.sourceRect = self._popoverSourceView?.frame ?? .zero
activityViewController.completionWithItemsHandler = { (activityType, finished, returnedItems, error) in activityViewController.completionWithItemsHandler = { (activityType, finished, returnedItems, error) in
// Make sure the user either shared the game or cancelled before deleting temporaryDirectory.
guard finished || activityType == nil else { return }
do do
{ {
try FileManager.default.removeItem(at: temporaryDirectory) try FileManager.default.removeItem(at: temporaryDirectory)
@ -754,85 +510,9 @@ private extension GameCollectionViewController
print(error) print(error)
} }
} }
self.present(activityViewController, animated: true, completion: nil) self.present(activityViewController, animated: true, completion: nil)
} }
func importSaveFile(for game: Game)
{
self._importingSaveFileGame = game
let importController = ImportController(documentTypes: [kUTTypeItem as String])
importController.delegate = self
self.present(importController, animated: true, completion: nil)
}
func importSaveFile(for game: Game, from fileURL: URL?, error: Error?)
{
// Dispatch to main queue so we can access game.gameSaveURL on its context's thread (main thread).
DispatchQueue.main.async {
do
{
if let error = error
{
throw error
}
if let fileURL = fileURL
{
try FileManager.default.copyItem(at: fileURL, to: game.gameSaveURL, shouldReplace: true)
if let gameSave = game.gameSave
{
SyncManager.shared.recordController?.updateRecord(for: gameSave)
}
}
}
catch
{
let alertController = UIAlertController(title: NSLocalizedString("Failed to Import Save File", comment: ""), error: error)
if let presentedViewController = self.presentedViewController
{
presentedViewController.dismiss(animated: true) {
self.present(alertController, animated: true, completion: nil)
}
}
else
{
self.present(alertController, animated: true, completion: nil)
}
}
}
}
func exportSaveFile(for game: Game)
{
do
{
let sanitizedFilename = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined()
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(sanitizedFilename)
try FileManager.default.copyItem(at: game.gameSaveURL, to: temporaryURL, shouldReplace: true)
self._exportedSaveFileURL = temporaryURL
let documentPicker = UIDocumentPickerViewController(urls: [temporaryURL], in: .exportToService)
documentPicker.delegate = self
self.present(documentPicker, animated: true, completion: nil)
}
catch
{
let alertController = UIAlertController(title: NSLocalizedString("Failed to Export Save File", comment: ""), error: error)
self.present(alertController, animated: true, completion: nil)
}
}
func changePreferredControllerSkin(for game: Game)
{
self.performSegue(withIdentifier: "preferredControllerSkins", sender: game)
}
@objc func textFieldTextDidChange(_ textField: UITextField) @objc func textFieldTextDidChange(_ textField: UITextField)
{ {
let text = textField.text ?? "" let text = textField.text ?? ""
@ -869,23 +549,8 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate
previewingContext.sourceRect = layoutAttributes.frame previewingContext.sourceRect = layoutAttributes.frame
let cell = collectionView.cellForItem(at: indexPath)
self._popoverSourceView = cell
let game = self.dataSource.item(at: indexPath) let game = self.dataSource.item(at: indexPath)
let gameViewController = self.makePreviewGameViewController(for: game)
_previewTransitionViewController = gameViewController
return gameViewController
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
{
self.commitPreviewTransition()
}
func makePreviewGameViewController(for game: Game) -> PreviewGameViewController
{
let gameViewController = PreviewGameViewController() let gameViewController = PreviewGameViewController()
gameViewController.game = game gameViewController.game = game
@ -895,55 +560,28 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate
gameViewController.previewImage = UIImage(contentsOfFile: previewSaveState.imageFileURL.path) gameViewController.previewImage = UIImage(contentsOfFile: previewSaveState.imageFileURL.path)
} }
if let emulatorBridge = gameViewController.emulatorCore?.deltaCore.emulatorBridge as? MelonDSEmulatorBridge
{
//TODO: Update this to work with multiple processes by retrieving emulatorBridge directly from emulatorCore.
if game.identifier == Game.melonDSDSiBIOSIdentifier
{
emulatorBridge.systemType = .dsi
}
else
{
emulatorBridge.systemType = .ds
}
emulatorBridge.isJITEnabled = ProcessInfo.processInfo.isJITAvailable
}
let actions = self.actions(for: game).previewActions let actions = self.actions(for: game).previewActions
gameViewController.overridePreviewActionItems = actions gameViewController.overridePreviewActionItems = actions
return gameViewController return gameViewController
} }
func commitPreviewTransition() func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
{ {
guard let gameViewController = _previewTransitionViewController else { return } let gameViewController = viewControllerToCommit as! PreviewGameViewController
let game = gameViewController.game as! Game let game = gameViewController.game as! Game
gameViewController.pauseEmulation()
let indexPath = self.dataSource.fetchedResultsController.indexPath(forObject: game)! let indexPath = self.dataSource.fetchedResultsController.indexPath(forObject: game)!
let fileURL = FileManager.default.uniqueTemporaryURL()
if gameViewController.isLivePreview let fileURL = FileManager.default.uniqueTemporaryURL()
{
self.activeSaveState = gameViewController.emulatorCore?.saveSaveState(to: fileURL) self.activeSaveState = gameViewController.emulatorCore?.saveSaveState(to: fileURL)
}
else
{
self.activeSaveState = gameViewController.previewSaveState
}
gameViewController.emulatorCore?.stop() gameViewController.emulatorCore?.stop()
_performingPreviewTransition = true _performing3DTouchTransition = true
self.launchGame(at: indexPath, clearScreen: true, ignoreAlreadyRunningError: true) self.launchGame(at: indexPath, clearScreen: true, ignoreAlreadyRunningError: true)
if gameViewController.isLivePreview
{
do do
{ {
try FileManager.default.removeItem(at: fileURL) try FileManager.default.removeItem(at: fileURL)
@ -953,7 +591,6 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate
print(error) print(error)
} }
} }
}
} }
//MARK: - SaveStatesViewControllerDelegate - //MARK: - SaveStatesViewControllerDelegate -
@ -981,17 +618,82 @@ extension GameCollectionViewController: ImportControllerDelegate
{ {
func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error]) func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error])
{ {
if let game = self._changingArtworkGame guard let game = self._changingArtworkGame else { return }
var errors = errors
var imageURL: URL?
if let url = urls.first
{ {
self.changeArtwork(for: game, toImageAt: urls.first, errors: errors) if url.isFileURL
{
do
{
let imageData = try Data(contentsOf: url)
if
let image = UIImage(data: imageData),
let resizedImage = image.resizing(toFit: CGSize(width: 300, height: 300)),
let resizedData = resizedImage.jpegData(compressionQuality: 0.85)
{
let destinationURL = DatabaseManager.artworkURL(for: game)
try resizedData.write(to: destinationURL, options: .atomic)
imageURL = destinationURL
} }
else if let game = self._importingSaveFileGame }
catch
{ {
self.importSaveFile(for: game, from: urls.first, error: errors.first) errors.append(error)
}
}
else
{
imageURL = url
}
} }
self._changingArtworkGame = nil for error in errors
self._importingSaveFileGame = nil {
print(error)
}
if let imageURL = imageURL
{
DatabaseManager.shared.performBackgroundTask { (context) in
let temporaryGame = context.object(with: game.objectID) as! Game
temporaryGame.artworkURL = imageURL
context.saveWithErrorLogging()
// Local image URLs may not change despite being a different image, so manually mark record as updated.
SyncManager.shared.recordController.updateRecord(for: temporaryGame)
DispatchQueue.main.async {
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
}
else
{
func presentAlertController()
{
let alertController = UIAlertController(title: NSLocalizedString("Unable to Change Artwork", comment: ""), message: NSLocalizedString("The image might be corrupted or in an unsupported format.", comment: ""), preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
if let presentedViewController = self.presentedViewController
{
presentedViewController.dismiss(animated: true) {
presentAlertController()
}
}
else
{
presentAlertController()
}
}
} }
func importControllerDidCancel(_ importController: ImportController) func importControllerDidCancel(_ importController: ImportController)
@ -1018,7 +720,6 @@ extension GameCollectionViewController: UICollectionViewDelegateFlowLayout
{ {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{ {
let collectionViewLayout = collectionView.collectionViewLayout as! GridCollectionViewLayout let collectionViewLayout = collectionView.collectionViewLayout as! GridCollectionViewLayout
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionViewLayout.itemWidth) let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionViewLayout.itemWidth)
@ -1028,99 +729,6 @@ extension GameCollectionViewController: UICollectionViewDelegateFlowLayout
self.configure(self.prototypeCell, for: indexPath) self.configure(self.prototypeCell, for: indexPath)
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
// return size return size
return CGSize(width: 150, height: 150)
}
}
@available(iOS 13.0, *)
extension GameCollectionViewController
{
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?
{
let game = self.dataSource.item(at: indexPath)
let actions = self.actions(for: game)
let cell = self.collectionView.cellForItem(at: indexPath)
self._popoverSourceView = cell
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { [weak self] in
guard let self = self else { return nil }
do
{
try self.validateLaunchingGame(game, ignoringErrors: [LaunchError.alreadyRunning])
}
catch
{
print("Error trying to preview game:", error)
return nil
}
let previewViewController = self.makePreviewGameViewController(for: game)
previewViewController.isLivePreview = Settings.isPreviewsEnabled
guard previewViewController.isLivePreview || previewViewController.previewSaveState != nil else { return nil }
self._previewTransitionViewController = previewViewController
return previewViewController
}) { suggestedActions in
return UIMenu(title: game.name, children: actions.menuActions)
}
}
override func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating)
{
self.commitPreviewTransition()
}
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
{
guard let indexPath = configuration.identifier as? NSIndexPath else { return nil }
guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? GridCollectionViewCell else { return nil }
let parameters = UIPreviewParameters()
parameters.backgroundColor = .clear
if let image = cell.imageView.image
{
let artworkFrame = AVMakeRect(aspectRatio: image.size, insideRect: cell.imageView.bounds)
let bezierPath = UIBezierPath(rect: artworkFrame)
parameters.visiblePath = bezierPath
}
let preview = UITargetedPreview(view: cell.imageView, parameters: parameters)
return preview
}
override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
{
_previewTransitionViewController = nil
return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration)
}
}
extension GameCollectionViewController: UIDocumentPickerDelegate
{
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL])
{
if let saveFileURL = self._exportedSaveFileURL
{
try? FileManager.default.removeItem(at: saveFileURL)
}
self._exportedSaveFileURL = nil
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController)
{
if let saveFileURL = self._exportedSaveFileURL
{
try? FileManager.default.removeItem(at: saveFileURL)
}
self._exportedSaveFileURL = nil
} }
} }

View File

@ -47,19 +47,6 @@ class GamesViewController: UIViewController
private let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult> private let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>
private var searchController: RSTSearchController? private var searchController: RSTSearchController?
private lazy var importController: ImportController = self.makeImportController()
private var syncingToastView: RSTToastView? {
didSet {
if self.syncingToastView == nil
{
self.syncingProgressObservation = nil
}
}
}
private var syncingProgressObservation: NSKeyValueObservation?
@IBOutlet private var importButton: UIBarButtonItem!
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
fatalError("initWithNibName: not implemented") fatalError("initWithNibName: not implemented")
@ -78,15 +65,6 @@ class GamesViewController: UIViewController
NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.syncingDidStart(_:)), name: SyncCoordinator.didStartSyncingNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.syncingDidStart(_:)), name: SyncCoordinator.didStartSyncingNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.syncingDidFinish(_:)), name: SyncCoordinator.didFinishSyncingNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.syncingDidFinish(_:)), name: SyncCoordinator.didFinishSyncingNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.settingsDidChange(_:)), name: Settings.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.emulationDidQuit(_:)), name: EmulatorCore.emulationDidQuitNotification, object: nil)
}
@IBAction func importfilesBtn(_ sender: UIButton) {
self.present(self.importController, animated: true, completion: nil)
} }
} }
@ -100,16 +78,9 @@ extension GamesViewController
self.placeholderView = RSTPlaceholderView(frame: self.view.bounds) self.placeholderView = RSTPlaceholderView(frame: self.view.bounds)
self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.placeholderView.textLabel.text = NSLocalizedString("", comment: "") self.placeholderView.textLabel.text = NSLocalizedString("No Games", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("You have not added any games", comment: "") self.placeholderView.detailTextLabel.text = NSLocalizedString("You can import games by pressing the + button in the top right.", comment: "")
let placeholderImagV = UIImageView(image: UIImage(named: "nogame"))
placeholderImagV.frame = CGRect(x: (self.view.bounds.width - 87) / 2, y: (self.view.bounds.height - (placeholderImagV.bounds.height + 80)) / 2, width: 87, height: 64)
placeholderImagV.contentMode = .center
self.view.insertSubview(self.placeholderView, at: 0) self.view.insertSubview(self.placeholderView, at: 0)
self.placeholderView.addSubview(placeholderImagV)
self.pageControl = UIPageControl() self.pageControl = UIPageControl()
self.pageControl.translatesAutoresizingMaskIntoConstraints = false self.pageControl.translatesAutoresizingMaskIntoConstraints = false
@ -122,48 +93,8 @@ extension GamesViewController
self.pageControl.centerXAnchor.constraint(equalTo: (self.navigationController?.toolbar.centerXAnchor)!, constant: 0).isActive = true self.pageControl.centerXAnchor.constraint(equalTo: (self.navigationController?.toolbar.centerXAnchor)!, constant: 0).isActive = true
self.pageControl.centerYAnchor.constraint(equalTo: (self.navigationController?.toolbar.centerYAnchor)!, constant: 0).isActive = true self.pageControl.centerYAnchor.constraint(equalTo: (self.navigationController?.toolbar.centerYAnchor)!, constant: 0).isActive = true
// if let navigationController = self.navigationController self.navigationController?.navigationBar.barStyle = .blackTranslucent
// { self.navigationController?.toolbar.barStyle = .blackTranslucent
// if #available(iOS 13.0, *)
// {
// navigationController.overrideUserInterfaceStyle = .dark
//
// let navigationBarAppearance = navigationController.navigationBar.standardAppearance.copy()
// navigationBarAppearance.backgroundEffect = UIBlurEffect(style: .dark)
// navigationController.navigationBar.standardAppearance = navigationBarAppearance
// navigationController.navigationBar.scrollEdgeAppearance = navigationBarAppearance
//
// let toolbarAppearance = navigationController.toolbar.standardAppearance.copy()
// toolbarAppearance.backgroundEffect = UIBlurEffect(style: .dark)
// navigationController.toolbar.standardAppearance = toolbarAppearance
//
// if #available(iOS 15, *)
// {
// navigationController.toolbar.scrollEdgeAppearance = toolbarAppearance
// }
// }
// else
// {
// navigationController.navigationBar.barStyle = .blackTranslucent
// navigationController.toolbar.barStyle = .blackTranslucent
// }
// }
if #available(iOS 14, *)
{
self.importController.presentingViewController = self
let importActions = self.importController.makeActions().menuActions
let importMenu = UIMenu(title: NSLocalizedString("Import From…", comment: ""), image: UIImage(systemName: "square.and.arrow.down"), children: importActions)
self.importButton.menu = importMenu
self.importButton.action = nil
self.importButton.target = nil
}
else
{
self.importController.barButtonItem = self.importButton
}
self.prepareSearchController() self.prepareSearchController()
@ -200,27 +131,16 @@ extension GamesViewController
// In a storyboard-based application, you will often want to do a little preparation before navigation // In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{ {
guard let identifier = segue.identifier else { return } guard let identifier = segue.identifier, identifier == "embedPageViewController" else { return }
switch identifier
{
case "embedPageViewController":
self.pageViewController = segue.destination as? UIPageViewController self.pageViewController = segue.destination as? UIPageViewController
self.pageViewController.dataSource = self self.pageViewController.dataSource = self
self.pageViewController.delegate = self self.pageViewController.delegate = self
self.pageViewController.view.isHidden = true self.pageViewController.view.isHidden = true
case "showSettings":
let destinationViewController = segue.destination
destinationViewController.presentationController?.delegate = self
default: break
}
} }
@IBAction private func unwindFromSettingsViewController(_ segue: UIStoryboardSegue) @IBAction private func unwindFromSettingsViewController(_ segue: UIStoryboardSegue)
{ {
self.sync()
} }
} }
@ -250,10 +170,8 @@ private extension GamesViewController
self.searchController = RSTSearchController(searchResultsController: searchResultsController) self.searchController = RSTSearchController(searchResultsController: searchResultsController)
self.searchController?.searchableKeyPaths = [#keyPath(Game.name)] self.searchController?.searchableKeyPaths = [#keyPath(Game.name)]
self.searchController?.searchHandler = { [weak self, weak searchResultsController] (searchValue, _) in self.searchController?.searchHandler = { [weak searchController, weak searchResultsController] (searchValue, _) in
guard let self = self else { return nil } if searchController?.searchBar.text?.isEmpty == false
if self.searchController?.searchBar.text?.isEmpty == false
{ {
self.pageViewController.view.isHidden = true self.pageViewController.view.isHidden = true
} }
@ -266,7 +184,6 @@ private extension GamesViewController
return nil return nil
} }
self.searchController?.searchBar.barStyle = .black self.searchController?.searchBar.barStyle = .black
self.searchController?.searchBar.placeholder = "Game here"
self.navigationItem.searchController = self.searchController self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false self.navigationItem.hidesSearchBarWhenScrolling = false
@ -327,7 +244,7 @@ private extension GamesViewController
if let viewController = self.pageViewController.viewControllers?.first as? GameCollectionViewController, let gameCollection = viewController.gameCollection if let viewController = self.pageViewController.viewControllers?.first as? GameCollectionViewController, let gameCollection = viewController.gameCollection
{ {
if let index = self.fetchedResultsController.fetchedObjects?.firstIndex(where: { $0 as! GameCollection == gameCollection }) if let index = self.fetchedResultsController.fetchedObjects?.index(where: { $0 as! GameCollection == gameCollection })
{ {
self.pageControl.currentPage = index self.pageControl.currentPage = index
} }
@ -356,7 +273,7 @@ private extension GamesViewController
if let gameCollection = Settings.previousGameCollection if let gameCollection = Settings.previousGameCollection
{ {
if let gameCollectionIndex = self.fetchedResultsController.fetchedObjects?.firstIndex(where: { $0 as! GameCollection == gameCollection }) if let gameCollectionIndex = self.fetchedResultsController.fetchedObjects?.index(where: { $0 as! GameCollection == gameCollection })
{ {
index = gameCollectionIndex index = gameCollectionIndex
} }
@ -369,8 +286,7 @@ private extension GamesViewController
self.pageViewController.setViewControllers([viewController], direction: .forward, animated: false, completion: nil) self.pageViewController.setViewControllers([viewController], direction: .forward, animated: false, completion: nil)
// self.title = viewController.title self.title = viewController.title
self.title = ""
self.pageControl.currentPage = index self.pageControl.currentPage = index
} }
} }
@ -393,40 +309,24 @@ private extension GamesViewController
/// Importing /// Importing
extension GamesViewController: ImportControllerDelegate extension GamesViewController: ImportControllerDelegate
{ {
private func makeImportController() -> ImportController @IBAction private func importFiles()
{ {
var documentTypes = Set(System.registeredSystems.map { $0.gameType.rawValue }) var documentTypes = Set(System.allCases.map { $0.gameType.rawValue })
documentTypes.insert(kUTTypeZipArchive as String) documentTypes.insert(kUTTypeZipArchive as String)
documentTypes.insert("com.rileytestut.delta.skin")
#if BETA
// .bin files (Genesis ROMs)
documentTypes.insert("com.apple.macbinary-archive")
#endif
// Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations) // Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations)
documentTypes.insert("com.rileytestut.gba") documentTypes.insert("com.rileytestut.gba")
documentTypes.insert("com.rileytestut.gbc") documentTypes.insert("com.rileytestut.gbc")
documentTypes.insert("com.rileytestut.gb") documentTypes.insert("com.rileytestut.gb")
// let itunesImportOption = iTunesImportOption(presentingViewController: self) let itunesImportOption = iTunesImportOption(presentingViewController: self)
let importController = ImportController(documentTypes: documentTypes) let importController = ImportController(documentTypes: documentTypes)
importController.delegate = self importController.delegate = self
// importController.importOptions = [itunesImportOption] importController.importOptions = [itunesImportOption]
self.present(importController, animated: true, completion: nil)
return importController
} }
@IBAction private func importFiles()
{
self.present(self.importController, animated: true, completion: nil)
}
func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error]) func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error])
{ {
for error in errors for error in errors
@ -462,93 +362,13 @@ extension GamesViewController: ImportControllerDelegate
} }
} }
} }
}
//MARK: - Syncing -
/// Syncing
private extension GamesViewController
{
@IBAction func sync() @IBAction func sync()
{ {
// Show toast view in case sync started before this view controller existed.
self.showSyncingToastViewIfNeeded()
SyncManager.shared.sync() SyncManager.shared.sync()
} }
func showSyncingToastViewIfNeeded()
{
guard let coordinator = SyncManager.shared.coordinator, let syncProgress = SyncManager.shared.syncProgress, coordinator.isSyncing && self.syncingToastView == nil else { return }
let toastView = RSTToastView(text: NSLocalizedString("Syncing...", comment: ""), detailText: syncProgress.localizedAdditionalDescription)
toastView.activityIndicatorView.startAnimating()
toastView.addTarget(self, action: #selector(GamesViewController.hideSyncingToastView), for: .touchUpInside)
toastView.show(in: self.view)
self.syncingProgressObservation = syncProgress.observe(\.localizedAdditionalDescription) { [weak toastView, weak self] (progress, change) in
DispatchQueue.main.async {
// Prevent us from updating text right as we're dismissing the toast view.
guard self?.syncingToastView != nil else { return }
toastView?.detailTextLabel.text = progress.localizedAdditionalDescription
}
}
self.syncingToastView = toastView
}
func showSyncFinishedToastView(result: SyncResult)
{
let toastView: RSTToastView
switch result
{
case .success: toastView = RSTToastView(text: NSLocalizedString("Sync Complete", comment: ""), detailText: nil)
case .failure(let error): toastView = RSTToastView(text: NSLocalizedString("Sync Failed", comment: ""), detailText: error.failureReason)
}
toastView.textLabel.textAlignment = .center
toastView.addTarget(self, action: #selector(GamesViewController.presentSyncResultsViewController), for: .touchUpInside)
toastView.show(in: self.view, duration: 2.0)
self.syncingToastView = nil
}
@objc func hideSyncingToastView()
{
self.syncingToastView = nil
}
@objc func presentSyncResultsViewController()
{
guard let result = SyncManager.shared.previousSyncResult else { return }
let navigationController = SyncResultViewController.make(result: result)
self.present(navigationController, animated: true, completion: nil)
}
func quitEmulation()
{
DispatchQueue.main.async {
self.activeEmulatorCore = nil
if let viewControllers = self.pageViewController.viewControllers as? [GameCollectionViewController]
{
for collectionViewController in viewControllers
{
collectionViewController.activeEmulatorCore = nil
}
}
self.theme = .opaque
}
}
} }
//MARK: - Notifications -
/// Notifications
private extension GamesViewController private extension GamesViewController
{ {
@objc func managedObjectContextDidChange(with notification: Notification) @objc func managedObjectContextDidChange(with notification: Notification)
@ -559,19 +379,25 @@ private extension GamesViewController
{ {
if deletedObjects.contains(game) if deletedObjects.contains(game)
{ {
self.quitEmulation() DispatchQueue.main.async {
self.theme = .opaque
}
} }
} }
else else
{ {
self.quitEmulation() DispatchQueue.main.async {
self.theme = .opaque
}
} }
} }
@objc func syncingDidStart(_ notification: Notification) @objc func syncingDidStart(_ notification: Notification)
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
self.showSyncingToastViewIfNeeded() let toastView = RSTToastView(text: NSLocalizedString("Syncing...", comment: ""), detailText: nil)
toastView.activityIndicatorView.startAnimating()
toastView.show(in: self.view)
} }
} }
@ -579,29 +405,27 @@ private extension GamesViewController
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? SyncResult else { return } guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? SyncResult else { return }
self.showSyncFinishedToastView(result: result)
}
}
@objc func emulationDidQuit(_ notification: Notification) let toastView: RSTToastView
switch result
{ {
self.quitEmulation() case .success: toastView = RSTToastView(text: NSLocalizedString("Sync Complete", comment: ""), detailText: nil)
case .failure(let error): toastView = RSTToastView(text: NSLocalizedString("Sync Failed", comment: ""), detailText: error.failureReason)
} }
@objc func settingsDidChange(_ notification: Notification) toastView.addTarget(self, action: #selector(GamesViewController.presentSyncResultsViewController), for: .touchUpInside)
toastView.show(in: self.view, duration: 2.0)
}
}
@objc func presentSyncResultsViewController()
{ {
guard let emulatorCore = self.activeEmulatorCore else { return } guard let result = SyncManager.shared.previousSyncResult else { return }
guard let game = emulatorCore.game as? Game else { return }
game.managedObjectContext?.performAndWait { let navigationController = SyncResultViewController.make(result: result)
guard self.present(navigationController, animated: true, completion: nil)
let name = notification.userInfo?[Settings.NotificationUserInfoKey.name] as? String, name == Settings.preferredCoreSettingsKey(for: emulatorCore.game.type),
let core = notification.userInfo?[Settings.NotificationUserInfoKey.core] as? DeltaCoreProtocol, core != emulatorCore.deltaCore
else { return }
emulatorCore.stop()
self.quitEmulation()
}
} }
} }
@ -627,7 +451,7 @@ extension GamesViewController: UIPageViewControllerDataSource, UIPageViewControl
{ {
if let viewController = pageViewController.viewControllers?.first as? GameCollectionViewController, let gameCollection = viewController.gameCollection if let viewController = pageViewController.viewControllers?.first as? GameCollectionViewController, let gameCollection = viewController.gameCollection
{ {
let index = self.fetchedResultsController.fetchedObjects?.firstIndex(where: { $0 as! GameCollection == gameCollection }) ?? 0 let index = self.fetchedResultsController.fetchedObjects?.index(where: { $0 as! GameCollection == gameCollection }) ?? 0
self.pageControl.currentPage = index self.pageControl.currentPage = index
Settings.previousGameCollection = gameCollection Settings.previousGameCollection = gameCollection
@ -665,11 +489,3 @@ extension GamesViewController: NSFetchedResultsControllerDelegate
self.updateSections(animated: true) self.updateSections(animated: true)
} }
} }
extension GamesViewController: UIAdaptivePresentationControllerDelegate
{
func presentationControllerDidDismiss(_ presentationController: UIPresentationController)
{
self.sync()
}
}

View File

@ -76,7 +76,6 @@ extension GamesStoryboardSegue: UIViewControllerAnimatedTransitioning
{ {
transitionContext.sourceViewController.beginAppearanceTransition(false, animated: true) transitionContext.sourceViewController.beginAppearanceTransition(false, animated: true)
transitionContext.destinationView.clipsToBounds = false
transitionContext.destinationView.frame = transitionContext.destinationViewFinalFrame! transitionContext.destinationView.frame = transitionContext.destinationViewFinalFrame!
transitionContext.destinationView.transform = CGAffineTransform(scaleX: 2.0, y: 2.0) transitionContext.destinationView.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
transitionContext.containerView.addSubview(transitionContext.destinationView) transitionContext.containerView.addSubview(transitionContext.destinationView)
@ -98,7 +97,6 @@ extension GamesStoryboardSegue: UIViewControllerAnimatedTransitioning
if let navigationController = transitionContext.destinationViewController as? UINavigationController if let navigationController = transitionContext.destinationViewController as? UINavigationController
{ {
let padding: CGFloat = 44 let padding: CGFloat = 44
let topViewController = navigationController.viewControllers[0]
if !navigationController.isNavigationBarHidden if !navigationController.isNavigationBarHidden
{ {
@ -107,18 +105,6 @@ extension GamesStoryboardSegue: UIViewControllerAnimatedTransitioning
topToolbar.barStyle = navigationController.toolbar.barStyle topToolbar.barStyle = navigationController.toolbar.barStyle
transitionContext.destinationView.insertSubview(topToolbar, at: 1) transitionContext.destinationView.insertSubview(topToolbar, at: 1)
if #available(iOS 13, *)
{
let appearance = UIToolbarAppearance(barAppearance: navigationController.navigationBar.standardAppearance)
topToolbar.standardAppearance = appearance
topToolbar.topAnchor.constraint(equalTo: topViewController.view.topAnchor, constant: -padding).isActive = true
topToolbar.leftAnchor.constraint(equalTo: topViewController.view.leftAnchor, constant: -padding).isActive = true
topToolbar.rightAnchor.constraint(equalTo: topViewController.view.rightAnchor, constant: padding).isActive = true
topToolbar.bottomAnchor.constraint(equalTo: topViewController.view.safeAreaLayoutGuide.topAnchor).isActive = true
}
else
{
topToolbar.topAnchor.constraint(equalTo: navigationController.navigationBar.topAnchor, constant: -padding).isActive = true topToolbar.topAnchor.constraint(equalTo: navigationController.navigationBar.topAnchor, constant: -padding).isActive = true
topToolbar.leftAnchor.constraint(equalTo: navigationController.navigationBar.leftAnchor, constant: -padding).isActive = true topToolbar.leftAnchor.constraint(equalTo: navigationController.navigationBar.leftAnchor, constant: -padding).isActive = true
topToolbar.rightAnchor.constraint(equalTo: navigationController.navigationBar.rightAnchor, constant: padding).isActive = true topToolbar.rightAnchor.constraint(equalTo: navigationController.navigationBar.rightAnchor, constant: padding).isActive = true
@ -126,7 +112,6 @@ extension GamesStoryboardSegue: UIViewControllerAnimatedTransitioning
// There is no easy way to determine the extra height necessary at this point of the transition, so hard code for now. // There is no easy way to determine the extra height necessary at this point of the transition, so hard code for now.
let additionalSearchBarHeight = 44 as CGFloat let additionalSearchBarHeight = 44 as CGFloat
topToolbar.heightAnchor.constraint(equalToConstant: navigationController.topViewController!.view.safeAreaInsets.top + additionalSearchBarHeight).isActive = true topToolbar.heightAnchor.constraint(equalToConstant: navigationController.topViewController!.view.safeAreaInsets.top + additionalSearchBarHeight).isActive = true
}
topPaddingToolbar = topToolbar topPaddingToolbar = topToolbar
} }
@ -138,23 +123,10 @@ extension GamesStoryboardSegue: UIViewControllerAnimatedTransitioning
bottomToolbar.barStyle = navigationController.toolbar.barStyle bottomToolbar.barStyle = navigationController.toolbar.barStyle
transitionContext.destinationView.insertSubview(bottomToolbar, belowSubview: navigationController.navigationBar) transitionContext.destinationView.insertSubview(bottomToolbar, belowSubview: navigationController.navigationBar)
if #available(iOS 13, *)
{
let appearance = UIToolbarAppearance(barAppearance: navigationController.toolbar.standardAppearance)
bottomToolbar.standardAppearance = appearance
bottomToolbar.topAnchor.constraint(equalTo: topViewController.view.safeAreaLayoutGuide.bottomAnchor).isActive = true
bottomToolbar.bottomAnchor.constraint(equalTo: topViewController.view.bottomAnchor, constant: padding).isActive = true
bottomToolbar.leftAnchor.constraint(equalTo: topViewController.view.leftAnchor, constant: -padding).isActive = true
bottomToolbar.rightAnchor.constraint(equalTo: topViewController.view.rightAnchor, constant: padding).isActive = true
}
else
{
bottomToolbar.topAnchor.constraint(equalTo: navigationController.toolbar.topAnchor).isActive = true bottomToolbar.topAnchor.constraint(equalTo: navigationController.toolbar.topAnchor).isActive = true
bottomToolbar.bottomAnchor.constraint(equalTo: navigationController.toolbar.bottomAnchor, constant: padding).isActive = true bottomToolbar.bottomAnchor.constraint(equalTo: navigationController.toolbar.bottomAnchor, constant: padding).isActive = true
bottomToolbar.leftAnchor.constraint(equalTo: navigationController.toolbar.leftAnchor, constant: -padding).isActive = true bottomToolbar.leftAnchor.constraint(equalTo: navigationController.toolbar.leftAnchor, constant: -padding).isActive = true
bottomToolbar.rightAnchor.constraint(equalTo: navigationController.toolbar.rightAnchor, constant: padding).isActive = true bottomToolbar.rightAnchor.constraint(equalTo: navigationController.toolbar.rightAnchor, constant: padding).isActive = true
}
bottomPaddingToolbar = bottomToolbar bottomPaddingToolbar = bottomToolbar
} }

Some files were not shown because too many files have changed in this diff Show More