GBA003/Delta/Pause Menu/Cheats/CheatsViewController.swift
2024-05-30 10:59:44 +08:00

362 lines
13 KiB
Swift

//
// CheatsViewController.swift
// Delta
//
// Created by Riley Testut on 5/20/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import UIKit
import CoreData
import SwiftUI
import DeltaCore
//import MelonDSDeltaCore
import Roxas
protocol CheatsViewControllerDelegate: class
{
func cheatsViewController(_ cheatsViewController: CheatsViewController, activateCheat cheat: Cheat)
func cheatsViewController(_ cheatsViewController: CheatsViewController, deactivateCheat cheat: Cheat)
}
class CheatsViewController: UITableViewController
{
var game: Game! {
didSet {
self.updateDataSource()
}
}
weak var delegate: CheatsViewControllerDelegate?
private let dataSource = RSTFetchedResultsTableViewDataSource<Cheat>(fetchedResultsController: NSFetchedResultsController())
private var cheatBaseCheats: [CheatMetadata]?
}
extension CheatsViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.title = NSLocalizedString("Cheats", comment: "")
let vibrancyEffect = UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark))
let vibrancyView = UIVisualEffectView(effect: vibrancyEffect)
let placeholderView = RSTPlaceholderView(frame: CGRect(x: 0, y: 0, width: vibrancyView.bounds.width, height: vibrancyView.bounds.height))
placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
placeholderView.textLabel.text = NSLocalizedString("No Cheats", comment: "")
placeholderView.textLabel.textColor = UIColor.white
placeholderView.detailTextLabel.text = NSLocalizedString("You can add a new cheat by pressing the + button in the top right.", comment: "")
placeholderView.detailTextLabel.textColor = UIColor.white
vibrancyView.contentView.addSubview(placeholderView)
self.dataSource.placeholderView = vibrancyView
self.dataSource.rowAnimation = .automatic
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
self.configure(cell, for: indexPath)
}
self.tableView.dataSource = self.dataSource
self.tableView.separatorEffect = vibrancyEffect
self.registerForPreviewing(with: self, sourceView: self.tableView)
if #available(iOS 14, *)
{
self.updateAddCheatMenu()
}
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if #available(iOS 14, *), self.cheatBaseCheats == nil
{
self.fetchCheatBaseCheats()
}
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
//MARK: - Navigation -
private extension CheatsViewController
{
@IBAction func unwindFromEditCheatViewController(_ segue: UIStoryboardSegue)
{
}
}
//MARK: - Update -
private extension CheatsViewController
{
func updateDataSource()
{
let fetchRequest: NSFetchRequest<Cheat> = Cheat.fetchRequest()
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Cheat.game), self.game)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Cheat.name), ascending: true)]
self.dataSource.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: nil, cacheName: nil)
}
@available(iOS 14, *) @MainActor
func updateAddCheatMenu()
{
// CheatBase only contains DS cheats for now, so hide option completely for other systems.
guard self.game.type == .ds else { return }
var searchCheatBaseTitle = NSLocalizedString("Search CheatBase", comment: "")
var attributes: UIMenuElement.Attributes = []
if let cheats = self.cheatBaseCheats, cheats.isEmpty
{
searchCheatBaseTitle = NSLocalizedString("No Cheats in CheatBase", comment: "")
attributes = [.disabled]
}
let addCheatMenu = UIMenu(children: [
UIAction(title: NSLocalizedString("New Cheat Code", comment: ""), image: UIImage(systemName: "square.and.pencil")) { [weak self] _ in
self?.addCheat()
},
UIAction(title: searchCheatBaseTitle, image: UIImage(systemName: "magnifyingglass"), attributes: attributes) { [weak self] _ in
self?.searchCheatBase()
},
])
self.navigationItem.rightBarButtonItem?.target = nil
self.navigationItem.rightBarButtonItem?.action = nil
self.navigationItem.rightBarButtonItem?.menu = addCheatMenu
}
}
//MARK: - Managing Cheats -
/// Managing Cheats
private extension CheatsViewController
{
@IBAction func addCheat()
{
let editCheatViewController = self.makeEditCheatViewController(cheat: nil)
editCheatViewController.presentWithPresentingViewController(self)
}
@available(iOS 14, *)
func fetchCheatBaseCheats()
{
Task {
do
{
let cheatBase = try CheatBase()
let cheats = try await cheatBase.cheats(for: self.game) ?? []
self.cheatBaseCheats = cheats
self.updateAddCheatMenu()
}
catch
{
print("[RSTLog] Failed to prefetch cheats from CheatBase:", error)
}
}
}
@available(iOS 14, *)
func searchCheatBase()
{
var rootView = CheatBaseView(game: self.game, cheats: self.cheatBaseCheats)
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)
DatabaseManager.shared.performBackgroundTask { (context) in
let temporaryCheat = context.object(with: cheat.objectID)
context.delete(temporaryCheat)
context.saveWithErrorLogging()
}
}
}
//MARK: - Convenience -
/// Convenience
private extension CheatsViewController
{
func configure(_ cell: UITableViewCell, for indexPath: IndexPath)
{
let cheat = self.dataSource.item(at: indexPath)
cell.textLabel?.text = cheat.name
cell.textLabel?.font = UIFont.boldSystemFont(ofSize: cell.textLabel!.font.pointSize)
cell.textLabel?.textColor = UIColor.white
cell.accessoryType = cheat.isEnabled ? .checkmark : .none
}
func makeEditCheatViewController(cheat: Cheat?) -> EditCheatViewController
{
let editCheatViewController = self.storyboard!.instantiateViewController(withIdentifier: "editCheatViewController") as! EditCheatViewController
editCheatViewController.delegate = self
editCheatViewController.cheat = cheat
editCheatViewController.game = self.game
return editCheatViewController
}
}
extension CheatsViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let cheat = self.dataSource.item(at: indexPath)
let backgroundContext = DatabaseManager.shared.newBackgroundContext()
backgroundContext.performAndWait {
let temporaryCheat = backgroundContext.object(with: cheat.objectID) as! Cheat
temporaryCheat.isEnabled = !temporaryCheat.isEnabled
if temporaryCheat.isEnabled
{
self.delegate?.cheatsViewController(self, activateCheat: temporaryCheat)
}
else
{
self.delegate?.cheatsViewController(self, deactivateCheat: temporaryCheat)
}
backgroundContext.saveWithErrorLogging()
}
self.tableView.reloadRows(at: [indexPath], with: .none)
}
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?
{
let cheat = self.dataSource.item(at: indexPath)
let deleteAction = UITableViewRowAction(style: .destructive, title: NSLocalizedString("Delete", comment: "")) { (action, indexPath) in
self.deleteCheat(cheat)
}
let editAction = UITableViewRowAction(style: .normal, title: NSLocalizedString("Edit", comment: "")) { (action, indexPath) in
let editCheatViewController = self.makeEditCheatViewController(cheat: cheat)
editCheatViewController.presentWithPresentingViewController(self)
}
return [deleteAction, editAction]
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
{
// This method intentionally left blank because someone decided it was a Good Idea to require this method be implemented to use UITableViewRowActions
}
}
//MARK: - <UIViewControllerPreviewingDelegate> -
extension CheatsViewController: UIViewControllerPreviewingDelegate
{
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
{
guard let indexPath = self.tableView.indexPathForRow(at: location) else { return nil }
let frame = self.tableView.rectForRow(at: indexPath)
previewingContext.sourceRect = frame
let cheat = self.dataSource.item(at: indexPath)
let editCheatViewController = self.makeEditCheatViewController(cheat: cheat)
editCheatViewController.isPreviewing = true
return editCheatViewController
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
{
let editCheatViewController = viewControllerToCommit as! EditCheatViewController
editCheatViewController.isPreviewing = false
editCheatViewController.presentWithPresentingViewController(self)
}
}
//MARK: - <EditCheatViewControllerDelegate> -
extension CheatsViewController: EditCheatViewControllerDelegate
{
func editCheatViewController(_ editCheatViewController: EditCheatViewController, activateCheat cheat: Cheat, previousCheat: Cheat?)
{
self.delegate?.cheatsViewController(self, activateCheat: cheat)
if let previousCheat = previousCheat
{
let code = cheat.code
previousCheat.managedObjectContext?.performAndWait({
guard previousCheat.code != code else { return }
self.delegate?.cheatsViewController(self, deactivateCheat: previousCheat)
})
}
}
func editCheatViewController(_ editCheatViewController: EditCheatViewController, deactivateCheat cheat: Cheat)
{
self.delegate?.cheatsViewController(self, deactivateCheat: cheat)
}
}