crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 8816715185f936efd8cb5182b242203d5be38854
parent 637108301883a3b8f6b6ce04d2f390bdb2b73562
Author: Michael Camilleri <[email protected]>
Date:   Wed, 15 Apr 2026 01:00:35 +0900

Support repairing of broken games

Diffstat:
MCrossmate/Persistence/GameStore.swift | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
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 {