Integrates CheatBase to browse and easily add cheats for recognized games
Limited to DS games right now.
This commit is contained in:
parent
d204ea35bd
commit
a135ea236d
@ -169,7 +169,13 @@
|
|||||||
D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */; };
|
D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */; };
|
||||||
D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */; };
|
D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */; };
|
||||||
D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */; };
|
D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */; };
|
||||||
|
D5864970297734280081477E /* CheatMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586496F297734280081477E /* CheatMetadata.swift */; };
|
||||||
|
D586497229774ABD0081477E /* CheatBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D586497129774ABD0081477E /* CheatBase.swift */; };
|
||||||
|
D5864978297756CE0081477E /* CheatBaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5864977297756CE0081477E /* CheatBaseView.swift */; };
|
||||||
D5A98CE2284EF14B00E023E5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */; };
|
D5A98CE2284EF14B00E023E5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */; };
|
||||||
|
D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF27629884F8600F21ACF /* CheatDevice.swift */; };
|
||||||
|
D5B6A6472988651800223C5F /* cheatbase.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = D5B6A6462988651400223C5F /* cheatbase.sqlite */; };
|
||||||
|
D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -366,7 +372,13 @@
|
|||||||
D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+JIT.swift"; sourceTree = "<group>"; };
|
D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+JIT.swift"; sourceTree = "<group>"; };
|
||||||
D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerManager+Delta.swift"; sourceTree = "<group>"; };
|
D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerManager+Delta.swift"; sourceTree = "<group>"; };
|
||||||
D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+Filename.swift"; sourceTree = "<group>"; };
|
D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+Filename.swift"; sourceTree = "<group>"; };
|
||||||
|
D586496F297734280081477E /* CheatMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatMetadata.swift; sourceTree = "<group>"; };
|
||||||
|
D586497129774ABD0081477E /* CheatBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBase.swift; sourceTree = "<group>"; };
|
||||||
|
D5864977297756CE0081477E /* CheatBaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBaseView.swift; sourceTree = "<group>"; };
|
||||||
D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
D5AAF27629884F8600F21ACF /* CheatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatDevice.swift; sourceTree = "<group>"; };
|
||||||
|
D5B6A6462988651400223C5F /* cheatbase.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; name = cheatbase.sqlite; path = External/CheatBase/cheatbase.sqlite; sourceTree = SOURCE_ROOT; };
|
||||||
|
D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySearchBar.swift; sourceTree = "<group>"; };
|
||||||
DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@ -527,6 +539,7 @@
|
|||||||
BF59426D1E09BC5D0051894B /* DatabaseManager.swift */,
|
BF59426D1E09BC5D0051894B /* DatabaseManager.swift */,
|
||||||
BF5942711E09BC690051894B /* Model */,
|
BF5942711E09BC690051894B /* Model */,
|
||||||
BF95E2751E49763D0030E7AD /* OpenVGDB */,
|
BF95E2751E49763D0030E7AD /* OpenVGDB */,
|
||||||
|
D586496E297734060081477E /* Cheats */,
|
||||||
);
|
);
|
||||||
path = Database;
|
path = Database;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -781,6 +794,7 @@
|
|||||||
children = (
|
children = (
|
||||||
BF6BB2451BB73FE800CCF94A /* Assets.xcassets */,
|
BF6BB2451BB73FE800CCF94A /* Assets.xcassets */,
|
||||||
BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */,
|
BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */,
|
||||||
|
D5B6A6462988651400223C5F /* cheatbase.sqlite */,
|
||||||
);
|
);
|
||||||
path = Resources;
|
path = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -891,6 +905,18 @@
|
|||||||
path = Launch;
|
path = Launch;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D586496E297734060081477E /* Cheats */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D586496F297734280081477E /* CheatMetadata.swift */,
|
||||||
|
D5AAF27629884F8600F21ACF /* CheatDevice.swift */,
|
||||||
|
D586497129774ABD0081477E /* CheatBase.swift */,
|
||||||
|
D5864977297756CE0081477E /* CheatBaseView.swift */,
|
||||||
|
D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */,
|
||||||
|
);
|
||||||
|
path = Cheats;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
FD1E8AE87FA2DB8793F7B937 /* Pods */ = {
|
FD1E8AE87FA2DB8793F7B937 /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -999,6 +1025,7 @@
|
|||||||
BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */,
|
BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */,
|
||||||
BF71CF8A1FE904B1001F1613 /* GameTableViewCell.xib in Resources */,
|
BF71CF8A1FE904B1001F1613 /* GameTableViewCell.xib in Resources */,
|
||||||
BFFC46461D59861000AF2CC6 /* LaunchScreen.storyboard in Resources */,
|
BFFC46461D59861000AF2CC6 /* LaunchScreen.storyboard in Resources */,
|
||||||
|
D5B6A6472988651800223C5F /* cheatbase.sqlite in Resources */,
|
||||||
BF1F45AB21AF4B5800EF9895 /* SyncResultsViewController.storyboard in Resources */,
|
BF1F45AB21AF4B5800EF9895 /* SyncResultsViewController.storyboard in Resources */,
|
||||||
BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */,
|
BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */,
|
||||||
BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */,
|
BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */,
|
||||||
@ -1158,6 +1185,7 @@
|
|||||||
BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */,
|
BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */,
|
||||||
BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */,
|
BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */,
|
||||||
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */,
|
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */,
|
||||||
|
D586497229774ABD0081477E /* CheatBase.swift in Sources */,
|
||||||
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */,
|
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */,
|
||||||
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */,
|
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */,
|
||||||
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */,
|
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */,
|
||||||
@ -1168,6 +1196,7 @@
|
|||||||
BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */,
|
BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */,
|
||||||
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */,
|
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */,
|
||||||
D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */,
|
D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */,
|
||||||
|
D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */,
|
||||||
BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */,
|
BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */,
|
||||||
BFD1EF402336BD8800D197CF /* UIDevice+Processor.swift in Sources */,
|
BFD1EF402336BD8800D197CF /* UIDevice+Processor.swift in Sources */,
|
||||||
BF71CF871FE90006001F1613 /* AppIconShortcutsViewController.swift in Sources */,
|
BF71CF871FE90006001F1613 /* AppIconShortcutsViewController.swift in Sources */,
|
||||||
@ -1176,6 +1205,7 @@
|
|||||||
BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */,
|
BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */,
|
||||||
BFFA4C091E8A24D600D87934 /* GameTableViewCell.swift in Sources */,
|
BFFA4C091E8A24D600D87934 /* GameTableViewCell.swift in Sources */,
|
||||||
BFFC46231D5984A000AF2CC6 /* LaunchViewController.swift in Sources */,
|
BFFC46231D5984A000AF2CC6 /* LaunchViewController.swift in Sources */,
|
||||||
|
D5864970297734280081477E /* CheatMetadata.swift in Sources */,
|
||||||
BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */,
|
BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */,
|
||||||
BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */,
|
BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */,
|
||||||
BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */,
|
BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */,
|
||||||
@ -1187,6 +1217,7 @@
|
|||||||
BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */,
|
BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */,
|
||||||
BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */,
|
BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */,
|
||||||
BFC9B7391CEFCD34008629BB /* CheatsViewController.swift in Sources */,
|
BFC9B7391CEFCD34008629BB /* CheatsViewController.swift in Sources */,
|
||||||
|
D5864978297756CE0081477E /* CheatBaseView.swift in Sources */,
|
||||||
BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */,
|
BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */,
|
||||||
BFFC464C1D5998D600AF2CC6 /* CheatTableViewCell.swift in Sources */,
|
BFFC464C1D5998D600AF2CC6 /* CheatTableViewCell.swift in Sources */,
|
||||||
BF5942941E09BD1A0051894B /* NSManagedObject+Conveniences.swift in Sources */,
|
BF5942941E09BD1A0051894B /* NSManagedObject+Conveniences.swift in Sources */,
|
||||||
@ -1205,6 +1236,7 @@
|
|||||||
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */,
|
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */,
|
||||||
BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */,
|
BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */,
|
||||||
BF8A333421A484A000A42FD4 /* BadgedTableViewCell.swift in Sources */,
|
BF8A333421A484A000A42FD4 /* BadgedTableViewCell.swift in Sources */,
|
||||||
|
D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */,
|
||||||
BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */,
|
BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */,
|
||||||
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */,
|
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */,
|
||||||
BF3D6C512202865F0083E05A /* Delta2ToDelta3.xcmappingmodel in Sources */,
|
BF3D6C512202865F0083E05A /* Delta2ToDelta3.xcmappingmodel in Sources */,
|
||||||
|
|||||||
141
Delta/Database/Cheats/CheatBase.swift
Normal file
141
Delta/Database/Cheats/CheatBase.swift
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
251
Delta/Database/Cheats/CheatBaseView.swift
Normal file
251
Delta/Database/Cheats/CheatBaseView.swift
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
//
|
||||||
|
// CheatBaseView.swift
|
||||||
|
// Delta
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 1/17/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@available(iOS 14, *)
|
||||||
|
extension CheatBaseView
|
||||||
|
{
|
||||||
|
private class ViewModel: ObservableObject
|
||||||
|
{
|
||||||
|
@Published
|
||||||
|
private(set) var database: CheatBase?
|
||||||
|
|
||||||
|
@Published
|
||||||
|
private(set) var allCheats: [CheatMetadata]?
|
||||||
|
|
||||||
|
@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()
|
||||||
|
self.database = database
|
||||||
|
|
||||||
|
let cheats = try await database.cheats(for: game) ?? []
|
||||||
|
self.allCheats = cheats
|
||||||
|
|
||||||
|
let cheatsByCategory = Dictionary(grouping: cheats, by: { $0.category }).sorted { $0.key.id < $1.key.id }
|
||||||
|
self.cheatsByCategory = cheatsByCategory
|
||||||
|
}
|
||||||
|
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("Activation Hint"),
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 14, *)
|
||||||
|
private extension CheatBaseView
|
||||||
|
{
|
||||||
|
func choose(_ cheatMetadata: CheatMetadata)
|
||||||
|
{
|
||||||
|
self.selectionHandler?(cheatMetadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
111
Delta/Database/Cheats/CheatDevice.swift
Normal file
111
Delta/Database/Cheats/CheatDevice.swift
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Delta/Database/Cheats/CheatMetadata.swift
Normal file
45
Delta/Database/Cheats/CheatMetadata.swift
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Delta/Database/Cheats/LegacySearchBar.swift
Normal file
51
Delta/Database/Cheats/LegacySearchBar.swift
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -261,6 +261,12 @@ private extension DatabaseManager
|
|||||||
try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.gamesDatabaseURL, shouldReplace: true)
|
try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.gamesDatabaseURL, shouldReplace: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if #available(iOS 14, *), !FileManager.default.fileExists(atPath: DatabaseManager.cheatBaseURL.path) || CheatBase.cheatsVersion != CheatBase.previousCheatsVersion
|
||||||
|
{
|
||||||
|
guard let bundleURL = Bundle.main.url(forResource: "cheatbase", withExtension: "sqlite") else { throw GamesDatabase.Error.doesNotExist }
|
||||||
|
try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.cheatBaseURL, shouldReplace: true)
|
||||||
|
}
|
||||||
|
|
||||||
self.gamesDatabase = try GamesDatabase()
|
self.gamesDatabase = try GamesDatabase()
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@ -618,6 +624,12 @@ extension DatabaseManager
|
|||||||
let gamesDatabaseURL = self.defaultDirectoryURL().appendingPathComponent("openvgdb.sqlite")
|
let gamesDatabaseURL = self.defaultDirectoryURL().appendingPathComponent("openvgdb.sqlite")
|
||||||
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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -11,15 +11,17 @@ 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 identifier: Int
|
let releaseID: Int
|
||||||
|
let romID: Int
|
||||||
|
|
||||||
let name: String?
|
let name: String?
|
||||||
let artworkURL: URL?
|
let artworkURL: URL?
|
||||||
|
|
||||||
init(identifier: Int, name: String?, artworkURL: URL?)
|
init(releaseID: Int, romID: 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,13 +29,13 @@ class GameMetadata: NSObject
|
|||||||
extension GameMetadata
|
extension GameMetadata
|
||||||
{
|
{
|
||||||
override var hash: Int {
|
override var hash: Int {
|
||||||
return self.identifier.hashValue
|
return self.releaseID.hashValue ^ self.romID.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.identifier == metadata.identifier
|
return self.releaseID == metadata.releaseID && self.romID == metadata.romID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,13 +60,12 @@ extension GamesDatabase
|
|||||||
enum Error: Swift.Error
|
enum Error: Swift.Error
|
||||||
{
|
{
|
||||||
case doesNotExist
|
case doesNotExist
|
||||||
case connection(Swift.Error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GamesDatabase
|
class GamesDatabase
|
||||||
{
|
{
|
||||||
static let version = 2
|
static let version = 3
|
||||||
static var previousVersion: Int? {
|
static var previousVersion: Int? {
|
||||||
return UserDefaults.standard.previousGamesDatabaseVersion
|
return UserDefaults.standard.previousGamesDatabaseVersion
|
||||||
}
|
}
|
||||||
@ -83,7 +82,7 @@ class GamesDatabase
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
throw Error.connection(error)
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
self.invalidateVirtualTableIfNeeded()
|
self.invalidateVirtualTableIfNeeded()
|
||||||
@ -92,10 +91,11 @@ 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, name, artworkAddress).filter(name.match(gameName + "*"))
|
let query = VirtualTable.search.select(releaseID, romID, name, artworkAddress).filter(name.match(gameName + "*"))
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@ -114,7 +114,7 @@ class GamesDatabase
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let metadata = GameMetadata(identifier: row[releaseID], name: row[name], artworkURL: artworkURL)
|
let metadata = GameMetadata(releaseID: row[releaseID], romID: row[romID], name: row[name], artworkURL: artworkURL)
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +148,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).filter(sha1Hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID])
|
let query = Table.roms.select(releaseID, name, artworkAddress, Table.roms[romID]).filter(sha1Hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID])
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@ -164,7 +164,7 @@ class GamesDatabase
|
|||||||
artworkURL = nil
|
artworkURL = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = GameMetadata(identifier: row[releaseID], name: row[name], artworkURL: artworkURL)
|
let metadata = GameMetadata(releaseID: row[releaseID], romID: row[Table.roms[romID]], name: row[name], artworkURL: artworkURL)
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,12 +200,13 @@ 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, name, artworkAddress], tokenize: .Unicode61())))
|
try self.connection.run(VirtualTable.search.create(.FTS4([releaseID, romID, name, artworkAddress], tokenize: .Unicode61())))
|
||||||
|
|
||||||
let update = VirtualTable.search.insert(Table.releases.select(releaseID, name, artworkAddress))
|
let update = VirtualTable.search.insert(Table.releases.select(releaseID, romID, name, artworkAddress))
|
||||||
_ = try self.connection.run(update)
|
_ = try self.connection.run(update)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@ -12,13 +12,26 @@ import DeltaCore
|
|||||||
|
|
||||||
extension CheatValidator
|
extension CheatValidator
|
||||||
{
|
{
|
||||||
enum Error: Swift.Error
|
enum Error: LocalizedError
|
||||||
{
|
{
|
||||||
case invalidCode
|
case invalidCode
|
||||||
case invalidName
|
case invalidName
|
||||||
case invalidGame
|
case invalidGame
|
||||||
case duplicateName
|
case duplicateName
|
||||||
case duplicateCode
|
case duplicateCode
|
||||||
|
case unknownCheatType
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .invalidCode: return NSLocalizedString("The cheat code isn't in the correct format.", comment: "")
|
||||||
|
case .invalidName: return NSLocalizedString("The name of this cheat is invalid.", comment: "")
|
||||||
|
case .invalidGame: return NSLocalizedString("There is no associated game with this cheat.", comment: "")
|
||||||
|
case .duplicateName: return NSLocalizedString("A cheat already exists with this name.", comment: "")
|
||||||
|
case .duplicateCode: return NSLocalizedString("A cheat already exists with this code.", comment: "")
|
||||||
|
case .unknownCheatType: return NSLocalizedString("Delta does not support this cheat type.", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
import DeltaCore
|
import DeltaCore
|
||||||
|
|
||||||
@ -61,6 +62,24 @@ extension CheatsViewController
|
|||||||
self.tableView.separatorEffect = vibrancyEffect
|
self.tableView.separatorEffect = vibrancyEffect
|
||||||
|
|
||||||
self.registerForPreviewing(with: self, sourceView: self.tableView)
|
self.registerForPreviewing(with: self, sourceView: self.tableView)
|
||||||
|
|
||||||
|
if #available(iOS 14, *)
|
||||||
|
{
|
||||||
|
let addCheatMenu = UIMenu(children: [
|
||||||
|
UIAction(title: NSLocalizedString("New Cheat Code", comment: ""), image: UIImage(systemName: "square.and.pencil")) { [weak self] _ in
|
||||||
|
self?.addCheat()
|
||||||
|
},
|
||||||
|
|
||||||
|
UIAction(title: NSLocalizedString("Search CheatBase", comment: ""), image: UIImage(systemName: "magnifyingglass")) { [weak self] _ in
|
||||||
|
self?.searchCheatBase()
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
self.navigationItem.rightBarButtonItem?.target = nil
|
||||||
|
self.navigationItem.rightBarButtonItem?.action = nil
|
||||||
|
|
||||||
|
self.navigationItem.rightBarButtonItem?.menu = addCheatMenu
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didReceiveMemoryWarning()
|
override func didReceiveMemoryWarning()
|
||||||
@ -103,6 +122,59 @@ private extension CheatsViewController
|
|||||||
editCheatViewController.presentWithPresentingViewController(self)
|
editCheatViewController.presentWithPresentingViewController(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 14, *)
|
||||||
|
func searchCheatBase()
|
||||||
|
{
|
||||||
|
var rootView = CheatBaseView(game: self.game)
|
||||||
|
rootView.cancellationHandler = { [weak self] in
|
||||||
|
self?.presentedViewController?.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView.selectionHandler = { [weak self] cheatMetadata in
|
||||||
|
self?.saveCheatMetadata(cheatMetadata)
|
||||||
|
self?.presentedViewController?.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hostingController = UIHostingController(rootView: rootView)
|
||||||
|
self.present(hostingController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveCheatMetadata(_ cheatMetadata: CheatMetadata)
|
||||||
|
{
|
||||||
|
DatabaseManager.shared.performBackgroundTask { context in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
guard let cheatType = cheatMetadata.device.cheatType, let cheatFormat = cheatMetadata.device.cheatFormat else { throw CheatValidator.Error.unknownCheatType }
|
||||||
|
|
||||||
|
let cheat = Cheat(context: context)
|
||||||
|
cheat.name = cheatMetadata.name
|
||||||
|
cheat.type = cheatType
|
||||||
|
cheat.isEnabled = true
|
||||||
|
|
||||||
|
let sanitizedCode = cheatMetadata.code.components(separatedBy: .whitespacesAndNewlines).joined()
|
||||||
|
let formattedCode = sanitizedCode.formatted(with: cheatFormat)
|
||||||
|
cheat.code = formattedCode
|
||||||
|
|
||||||
|
let game = context.object(with: self.game.objectID) as! Game
|
||||||
|
cheat.game = game
|
||||||
|
|
||||||
|
let validator = CheatValidator(format: cheatFormat, managedObjectContext: context)
|
||||||
|
try validator.validate(cheat)
|
||||||
|
|
||||||
|
self.delegate?.cheatsViewController(self, activateCheat: cheat)
|
||||||
|
|
||||||
|
try context.save()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("Unable to Add Cheat", comment: ""), error: error)
|
||||||
|
self.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func deleteCheat(_ cheat: Cheat)
|
func deleteCheat(_ cheat: Cheat)
|
||||||
{
|
{
|
||||||
self.delegate?.cheatsViewController(self, deactivateCheat: cheat)
|
self.delegate?.cheatsViewController(self, deactivateCheat: cheat)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user