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 extension GameSave: Syncable
{ {
public static var syncablePrimaryKey: AnyKeyPath { public static var syncablePrimaryKey: AnyKeyPath {
@ -104,7 +98,7 @@ extension GameSave: Syncable
} }
catch let error as SyncValidationError 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() let fetchRequest = Game.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), self.identifier) fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), self.identifier)

View File

@ -134,7 +134,7 @@ extension SaveState: Syncable
public var syncableMetadata: [HarmonyMetadataKey : String] { public var syncableMetadata: [HarmonyMetadataKey : String] {
guard let game = self.game else { return [:] } guard let game = self.game else { return [:] }
return [.gameID: game.identifier, .gameName: game.name, .coreID: self.coreIdentifier].compactMapValues { $0 } return [.gameID: game.identifier, .gameName: game.name, .coreID: self.coreIdentifier, .verifiedGameID: game.identifier].compactMapValues { $0 }
} }
public var syncableLocalizedName: String? { public var syncableLocalizedName: String? {
@ -143,9 +143,14 @@ extension SaveState: Syncable
public func awakeFromSync(_ record: AnyRecord) public func awakeFromSync(_ record: AnyRecord)
{ {
guard self.coreIdentifier == nil else { return } let verifiedGameID = record.remoteMetadata?[.verifiedGameID]
guard let game = self.game, let system = System(gameType: game.type) else { return }
do
{
guard let game = self.game else { return }
if let system = System(gameType: game.type), self.coreIdentifier == nil
{
if let coreIdentifier = record.remoteMetadata?[.coreID] if let coreIdentifier = record.remoteMetadata?[.coreID]
{ {
// SaveState was synced to older version of Delta and lost its coreIdentifier, // SaveState was synced to older version of Delta and lost its coreIdentifier,
@ -161,4 +166,29 @@ extension SaveState: Syncable
} }
} }
} }
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 UIKit
import OSLog import OSLog
import Harmony
import Roxas import Roxas
extension RecordFlags
{
static let isGameRelationshipVerified = RecordFlags(rawValue: 1 << 0)
}
class ReviewSaveStatesViewController: UITableViewController class ReviewSaveStatesViewController: UITableViewController
{ {
var completionHandler: (() -> Void)? var completionHandler: (() -> Void)?
@ -186,6 +192,28 @@ private extension ReviewSaveStatesViewController
{ {
try self.managedObjectContext.save() 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 { DispatchQueue.main.async {
self.completionHandler?() self.completionHandler?()
} }

View File

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

View File

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

View File

@ -95,6 +95,9 @@ final class SyncManager
return self.coordinator?.recordController 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 syncProgress: Progress?
private(set) var previousSyncResult: SyncResult? 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