Throws SyncValidationError when downloading corrupted versions of “verified” SaveStates

After reviewing save states upon first launch, Delta will upload the verified game ID as metadata to ensure other devices don’t download remote versions with incorrect relationships.
This commit is contained in:
Riley Testut 2023-08-11 17:33:30 -05:00
parent dc3a5b479c
commit d1643dbc8f
8 changed files with 2454 additions and 2385 deletions

View File

@ -23,12 +23,6 @@ public class GameSave: _GameSave
}
}
extension GameSave
{
// Hacky, but YOLO I'm under time crunch.
public static var ignoredCorruptedIDs = Set<String>()
}
extension GameSave: Syncable
{
public static var syncablePrimaryKey: AnyKeyPath {
@ -104,7 +98,7 @@ extension GameSave: Syncable
}
catch let error as SyncValidationError
{
guard GameSave.ignoredCorruptedIDs.contains(self.identifier) else { throw error }
guard SyncManager.shared.ignoredCorruptedRecordIDs.contains(record.recordID) else { throw error }
let fetchRequest = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), self.identifier)

View File

@ -134,7 +134,7 @@ extension SaveState: Syncable
public var syncableMetadata: [HarmonyMetadataKey : String] {
guard let game = self.game else { return [:] }
return [.gameID: game.identifier, .gameName: game.name, .coreID: self.coreIdentifier].compactMapValues { $0 }
return [.gameID: game.identifier, .gameName: game.name, .coreID: self.coreIdentifier, .verifiedGameID: game.identifier].compactMapValues { $0 }
}
public var syncableLocalizedName: String? {
@ -143,21 +143,51 @@ extension SaveState: Syncable
public func awakeFromSync(_ record: AnyRecord)
{
guard self.coreIdentifier == nil else { return }
guard let game = self.game, let system = System(gameType: game.type) else { return }
if let coreIdentifier = record.remoteMetadata?[.coreID]
let verifiedGameID = record.remoteMetadata?[.verifiedGameID]
do
{
// SaveState was synced to older version of Delta and lost its coreIdentifier,
// but it remains in the remote metadata so we can reassign it.
self.coreIdentifier = coreIdentifier
}
else
{
switch system
guard let game = self.game else { return }
if let system = System(gameType: game.type), self.coreIdentifier == nil
{
case .ds: self.coreIdentifier = DS.core.identifier // Assume DS save state with nil coreIdentifier is from DeSmuME core.
default: self.coreIdentifier = system.deltaCore.identifier
if let coreIdentifier = record.remoteMetadata?[.coreID]
{
// SaveState was synced to older version of Delta and lost its coreIdentifier,
// but it remains in the remote metadata so we can reassign it.
self.coreIdentifier = coreIdentifier
}
else
{
switch system
{
case .ds: self.coreIdentifier = DS.core.identifier // Assume DS save state with nil coreIdentifier is from DeSmuME core.
default: self.coreIdentifier = system.deltaCore.identifier
}
}
}
if let verifiedGameID, verifiedGameID != game.identifier
{
// Game does not match verified game ID, which most likely means
// this SaveState was reviewed + fixed on another device, but not uploaded.
throw SyncValidationError.incorrectGame(game.name)
}
}
catch let error as SyncValidationError
{
guard let verifiedGameID, SyncManager.shared.ignoredCorruptedRecordIDs.contains(record.recordID) else { throw error }
let fetchRequest = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), verifiedGameID)
if let correctGame = try self.managedObjectContext?.fetch(fetchRequest).first
{
self.game = correctGame
}
else
{
throw ValidationError.nilRelationshipObjects(keys: [#keyPath(GameSave.game)])
}
}
}

View File

@ -9,8 +9,14 @@
import UIKit
import OSLog
import Harmony
import Roxas
extension RecordFlags
{
static let isGameRelationshipVerified = RecordFlags(rawValue: 1 << 0)
}
class ReviewSaveStatesViewController: UITableViewController
{
var completionHandler: (() -> Void)?
@ -186,6 +192,28 @@ private extension ReviewSaveStatesViewController
{
try self.managedObjectContext.save()
if let saveStates = self.saveStatesDataSource.fetchedResultsController.fetchedObjects, let coordinator = SyncManager.shared.coordinator
{
let records = try coordinator.recordController.fetchRecords(for: saveStates)
if let context = records.first?.recordedObject?.managedObjectContext
{
try context.performAndWait {
for record in records
{
record.perform { managedRecord in
managedRecord.flags.insert(.isGameRelationshipVerified)
managedRecord.setNeedsMetadataUpdate()
let saveState = record.recordedObject
Logger.database.notice("Flagged SaveState “\(saveState?.localizedName ?? record.recordID.identifier, privacy: .public)” for metadata update.")
}
}
try context.save()
}
}
}
DispatchQueue.main.async {
self.completionHandler?()
}

View File

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

View File

@ -258,7 +258,7 @@ private extension RecordVersionsViewController
{
DispatchQueue.main.async {
GameSave.ignoredCorruptedIDs.remove(self.record.recordID.identifier)
SyncManager.shared.ignoredCorruptedRecordIDs.remove(self.record.recordID)
CATransaction.begin()
@ -295,11 +295,11 @@ private extension RecordVersionsViewController
// Only allow restoring corrupted records with incorrect games.
guard case .incorrectGame = error else { fallthrough }
let message = NSLocalizedString("Would you like to download this version anyway?", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("Record Version Corrupted", comment: ""), message: message, preferredStyle: .alert)
let message = NSLocalizedString("Would you like to download this version anyway? Delta will try to fix the corruption if possible.", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("Version Corrupted", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Download Anyway", comment: ""), style: .destructive) { _ in
GameSave.ignoredCorruptedIDs.insert(record.recordID.identifier)
SyncManager.shared.ignoredCorruptedRecordIDs.insert(record.recordID)
self.restoreVersion()
})
self.present(alertController, animated: true, completion: nil)

View File

@ -95,6 +95,9 @@ final class SyncManager
return self.coordinator?.recordController
}
// Hacky, but YOLO I'm under time crunch.
public var ignoredCorruptedRecordIDs = Set<RecordID>()
private(set) var syncProgress: Progress?
private(set) var previousSyncResult: SyncResult?

2
External/Harmony vendored

@ -1 +1 @@
Subproject commit a30b440dfedb5fd109fc2b40064035cbcfdd4cce
Subproject commit 07883cf4d90f4fc841a7162f348ea3b1b438fa50

File diff suppressed because it is too large Load Diff