PuzzleCatalog.swift (2086B)
1 import Foundation 2 3 /// Knows about packaged `.xd` puzzle resources. Used by the new-game picker 4 /// to list available puzzles. 5 struct PuzzleCatalog { 6 struct Entry: Identifiable { 7 let id: String // resource name (e.g. "sample") 8 let title: String // parsed from the XD metadata 9 let source: String // raw XD text 10 let cmVersion: Int // Crossmate version parser version 11 } 12 13 static func bundledPuzzles() -> [Entry] { 14 puzzles(in: "Puzzles/Bundled") 15 } 16 17 static func debugPuzzles() -> [Entry] { 18 puzzles(in: "Puzzles/Debug") 19 } 20 21 static func source( 22 matchingResourceID resourceID: String?, 23 title: String? 24 ) -> Entry? { 25 let entries = bundledPuzzles() + debugPuzzles() 26 27 if let resourceID { 28 return entries.first { $0.id == resourceID } 29 } else if let title { 30 return entries.first { $0.title == title } 31 } else { 32 return nil 33 } 34 } 35 36 static func resourceID(matching source: String) -> String? { 37 (bundledPuzzles() + debugPuzzles()).first { $0.source == source }?.id 38 } 39 40 private static func puzzles(in subdirectory: String) -> [Entry] { 41 guard let directoryURL = Bundle.main.resourceURL? 42 .appendingPathComponent(subdirectory, isDirectory: true) else { 43 return [] 44 } 45 let urls = (try? FileManager.default.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: nil)) ?? [] 46 47 return urls.compactMap { url in 48 guard url.pathExtension == "xd" else { 49 return nil 50 } 51 let name = url.deletingPathExtension().lastPathComponent 52 guard let source = try? String(contentsOf: url, encoding: .utf8), 53 let xd = try? XD.parse(source) else { 54 return nil 55 } 56 return Entry(id: name, title: xd.title ?? name, source: source, cmVersion: xd.cmVersion) 57 } 58 .sorted { $0.id.localizedStandardCompare($1.id) == .orderedAscending } 59 } 60 }