commit 8816715185f936efd8cb5182b242203d5be38854
parent 637108301883a3b8f6b6ce04d2f390bdb2b73562
Author: Michael Camilleri <[email protected]>
Date: Wed, 15 Apr 2026 01:00:35 +0900
Support repairing of broken games
Diffstat:
1 file changed, 52 insertions(+), 0 deletions(-)
diff --git a/Crossmate/Persistence/GameStore.swift b/Crossmate/Persistence/GameStore.swift
@@ -51,6 +51,8 @@ final class GameStore {
/// Fetches all games, sorted: incomplete first (by `updatedAt` DESC),
/// then completed (by `completedAt` DESC).
func listGames() throws -> [GameSummary] {
+ repairSyncedGamesIfNeeded()
+
let request = NSFetchRequest<GameEntity>(entityName: "GameEntity")
request.sortDescriptors = [NSSortDescriptor(key: "updatedAt", ascending: false)]
let entities = try context.fetch(request)
@@ -310,6 +312,56 @@ final class GameStore {
return (entity, puzzle)
}
+ // MARK: - Repair for sync-applied games
+
+ /// Fixes up `GameEntity` rows that were materialized by the old sync
+ /// apply path, which failed to populate `id`, `createdAt`, `updatedAt`,
+ /// and child cells. Runs inline on any code path that lists or loads
+ /// games; the predicate targets only broken rows so the steady-state
+ /// cost is a single count query.
+ private func repairSyncedGamesIfNeeded() {
+ let request = NSFetchRequest<GameEntity>(entityName: "GameEntity")
+ request.predicate = NSPredicate(
+ format: "ckRecordName != nil AND (id == nil OR cells.@count == 0)"
+ )
+ guard let broken = try? context.fetch(request), !broken.isEmpty else { return }
+
+ for entity in broken {
+ if entity.id == nil,
+ let recordName = entity.ckRecordName,
+ recordName.hasPrefix("game-") {
+ let uuidString = String(recordName.dropFirst("game-".count))
+ entity.id = UUID(uuidString: uuidString)
+ }
+
+ let now = Date()
+ if entity.createdAt == nil { entity.createdAt = now }
+ if entity.updatedAt == nil { entity.updatedAt = now }
+
+ let existingCellCount = (entity.cells as? Set<CellEntity>)?.count ?? 0
+ if existingCellCount == 0,
+ let source = entity.puzzleSource,
+ let gameID = entity.id,
+ let xd = try? XD.parse(source) {
+ let puzzle = Puzzle(xd: xd)
+ for row in puzzle.cells {
+ for cell in row where !cell.isBlock {
+ let cellEntity = CellEntity(context: context)
+ cellEntity.row = Int16(cell.row)
+ cellEntity.col = Int16(cell.col)
+ cellEntity.letter = ""
+ cellEntity.markKind = 0
+ cellEntity.checkedWrong = false
+ cellEntity.ckRecordName = "cell-\(gameID.uuidString)-\(cell.row)-\(cell.col)"
+ cellEntity.game = entity
+ }
+ }
+ }
+ }
+
+ try? context.save()
+ }
+
private func restore(game: Game, from entity: GameEntity) {
let cellEntities = (entity.cells as? Set<CellEntity>) ?? []
for cellEntity in cellEntities {