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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -366,7 +372,13 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@ -527,6 +539,7 @@
|
||||
BF59426D1E09BC5D0051894B /* DatabaseManager.swift */,
|
||||
BF5942711E09BC690051894B /* Model */,
|
||||
BF95E2751E49763D0030E7AD /* OpenVGDB */,
|
||||
D586496E297734060081477E /* Cheats */,
|
||||
);
|
||||
path = Database;
|
||||
sourceTree = "<group>";
|
||||
@ -781,6 +794,7 @@
|
||||
children = (
|
||||
BF6BB2451BB73FE800CCF94A /* Assets.xcassets */,
|
||||
BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */,
|
||||
D5B6A6462988651400223C5F /* cheatbase.sqlite */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
@ -891,6 +905,18 @@
|
||||
path = Launch;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -999,6 +1025,7 @@
|
||||
BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */,
|
||||
BF71CF8A1FE904B1001F1613 /* GameTableViewCell.xib in Resources */,
|
||||
BFFC46461D59861000AF2CC6 /* LaunchScreen.storyboard in Resources */,
|
||||
D5B6A6472988651800223C5F /* cheatbase.sqlite in Resources */,
|
||||
BF1F45AB21AF4B5800EF9895 /* SyncResultsViewController.storyboard in Resources */,
|
||||
BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */,
|
||||
BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */,
|
||||
@ -1158,6 +1185,7 @@
|
||||
BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */,
|
||||
BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */,
|
||||
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */,
|
||||
D586497229774ABD0081477E /* CheatBase.swift in Sources */,
|
||||
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */,
|
||||
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */,
|
||||
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */,
|
||||
@ -1168,6 +1196,7 @@
|
||||
BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */,
|
||||
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */,
|
||||
D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */,
|
||||
D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */,
|
||||
BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */,
|
||||
BFD1EF402336BD8800D197CF /* UIDevice+Processor.swift in Sources */,
|
||||
BF71CF871FE90006001F1613 /* AppIconShortcutsViewController.swift in Sources */,
|
||||
@ -1176,6 +1205,7 @@
|
||||
BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */,
|
||||
BFFA4C091E8A24D600D87934 /* GameTableViewCell.swift in Sources */,
|
||||
BFFC46231D5984A000AF2CC6 /* LaunchViewController.swift in Sources */,
|
||||
D5864970297734280081477E /* CheatMetadata.swift in Sources */,
|
||||
BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */,
|
||||
BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */,
|
||||
BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */,
|
||||
@ -1187,6 +1217,7 @@
|
||||
BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */,
|
||||
BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */,
|
||||
BFC9B7391CEFCD34008629BB /* CheatsViewController.swift in Sources */,
|
||||
D5864978297756CE0081477E /* CheatBaseView.swift in Sources */,
|
||||
BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */,
|
||||
BFFC464C1D5998D600AF2CC6 /* CheatTableViewCell.swift in Sources */,
|
||||
BF5942941E09BD1A0051894B /* NSManagedObject+Conveniences.swift in Sources */,
|
||||
@ -1205,6 +1236,7 @@
|
||||
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */,
|
||||
BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */,
|
||||
BF8A333421A484A000A42FD4 /* BadgedTableViewCell.swift in Sources */,
|
||||
D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */,
|
||||
BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */,
|
||||
BF107EC41BF413F000E0C32C /* GamesViewController.swift 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)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
catch
|
||||
@ -618,6 +624,12 @@ extension DatabaseManager
|
||||
let gamesDatabaseURL = self.defaultDirectoryURL().appendingPathComponent("openvgdb.sqlite")
|
||||
return gamesDatabaseURL
|
||||
}
|
||||
|
||||
class var cheatBaseURL: URL
|
||||
{
|
||||
let gamesDatabaseURL = self.defaultDirectoryURL().appendingPathComponent("cheatbase.sqlite")
|
||||
return gamesDatabaseURL
|
||||
}
|
||||
|
||||
class var gamesDirectoryURL: URL
|
||||
{
|
||||
|
||||
@ -11,15 +11,17 @@ import Foundation
|
||||
// Must be an NSObject subclass so it can be used with RSTCellContentDataSource.
|
||||
class GameMetadata: NSObject
|
||||
{
|
||||
let identifier: Int
|
||||
let releaseID: Int
|
||||
let romID: Int
|
||||
|
||||
let name: String?
|
||||
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.identifier = identifier
|
||||
self.artworkURL = artworkURL
|
||||
}
|
||||
}
|
||||
@ -27,13 +29,13 @@ class GameMetadata: NSObject
|
||||
extension GameMetadata
|
||||
{
|
||||
override var hash: Int {
|
||||
return self.identifier.hashValue
|
||||
return self.releaseID.hashValue ^ self.romID.hashValue
|
||||
}
|
||||
|
||||
override func isEqual(_ object: Any?) -> Bool
|
||||
{
|
||||
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
|
||||
{
|
||||
case doesNotExist
|
||||
case connection(Swift.Error)
|
||||
}
|
||||
}
|
||||
|
||||
class GamesDatabase
|
||||
{
|
||||
static let version = 2
|
||||
static let version = 3
|
||||
static var previousVersion: Int? {
|
||||
return UserDefaults.standard.previousGamesDatabaseVersion
|
||||
}
|
||||
@ -83,7 +82,7 @@ class GamesDatabase
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw Error.connection(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
self.invalidateVirtualTableIfNeeded()
|
||||
@ -92,10 +91,11 @@ class GamesDatabase
|
||||
func metadataResults(forGameName gameName: String) -> [GameMetadata]
|
||||
{
|
||||
let releaseID = Expression<Any>.releaseID
|
||||
let romID = Expression<Any>.romID
|
||||
let name = Expression<Any>.name
|
||||
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
|
||||
{
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -148,7 +148,7 @@ class GamesDatabase
|
||||
let romID = Expression<Any>.romID
|
||||
|
||||
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
|
||||
{
|
||||
@ -164,7 +164,7 @@ class GamesDatabase
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -200,12 +200,13 @@ private extension GamesDatabase
|
||||
let name = Expression<Any>.name
|
||||
let artworkAddress = Expression<Any>.artworkAddress
|
||||
let releaseID = Expression<Any>.releaseID
|
||||
let romID = Expression<Any>.romID
|
||||
|
||||
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)
|
||||
}
|
||||
catch
|
||||
|
||||
@ -12,13 +12,26 @@ import DeltaCore
|
||||
|
||||
extension CheatValidator
|
||||
{
|
||||
enum Error: Swift.Error
|
||||
enum Error: LocalizedError
|
||||
{
|
||||
case invalidCode
|
||||
case invalidName
|
||||
case invalidGame
|
||||
case duplicateName
|
||||
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 CoreData
|
||||
import SwiftUI
|
||||
|
||||
import DeltaCore
|
||||
|
||||
@ -61,6 +62,24 @@ extension CheatsViewController
|
||||
self.tableView.separatorEffect = vibrancyEffect
|
||||
|
||||
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()
|
||||
@ -103,6 +122,59 @@ private extension CheatsViewController
|
||||
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)
|
||||
{
|
||||
self.delegate?.cheatsViewController(self, deactivateCheat: cheat)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user