// // SourceLibrary.swift // World Manager for Minecraft // // Created by John Burwell on 2026-05-25. // import Combine import Foundation import OSLog struct SidebarFooterState { enum Style { case idle case inProgress case failure case success } let style: Style let title: String let subtitle: String? let detail: String? let revealURL: URL? } struct ConnectedDeviceSidebarEntry: Identifiable, Hashable { let device: ConnectedDevice let containers: [DeviceAppContainer] let matchedSourceID: URL? let discoveryErrorDescription: String? var id: String { device.id } var minecraftContainer: DeviceAppContainer? { containers.first(where: { $0.appID == "com.mojang.minecraftpe" }) ?? containers.first(where: { $0.minecraftFolderRelativePath != nil }) } var hasMinecraftContainer: Bool { minecraftContainer != nil } } private struct CachedConnectedDeviceDiscovery { let device: ConnectedDevice let containers: [DeviceAppContainer] let discoveryErrorDescription: String? let refreshedAt: Date } @MainActor final class SourceLibrary: ObservableObject { private static let enrichmentWorkerCount = 4 private static let sizeWorkerCount = 2 private static let minimumVisibleScanDuration: TimeInterval = 0.8 private static let connectedDeviceRefreshInterval: TimeInterval = 2.0 private static let connectedDeviceRefreshIntervalWhileScanning: TimeInterval = 5.0 private static let usbConnectedDeviceDiscoveryCacheTTL: TimeInterval = 60.0 private static let networkConnectedDeviceDiscoveryCacheTTL: TimeInterval = 180.0 private static let performanceLogger = Logger( subsystem: Bundle.main.bundleIdentifier ?? "WorldManagerForMinecraft", category: "ConnectedDevicePerformance" ) @Published var sources: [MinecraftSource] = [] @Published private(set) var connectedDevices: [ConnectedDeviceSidebarEntry] = [] @Published private(set) var sidebarFooterState = SidebarFooterState( style: .idle, title: "", subtitle: nil, detail: nil, revealURL: nil ) @Published private(set) var isRestoringPersistedSources = true private var scanTasks: [URL: Task] = [:] private var connectedDeviceRefreshTask: Task? private var footerResetTask: Task? private let persistenceStore: SourcePersistenceStore private let sourceAccessMethod: SourceAccessMethod private let connectedDeviceAccessMethod: ConnectedDeviceSourceAccessMethod? private let notificationService: ScanNotificationServicing private let connectedDeviceSourceFactory = ConnectedDeviceSourceFactory() private var lastMatchedConnectedSourceIDs: Set = [] private var cachedDeviceDiscoveryByUDID: [String: CachedConnectedDeviceDiscovery] = [:] private var isShuttingDown = false init( persistenceStore: SourcePersistenceStore = .shared, sourceAccessMethod: SourceAccessMethod = LocalFolderSourceAccess(), connectedDeviceAccessMethod: ConnectedDeviceSourceAccessMethod? = nil, notificationService: ScanNotificationServicing? = nil ) { self.persistenceStore = persistenceStore self.sourceAccessMethod = sourceAccessMethod self.connectedDeviceAccessMethod = connectedDeviceAccessMethod self.notificationService = notificationService ?? ScanNotificationService.shared Task { [weak self] in await self?.restorePersistedSources() } if connectedDeviceAccessMethod != nil { connectedDeviceRefreshTask = Task { [weak self] in await self?.runConnectedDeviceRefreshLoop() } } } deinit { connectedDeviceRefreshTask?.cancel() footerResetTask?.cancel() scanTasks.values.forEach { $0.cancel() } } var visibleSources: [MinecraftSource] { sources } var sidebarSources: [MinecraftSource] { visibleSources } func shutdown() { guard !isShuttingDown else { return } isShuttingDown = true connectedDeviceRefreshTask?.cancel() connectedDeviceRefreshTask = nil footerResetTask?.cancel() footerResetTask = nil for task in scanTasks.values { task.cancel() } scanTasks.removeAll() } func shutdownGracefully(timeout: TimeInterval = 1.0) async { guard !isShuttingDown else { return } shutdown() try? await Task.sleep(for: .seconds(timeout)) } func addSource(at url: URL) -> URL { let normalizedURL = url.standardizedFileURL let bookmarkData = securityScopedBookmarkData(for: normalizedURL) if sources.contains(where: { $0.id == normalizedURL }) { updateSource(normalizedURL) { source in if source.bookmarkData == nil { source.bookmarkData = bookmarkData } source.accessDescriptor = sourceAccessMethod.accessDescriptor(for: source) } startScan(for: normalizedURL) return normalizedURL } let source = MinecraftSource( folderURL: normalizedURL, bookmarkData: bookmarkData, accessDescriptor: SourceAccessDescriptor( accessorIdentifier: LocalFolderSourceAccess().accessorIdentifier, kind: .localFolder, capabilities: .localFolder, refreshStrategy: .eagerFullScan ) ) return addSource(source, shouldPersist: true, shouldScan: true) } @discardableResult func addSource(_ source: MinecraftSource, shouldPersist: Bool = false, shouldScan: Bool = true) -> URL { if sources.contains(where: { $0.id == source.id }) { updateSource(source.id) { existingSource in existingSource.origin = source.origin existingSource.accessDescriptor = source.accessDescriptor existingSource.availability = source.availability if existingSource.bookmarkData == nil { existingSource.bookmarkData = source.bookmarkData } if existingSource.displayName.isEmpty { existingSource.displayName = source.displayName } } } else { var resolvedSource = source resolvedSource.accessDescriptor = sourceAccessMethod.accessDescriptor(for: resolvedSource) sources.append(resolvedSource) sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending } } if shouldPersist { persistSourceIfAvailable(withID: source.id) } if shouldScan { startScan(for: source.id) } return source.id } func source(withID sourceID: URL) -> MinecraftSource? { sources.first(where: { $0.id == sourceID }) } func rescanSource(withID sourceID: URL) { startScan(for: sourceID) } func listContents(for item: MinecraftContentItem, in source: MinecraftSource) async throws -> [DirectoryPreviewEntry] { try await sourceAccessMethod.listItemContents(for: item, in: source) } func materializeItem(_ item: MinecraftContentItem, in source: MinecraftSource) async throws -> URL { try await sourceAccessMethod.materializeItem(for: item, in: source) } func removeSource(withID sourceID: URL) { let removedSource = source(withID: sourceID) scanTasks[sourceID]?.cancel() scanTasks[sourceID] = nil sources.removeAll { $0.id == sourceID } deletePersistedSource(withID: sourceID) if let removedSource { purgeCachedArtifacts(for: removedSource) } refreshSidebarFooterState() } func setItemActionInProgress(_ description: String) { cancelFooterReset() sidebarFooterState = SidebarFooterState( style: .inProgress, title: description, subtitle: nil, detail: nil, revealURL: nil ) } func setItemActionFailure(_ message: String) { sidebarFooterState = SidebarFooterState( style: .failure, title: "Action Failed", subtitle: message, detail: nil, revealURL: nil ) scheduleFooterReset() } func setItemActionSuccess(title: String, subtitle: String, revealURL: URL?) { sidebarFooterState = SidebarFooterState( style: .success, title: title, subtitle: subtitle, detail: nil, revealURL: revealURL ) scheduleFooterReset() } var activeScanSummary: String? { let scanningSources = sources.filter(\.isScanning) guard !scanningSources.isEmpty else { return nil } if scanningSources.count == 1, let source = scanningSources.first { return "\(source.displayName): \(source.scanStatus)" } return "Scanning \(scanningSources.count) sources..." } private func startScan(for sourceID: URL) { guard !isShuttingDown else { return } scanTasks[sourceID]?.cancel() let task = Task { [weak self] in guard let self else { return } await self.scanSource(withID: sourceID) } scanTasks[sourceID] = task } private func scanSource(withID sourceID: URL) async { var workerTasks: [Task] = [] var previewWorkerTasks: [Task] = [] var sizeWorkerTasks: [Task] = [] let scanStartTime = Date() defer { workerTasks.forEach { $0.cancel() } previewWorkerTasks.forEach { $0.cancel() } sizeWorkerTasks.forEach { $0.cancel() } scanTasks[sourceID] = nil } guard let source = source(withID: sourceID) else { return } let previousSource = source let performanceContext = performanceContext(for: source) updateSource(sourceID) { source in source.isScanning = true source.scanError = nil source.scanDiagnostic = nil source.scanStatus = initialScanStatus(for: source) source.scanProgress = nil source.indexedItemCount = 0 source.indexedDetailCount = 0 } refreshSidebarFooterState() updateSource(sourceID) { source in source.accessDescriptor = sourceAccessMethod.accessDescriptor(for: source) } let currentAvailability = await sourceAccessMethod.availability(for: source) updateSource(sourceID) { source in source.availability = currentAvailability } let scanContextURL = source.folderURL await WorldScanner.beginScanSession(for: scanContextURL) defer { Task.detached(priority: .utility) { await WorldScanner.endScanSession(for: scanContextURL) } } updateSource(sourceID) { source in source.availability = .available source.scanStatus = scanningLibraryStatus(for: source) } refreshSidebarFooterState() do { let index = SourceIndexActor(sourceID: sourceID, folderURL: scanContextURL) let enrichmentQueue = EnrichmentWorkQueue() let previewQueue = EnrichmentWorkQueue() let sizeQueue = EnrichmentWorkQueue() let enrichmentWorkerCount = source.origin.kind == .connectedDevice ? 1 : Self.enrichmentWorkerCount let previewWorkerCount = source.origin.kind == .connectedDevice ? 1 : 1 let sizeWorkerCount = source.origin.kind == .connectedDevice ? 1 : Self.sizeWorkerCount workerTasks = (0.. { continuation in let accessMethod = sourceAccessMethod let discoveryTask = Task.detached(priority: .userInitiated) { do { _ = try await accessMethod.discoverItems(for: source) { item in continuation.yield(item) } continuation.finish() } catch { continuation.finish(throwing: error) } } continuation.onTermination = { @Sendable _ in discoveryTask.cancel() } } var discoveredCount = 0 let discoveryStartTime = Date() for try await item in discoveryStream { guard !Task.isCancelled else { break } discoveredCount += 1 if let snapshot = await index.addDiscoveredItem( item, discoveredCount: discoveredCount ) { applySnapshot(snapshot, to: sourceID) } refreshSidebarFooterState() await enrichmentQueue.enqueue(item) } logScanStage( "Discovery", elapsed: Date().timeIntervalSince(discoveryStartTime), context: performanceContext, itemCount: discoveredCount ) if let snapshot = await index.markDiscoveryFinished() { applySnapshot(snapshot, to: sourceID) } refreshSidebarFooterState() await enrichmentQueue.finish() let enrichmentStartTime = Date() for workerTask in workerTasks { await workerTask.value } logScanStage( "Enrichment", elapsed: Date().timeIntervalSince(enrichmentStartTime), context: performanceContext, itemCount: discoveredCount ) if let snapshot = await index.markMetadataFinished() { applySnapshot(snapshot, to: sourceID) } refreshSidebarFooterState() await previewQueue.finish() let previewStageStartTime = Date() for previewWorkerTask in previewWorkerTasks { await previewWorkerTask.value } logScanStage( "Previews", elapsed: Date().timeIntervalSince(previewStageStartTime), context: performanceContext, itemCount: discoveredCount ) if let snapshot = await index.markPreviewsFinished() { applySnapshot(snapshot, to: sourceID) } refreshSidebarFooterState() await sizeQueue.finish() let sizeStageStartTime = Date() for sizeWorkerTask in sizeWorkerTasks { await sizeWorkerTask.value } logScanStage( "Size", elapsed: Date().timeIntervalSince(sizeStageStartTime), context: performanceContext, itemCount: discoveredCount ) let elapsedScanTime = Date().timeIntervalSince(scanStartTime) if elapsedScanTime < Self.minimumVisibleScanDuration { try? await Task.sleep( for: .seconds(Self.minimumVisibleScanDuration - elapsedScanTime) ) } if let snapshot = await index.finishScan() { applySnapshot(snapshot, to: sourceID) } updateSource(sourceID) { source in if source.origin.kind == .localFolder { source.snapshot = buildSnapshot(for: source, scanRootURL: scanContextURL, packMetadataByItemID: [:]) } else { source.snapshot = nil } } persistSourceIfAvailable(withID: sourceID) refreshSidebarFooterState() logScanStage( "Total", elapsed: Date().timeIntervalSince(scanStartTime), context: performanceContext, itemCount: discoveredCount ) if let completedSource = self.source(withID: sourceID) { await notificationService.notifyScanCompleted( for: completedSource, duration: Date().timeIntervalSince(scanStartTime) ) } } catch { updateSource(sourceID) { source in restoreScannedContent(from: previousSource, into: &source) source.availability = Task.isCancelled ? previousSource.availability : availabilityStatus(for: error, defaultingTo: previousSource.availability) source.scanError = Task.isCancelled ? previousSource.scanError : friendlyScanError(for: error, source: source) source.scanDiagnostic = Task.isCancelled ? previousSource.scanDiagnostic : error.localizedDescription source.scanStatus = previousSource.scanStatus source.scanProgress = previousSource.scanProgress source.isScanning = false } persistSourceIfAvailable(withID: sourceID) refreshSidebarFooterState() } } private func handleEnrichedItem(_ enrichedItem: MinecraftContentItem, for sourceID: URL) { var previousItem: MinecraftContentItem? updateSource(sourceID) { source in guard let index = source.rawItems.firstIndex(where: { $0.id == enrichedItem.id }) else { return } previousItem = source.rawItems[index] source.rawItems[index] = enrichedItem source.indexedDetailCount += 1 if source.indexedDetailCount < source.indexedItemCount { source.scanStatus = "Loaded details for \(source.indexedDetailCount) of \(source.indexedItemCount) items..." } handleMetadataUpdate( for: enrichedItem, previousItem: previousItem, in: &source, sourceID: sourceID ) } refreshSidebarFooterState() } private func handleSizedItem(_ sizedItem: MinecraftContentItem, for sourceID: URL) { updateSource(sourceID) { source in guard let index = source.rawItems.firstIndex(where: { $0.id == sizedItem.id }) else { return } source.rawItems[index].sizeBytes = sizedItem.sizeBytes source.rawItems[index].sizeLoaded = sizedItem.sizeLoaded if source.isScanning { source.scanStatus = "Calculating sizes for \(source.rawItems.filter(\.sizeLoaded).count) of \(source.indexedItemCount) items..." } } refreshSidebarFooterState() } private func rebuildNormalizedIndex(for sourceID: URL) { updateSource(sourceID) { source in let rawItems = source.rawItems.sorted(by: WorldScanner.sortItems) source.rawItems = rawItems let rawItemsByID = Dictionary(uniqueKeysWithValues: rawItems.map { ($0.id, $0) }) let rawPacks = rawItems.filter { $0.contentType == .behaviorPack || $0.contentType == .resourcePack } let rawWorlds = rawItems.filter { $0.contentType == .world } let packMetadataByItemID = Dictionary(uniqueKeysWithValues: rawPacks.map { item in (item.id, packMetadata(for: item, sourceRootURL: source.folderURL)) }) var chosenRepresentativeByIdentity: [PackIdentity: MinecraftContentItem] = [:] var allPackItemsByIdentity: [PackIdentity: [MinecraftContentItem]] = [:] for item in rawPacks { let metadata = packMetadataByItemID[item.id] ?? packMetadata(for: item, sourceRootURL: source.folderURL) let identity = metadata.identity allPackItemsByIdentity[identity, default: []].append(item) guard let existing = chosenRepresentativeByIdentity[identity] else { chosenRepresentativeByIdentity[identity] = item continue } if shouldPreferPackItem(item, over: existing) { chosenRepresentativeByIdentity[identity] = item } } let logicalPacks = allPackItemsByIdentity.keys.sorted { let lhs = chosenRepresentativeByIdentity[$0]?.displayName ?? "" let rhs = chosenRepresentativeByIdentity[$1]?.displayName ?? "" let nameOrder = lhs.localizedStandardCompare(rhs) if nameOrder != .orderedSame { return nameOrder == .orderedAscending } return $0.id.localizedStandardCompare($1.id) == .orderedAscending }.compactMap { identity -> LogicalPack? in guard let representativeItem = chosenRepresentativeByIdentity[identity], let instances = allPackItemsByIdentity[identity] else { return nil } let metadata = packMetadataByItemID[representativeItem.id] return LogicalPack( id: identity, contentType: identity.type, displayName: representativeItem.displayName, uuid: metadata?.uuid, version: metadata?.version, representativeItemID: representativeItem.id, instanceItemIDs: instances.map(\.id).sorted { $0.path.localizedStandardCompare($1.path) == .orderedAscending }, isSuspicious: identity.isSuspicious ) } var packInstances: [PackInstance] = [] for logicalPack in logicalPacks { for itemID in logicalPack.instanceItemIDs { guard let item = rawItemsByID[itemID] else { continue } packInstances.append( PackInstance( id: item.id, itemID: item.id, sourceID: sourceID, logicalPackID: logicalPack.id, origin: packOrigin(for: item), hostWorldItemID: hostWorldItemID(for: item, in: rawWorlds) ) ) } } let logicalPacksByID = Dictionary(uniqueKeysWithValues: logicalPacks.map { ($0.id, $0) }) var worldRelationships: [WorldPackRelationship] = [] var logicalWorlds: [LogicalWorld] = [] for world in rawWorlds { var usedPackIDs = Set() var unresolvedReferences: [ContentPackReference] = [] for reference in world.packReferences { let referenceIdentity = PackIdentity( type: reference.type, uuid: reference.uuid, version: reference.version, fallbackName: reference.name, fallbackLocationHint: world.folderName ) let resolvedID = logicalPacksByID[referenceIdentity]?.id if let resolvedID { usedPackIDs.insert(resolvedID) } else { unresolvedReferences.append(reference) } worldRelationships.append( WorldPackRelationship( worldItemID: world.id, logicalPackID: resolvedID, reference: reference ) ) } logicalWorlds.append( LogicalWorld( id: world.id, itemID: world.id, usedPackIDs: usedPackIDs.sorted { $0.id.localizedStandardCompare($1.id) == .orderedAscending }, unresolvedReferences: unresolvedReferences ) ) } source.logicalPacks = logicalPacks source.logicalWorlds = logicalWorlds.sorted { guard let lhs = source.rawItem(withID: $0.itemID), let rhs = source.rawItem(withID: $1.itemID) else { return $0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending } return WorldScanner.sortItems(lhs, rhs) } source.packInstances = packInstances.sorted { $0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending } source.worldPackRelationships = worldRelationships source.displayItems = buildDisplayItems( from: rawItems, logicalPacks: logicalPacks, rawItemsByID: rawItemsByID ) } } private func restoreScannedContent(from previousSource: MinecraftSource, into source: inout MinecraftSource) { source.displayItems = previousSource.displayItems source.rawItems = previousSource.rawItems source.logicalPacks = previousSource.logicalPacks source.logicalWorlds = previousSource.logicalWorlds source.packInstances = previousSource.packInstances source.worldPackRelationships = previousSource.worldPackRelationships source.snapshot = previousSource.snapshot source.indexedItemCount = previousSource.indexedItemCount source.indexedDetailCount = previousSource.indexedDetailCount source.scanProgress = previousSource.scanProgress source.lastScanDate = previousSource.lastScanDate } private func performanceContext(for source: MinecraftSource) -> String { switch source.origin { case .localFolder: return "source=\(source.displayName) kind=local" case .connectedDevice(let device, let container): let transport = device.connection == .usb ? "usb" : "network" return "source=\(source.displayName) kind=connected-device transport=\(transport) udid=\(device.udid) app=\(container.appID)" } } private func logScanStage( _ stage: String, elapsed: TimeInterval, context: String, itemCount: Int ) { Self.performanceLogger.log( "\(stage, privacy: .public) \(context, privacy: .public) elapsed=\(elapsed, format: .fixed(precision: 3))s items=\(itemCount)" ) } private func logDeviceRefreshStage( _ stage: String, elapsed: TimeInterval, device: ConnectedDevice, containerCount: Int, error: Error? = nil ) { let transport = device.connection == .usb ? "usb" : "network" let errorDescription = error?.localizedDescription ?? "" Self.performanceLogger.log( "\(stage, privacy: .public) device=\(device.name, privacy: .public) transport=\(transport, privacy: .public) udid=\(device.udid, privacy: .public) elapsed=\(elapsed, format: .fixed(precision: 3))s containers=\(containerCount) error=\(errorDescription, privacy: .public)" ) } private func friendlyScanError(for error: Error, source: MinecraftSource) -> String { let description = error.localizedDescription guard source.origin.kind == .connectedDevice else { return "Failed to scan folder: \(description)" } if description.contains("AMDeviceCreateHouseArrestService returned -402653093") || description.contains("kAMDServiceLimitError") { return "Device is busy. Too many device access sessions were open, so the scan could not start." } if description.localizedCaseInsensitiveContains("InstallationLookupFailed") { return "The device refused access to the Minecraft app container." } if description.localizedCaseInsensitiveContains("not paired") { return "The device is not paired with this Mac." } if description.localizedCaseInsensitiveContains("no longer available") { return "The device disconnected during the scan." } return "Failed to scan device library." } private func handleDiscoveredItem(_ item: MinecraftContentItem, in source: inout MinecraftSource, sourceID: URL) { guard isLogicalPackType(item.contentType) else { return } let identity = packMetadata(for: item, sourceRootURL: source.folderURL).identity refreshLogicalPack(identity: identity, in: &source, sourceID: sourceID) } private func handleMetadataUpdate( for item: MinecraftContentItem, previousItem: MinecraftContentItem?, in source: inout MinecraftSource, sourceID: URL ) { if isLogicalPackType(item.contentType) { let newIdentity = packMetadata(for: item, sourceRootURL: source.folderURL).identity let previousIdentity = previousItem.map { packMetadata(for: $0, sourceRootURL: source.folderURL).identity } if let previousIdentity, previousIdentity != newIdentity { refreshLogicalPack(identity: previousIdentity, in: &source, sourceID: sourceID) } refreshLogicalPack(identity: newIdentity, in: &source, sourceID: sourceID) refreshWorldRelationships(in: &source, filteringTo: item.contentType) return } if item.contentType == .world { refreshWorldRelationship(for: item, in: &source) } } private func updateSource(_ sourceID: URL, mutate: (inout MinecraftSource) -> Void) { guard let index = sources.firstIndex(where: { $0.id == sourceID }) else { return } var source = sources[index] mutate(&source) sources[index] = source } private func applySnapshot(_ snapshot: SourceIndexSnapshot, to sourceID: URL) { updateSource(sourceID) { source in source.displayItems = snapshot.displayItems source.rawItems = snapshot.rawItems source.logicalPacks = snapshot.logicalPacks source.logicalWorlds = snapshot.logicalWorlds source.packInstances = snapshot.packInstances source.worldPackRelationships = snapshot.worldPackRelationships source.indexedItemCount = snapshot.indexedItemCount source.indexedDetailCount = snapshot.indexedDetailCount source.scanStatus = snapshot.scanStatus source.scanProgress = snapshot.scanProgress source.isScanning = snapshot.isScanning source.lastScanDate = snapshot.lastScanDate } } private func refreshLogicalPack(identity: PackIdentity, in source: inout MinecraftSource, sourceID: URL) { let matchingItems = source.rawItems.filter { item in guard isLogicalPackType(item.contentType) else { return false } return packMetadata(for: item, sourceRootURL: source.folderURL).identity == identity } source.logicalPacks.removeAll { $0.id == identity } source.packInstances.removeAll { $0.logicalPackID == identity } guard !matchingItems.isEmpty else { return } let representativeItem = matchingItems.reduce(matchingItems[0]) { current, candidate in shouldPreferPackItem(candidate, over: current) ? candidate : current } let representativeMetadata = packMetadata(for: representativeItem, sourceRootURL: source.folderURL) source.logicalPacks.append( LogicalPack( id: identity, contentType: identity.type, displayName: representativeItem.displayName, uuid: representativeMetadata.uuid, version: representativeMetadata.version, representativeItemID: representativeItem.id, instanceItemIDs: matchingItems.map(\.id).sorted { $0.path.localizedStandardCompare($1.path) == .orderedAscending }, isSuspicious: identity.isSuspicious ) ) source.logicalPacks.sort { let nameOrder = $0.displayName.localizedStandardCompare($1.displayName) if nameOrder != .orderedSame { return nameOrder == .orderedAscending } return $0.id.id.localizedStandardCompare($1.id.id) == .orderedAscending } let rawWorlds = source.rawItems.filter { $0.contentType == .world } source.packInstances.append( contentsOf: matchingItems.map { item in PackInstance( id: item.id, itemID: item.id, sourceID: sourceID, logicalPackID: identity, origin: packOrigin(for: item), hostWorldItemID: hostWorldItemID(for: item, in: rawWorlds) ) } ) source.packInstances.sort { $0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending } } private func refreshWorldRelationships(in source: inout MinecraftSource, filteringTo type: MinecraftContentType? = nil) { let worlds = source.rawItems.filter { $0.contentType == .world } for world in worlds { guard type == nil || world.packReferences.contains(where: { $0.type == type }) else { continue } refreshWorldRelationship(for: world, in: &source) } } private func refreshWorldRelationship(for world: MinecraftContentItem, in source: inout MinecraftSource) { source.worldPackRelationships.removeAll { $0.worldItemID == world.id } source.logicalWorlds.removeAll { $0.itemID == world.id } let logicalPacksByID = Dictionary(uniqueKeysWithValues: source.logicalPacks.map { ($0.id, $0) }) var usedPackIDs = Set() var unresolvedReferences: [ContentPackReference] = [] var relationships: [WorldPackRelationship] = [] for reference in world.packReferences { let referenceIdentity = PackIdentity( type: reference.type, uuid: reference.uuid, version: reference.version, fallbackName: reference.name, fallbackLocationHint: world.folderName ) let resolvedID = logicalPacksByID[referenceIdentity]?.id if let resolvedID { usedPackIDs.insert(resolvedID) } else { unresolvedReferences.append(reference) } relationships.append( WorldPackRelationship( worldItemID: world.id, logicalPackID: resolvedID, reference: reference ) ) } source.worldPackRelationships.append(contentsOf: relationships) source.logicalWorlds.append( LogicalWorld( id: world.id, itemID: world.id, usedPackIDs: usedPackIDs.sorted { $0.id.localizedStandardCompare($1.id) == .orderedAscending }, unresolvedReferences: unresolvedReferences ) ) let rawItemsByID = Dictionary(uniqueKeysWithValues: source.rawItems.map { ($0.id, $0) }) source.logicalWorlds.sort { guard let lhs = rawItemsByID[$0.itemID], let rhs = rawItemsByID[$1.itemID] else { return $0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending } return WorldScanner.sortItems(lhs, rhs) } } private func runConnectedDeviceRefreshLoop() async { while !Task.isCancelled && !isShuttingDown { await refreshConnectedDevices() do { let refreshInterval = sources.contains { $0.isScanning && $0.origin.kind == .connectedDevice } ? Self.connectedDeviceRefreshIntervalWhileScanning : Self.connectedDeviceRefreshInterval try await Task.sleep(for: .seconds(refreshInterval)) } catch { return } } } private func refreshConnectedDevices() async { guard !isShuttingDown else { return } guard let connectedDeviceAccessMethod else { return } let devices: [ConnectedDevice] do { devices = try await connectedDeviceAccessMethod.listConnectedDevices() } catch { markAllConnectedDeviceSourcesDisconnected() connectedDevices = [] lastMatchedConnectedSourceIDs = [] return } var entries: [ConnectedDeviceSidebarEntry] = [] var matchedSourceIDs = Set() let activeScanningDeviceUDIDs = Set( sources.compactMap { source -> String? in guard source.isScanning, case .connectedDevice(let device, _) = source.origin else { return nil } return device.udid } ) let currentDeviceUDIDs = Set(devices.map(\.udid)) cachedDeviceDiscoveryByUDID = cachedDeviceDiscoveryByUDID.filter { currentDeviceUDIDs.contains($0.key) } for device in devices { if let matchedSourceID = knownConnectedDeviceSourceID(for: device) { matchedSourceIDs.insert(matchedSourceID) let cachedContainers = cachedDeviceDiscoveryByUDID[device.udid]?.containers ?? [] refreshMatchedConnectedDeviceSource( sourceID: matchedSourceID, device: device, containers: cachedContainers ) continue } let containers: [DeviceAppContainer] let discoveryErrorDescription: String? if let cachedDiscovery = cachedDiscovery(for: device, isActivelyScanning: activeScanningDeviceUDIDs.contains(device.udid)) { containers = cachedDiscovery.containers discoveryErrorDescription = cachedDiscovery.discoveryErrorDescription } else { let containerDiscoveryStartTime = Date() do { containers = try await connectedDeviceAccessMethod.listAccessibleContainers(for: device) discoveryErrorDescription = nil cacheDeviceDiscovery( device: device, containers: containers, discoveryErrorDescription: nil ) logDeviceRefreshStage( "Container discovery", elapsed: Date().timeIntervalSince(containerDiscoveryStartTime), device: device, containerCount: containers.count ) } catch { containers = [] discoveryErrorDescription = error.localizedDescription cacheDeviceDiscovery( device: device, containers: [], discoveryErrorDescription: error.localizedDescription ) logDeviceRefreshStage( "Container discovery failed", elapsed: Date().timeIntervalSince(containerDiscoveryStartTime), device: device, containerCount: 0, error: error ) } } let matchedSourceID = matchingConnectedDeviceSourceID( device: device, containers: containers ) if let matchedSourceID { matchedSourceIDs.insert(matchedSourceID) refreshMatchedConnectedDeviceSource( sourceID: matchedSourceID, device: device, containers: containers ) } let shouldDisplayEntry = matchedSourceID == nil && (!containers.isEmpty || device.trustState != .trusted) if shouldDisplayEntry { entries.append( ConnectedDeviceSidebarEntry( device: device, containers: containers, matchedSourceID: matchedSourceID, discoveryErrorDescription: discoveryErrorDescription ) ) } } markDisconnectedConnectedDeviceSources(excluding: matchedSourceIDs) connectedDevices = entries.sorted { let lhsKnown = $0.matchedSourceID != nil let rhsKnown = $1.matchedSourceID != nil if lhsKnown != rhsKnown { return lhsKnown && !rhsKnown } let lhsMinecraft = $0.hasMinecraftContainer let rhsMinecraft = $1.hasMinecraftContainer if lhsMinecraft != rhsMinecraft { return lhsMinecraft && !rhsMinecraft } return $0.device.name.localizedStandardCompare($1.device.name) == .orderedAscending } lastMatchedConnectedSourceIDs = matchedSourceIDs } private func cachedDiscovery(for device: ConnectedDevice, isActivelyScanning: Bool) -> CachedConnectedDeviceDiscovery? { guard let cachedDiscovery = cachedDeviceDiscoveryByUDID[device.udid] else { return nil } if isActivelyScanning { return cachedDiscovery } let age = Date().timeIntervalSince(cachedDiscovery.refreshedAt) guard age <= discoveryCacheTTL(for: device) else { return nil } guard cachedDiscovery.device.connection == device.connection, cachedDiscovery.device.trustState == device.trustState, cachedDiscovery.device.name == device.name else { return nil } return cachedDiscovery } private func discoveryCacheTTL(for device: ConnectedDevice) -> TimeInterval { switch device.connection { case .usb: return Self.usbConnectedDeviceDiscoveryCacheTTL case .network: return Self.networkConnectedDeviceDiscoveryCacheTTL } } private func cacheDeviceDiscovery( device: ConnectedDevice, containers: [DeviceAppContainer], discoveryErrorDescription: String? ) { cachedDeviceDiscoveryByUDID[device.udid] = CachedConnectedDeviceDiscovery( device: device, containers: containers, discoveryErrorDescription: discoveryErrorDescription, refreshedAt: Date() ) } private func matchingConnectedDeviceSourceID( device: ConnectedDevice, containers: [DeviceAppContainer] ) -> URL? { for container in containers { let sourceID = connectedDeviceSourceFactory.makeSourceIdentifier( device: device, container: container ) if sources.contains(where: { $0.id == sourceID }) { return sourceID } } return nil } private func knownConnectedDeviceSourceID(for device: ConnectedDevice) -> URL? { let matchingSourceIDs = sources.compactMap { source -> URL? in guard case .connectedDevice(let expectedDevice, _) = source.origin else { return nil } return expectedDevice.udid == device.udid ? source.id : nil } guard matchingSourceIDs.count == 1 else { return nil } return matchingSourceIDs.first } private func refreshMatchedConnectedDeviceSource( sourceID: URL, device: ConnectedDevice, containers: [DeviceAppContainer] ) { updateSource(sourceID) { source in guard case .connectedDevice(_, let previousContainer) = source.origin else { return } let resolvedContainer = containers.first(where: { $0.appID == previousContainer.appID && $0.accessMode == previousContainer.accessMode }) ?? previousContainer source.origin = .connectedDevice(device: device, container: resolvedContainer) source.displayName = "\(device.name) • \(resolvedContainer.appName)" source.accessDescriptor = sourceAccessMethod.accessDescriptor(for: source) source.availability = availability(for: device, hasMinecraftContainer: true) } persistSourceIfAvailable(withID: sourceID) } private func markAllConnectedDeviceSourcesDisconnected() { for source in sources where source.origin.kind == .connectedDevice { updateSource(source.id) { source in source.availability = .disconnected } } } private func markDisconnectedConnectedDeviceSources(excluding matchedSourceIDs: Set) { for source in sources where source.origin.kind == .connectedDevice && !matchedSourceIDs.contains(source.id) { updateSource(source.id) { source in source.availability = .disconnected } } } private func availability(for device: ConnectedDevice, hasMinecraftContainer: Bool) -> SourceAvailability { guard hasMinecraftContainer else { return .unavailable } switch device.trustState { case .trusted: return .available case .locked, .untrusted: return .limited case .unavailable: return .disconnected } } private func restorePersistedSources() async { defer { isRestoringPersistedSources = false refreshSidebarFooterState() } let records: [PersistedSourceRecord] do { records = try await persistenceStore.loadSources() } catch { return } for record in records { var source = MinecraftSource( sourceID: record.sourceID, folderURL: record.folderURL, bookmarkData: record.bookmarkData, origin: record.origin, accessDescriptor: record.accessDescriptor, availability: record.availability ) source.displayName = record.displayName source.rawItems = await restoreCachedImages(in: record.rawItems) source.indexedItemCount = record.rawItems.count source.indexedDetailCount = source.rawItems.filter(\.metadataLoaded).count source.lastScanDate = record.lastScanDate source.snapshot = record.snapshot sources.append(source) rebuildNormalizedIndex(for: source.id) updateSource(source.id) { source in source.displayItems = source.displayItems.sorted(by: WorldScanner.sortItems) source.scanStatus = source.indexedItemCount == 0 ? "No Minecraft items found." : "Loaded \(source.indexedDetailCount) items." } } sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending } for record in records { if sourceNeedsRescan(record) { startScan(for: record.sourceID) } } await refreshConnectedDevices() } private func restoreCachedImages(in items: [MinecraftContentItem]) async -> [MinecraftContentItem] { var restoredItems: [MinecraftContentItem] = [] restoredItems.reserveCapacity(items.count) for var item in items { item.iconURL = await ImageCacheStore.shared.cachedImageURL(for: item.iconURL) item.packReferences = await restoreCachedImages(in: item.packReferences) restoredItems.append(item) } return restoredItems } private func restoreCachedImages(in references: [ContentPackReference]) async -> [ContentPackReference] { var restoredReferences: [ContentPackReference] = [] restoredReferences.reserveCapacity(references.count) for reference in references { let cachedIconURL = await ImageCacheStore.shared.cachedImageURL(for: reference.iconURL) restoredReferences.append( ContentPackReference( name: reference.name, type: reference.type, iconURL: cachedIconURL, uuid: reference.uuid, version: reference.version, source: reference.source ) ) } return restoredReferences } private func sourceNeedsRescan(_ record: PersistedSourceRecord) -> Bool { guard record.accessDescriptor.refreshStrategy == .eagerFullScan else { return record.rawItems.isEmpty } guard let snapshot = record.snapshot else { return true } let fileManager = FileManager.default let sourceURL = record.folderURL guard fileManager.fileExists(atPath: sourceURL.path) else { return true } let currentCollections = Dictionary(uniqueKeysWithValues: currentCollectionSnapshots(for: sourceURL).map { ($0.folderName, $0) }) let persistedCollections = Dictionary(uniqueKeysWithValues: snapshot.collectionSnapshots.map { ($0.folderName, $0) }) if currentCollections.count != persistedCollections.count { return true } for (folderName, persistedCollection) in persistedCollections { guard let currentCollection = currentCollections[folderName], currentCollection == persistedCollection else { return true } } for itemSnapshot in snapshot.itemSnapshots { let itemURL = sourceURL.appendingPathComponent(itemSnapshot.relativePath, isDirectory: true) guard fileManager.fileExists(atPath: itemURL.path) else { return true } let modifiedDate = try? itemURL.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate if modifiedDate != itemSnapshot.modifiedDate { return true } } return false } private func currentCollectionSnapshots(for sourceURL: URL) -> [CollectionSnapshot] { let fileManager = FileManager.default return MinecraftContentType.allCases.compactMap { type -> CollectionSnapshot? in let collectionURL = sourceURL.appendingPathComponent(type.collectionFolderName, isDirectory: true) guard fileManager.fileExists(atPath: collectionURL.path) else { return nil } let children = (try? fileManager.contentsOfDirectory( at: collectionURL, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles] )) ?? [] let childDirectoryCount = children.filter { (try? $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true }.count let modifiedDate = try? collectionURL.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate return CollectionSnapshot( folderName: type.collectionFolderName, modifiedDate: modifiedDate, childDirectoryCount: childDirectoryCount, fingerprint: [ type.collectionFolderName, String(childDirectoryCount), modifiedDate?.timeIntervalSince1970.formatted() ?? "nil" ].joined(separator: "::") ) } } private func buildDisplayItems( from rawItems: [MinecraftContentItem], logicalPacks: [LogicalPack], rawItemsByID: [URL: MinecraftContentItem] ) -> [MinecraftContentItem] { var normalizedItemIDs = Set() var normalizedItems: [MinecraftContentItem] = [] for item in rawItems where item.contentType == .world { guard normalizedItemIDs.insert(item.id).inserted else { continue } normalizedItems.append(item) } for logicalPack in logicalPacks { guard let item = rawItemsByID[logicalPack.representativeItemID], normalizedItemIDs.insert(item.id).inserted else { continue } normalizedItems.append(item) } for item in rawItems where item.contentType == .skinPack || item.contentType == .worldTemplate { guard normalizedItemIDs.insert(item.id).inserted else { continue } normalizedItems.append(item) } return normalizedItems } private func persistSourceIfAvailable(withID sourceID: URL) { guard let source = source(withID: sourceID) else { return } let persistedSource = source Task { try? await persistenceStore.save(source: persistedSource) } } private func deletePersistedSource(withID sourceID: URL) { Task { try? await persistenceStore.deleteSource(withID: sourceID) } } private func purgeCachedArtifacts(for source: MinecraftSource) { Task.detached(priority: .utility) { [sourceAccessMethod] in await sourceAccessMethod.purgeCachedArtifacts(for: source) } } private func securityScopedBookmarkData(for url: URL) -> Data? { try? url.bookmarkData( options: [.withSecurityScope], includingResourceValuesForKeys: nil, relativeTo: nil ) } private func isLogicalPackType(_ contentType: MinecraftContentType) -> Bool { contentType == .behaviorPack || contentType == .resourcePack } private func refreshSidebarFooterState() { if isRestoringPersistedSources { cancelFooterReset() sidebarFooterState = SidebarFooterState( style: .inProgress, title: "Restoring library...", subtitle: "Loading saved sources and cached metadata", detail: nil, revealURL: nil ) return } let scanningSources = sources.filter(\.isScanning) if let source = scanningSources.first { cancelFooterReset() let title = source.scanStatus.isEmpty ? "Scanning Minecraft library..." : source.scanStatus let subtitle: String let detail: String? if source.indexedItemCount > 0 { subtitle = source.displayName let previewLoadedCount = source.rawItems.filter(\.previewLoaded).count let sizeLoadedCount = source.rawItems.filter(\.sizeLoaded).count if sizeLoadedCount > 0 || source.scanStatus.contains("Calculating sizes") { detail = "\(sizeLoadedCount) of \(source.indexedItemCount) sizes calculated" } else if previewLoadedCount > 0 || source.scanStatus.contains("Loading previews") { detail = "\(previewLoadedCount) of \(source.indexedItemCount) previews loaded" } else { detail = "\(source.indexedDetailCount) of \(source.indexedItemCount) indexed" } } else { subtitle = "Searching \(source.displayName)" detail = nil } sidebarFooterState = SidebarFooterState( style: .inProgress, title: title, subtitle: subtitle, detail: detail, revealURL: nil ) return } if let source = sources.first(where: { $0.scanError != nil }) { sidebarFooterState = SidebarFooterState( style: .failure, title: "Scan failed", subtitle: source.scanError, detail: nil, revealURL: nil ) scheduleFooterReset() return } cancelFooterReset() sidebarFooterState = SidebarFooterState(style: .idle, title: "", subtitle: nil, detail: nil, revealURL: nil) } private func cancelFooterReset() { footerResetTask?.cancel() footerResetTask = nil } private func initialScanStatus(for source: MinecraftSource) -> String { switch source.origin { case .localFolder: return "Preparing folder scan..." case .connectedDevice: return "Connecting to device and discovering Minecraft items..." } } private func scanningLibraryStatus(for source: MinecraftSource) -> String { switch source.origin { case .localFolder: return "Scanning Minecraft library..." case .connectedDevice: return "Scanning Minecraft library on device..." } } private func scheduleFooterReset(after seconds: Double = 5) { cancelFooterReset() footerResetTask = Task { @MainActor [weak self] in try? await Task.sleep(for: .seconds(seconds)) guard let self, !Task.isCancelled else { return } self.refreshSidebarFooterState() } } private func buildSnapshot( for source: MinecraftSource, scanRootURL: URL, packMetadataByItemID: [URL: PackMetadata] ) -> SourceSnapshot { let collectionSnapshots = MinecraftContentType.allCases.compactMap { type -> CollectionSnapshot? in let collectionURL = scanRootURL.appendingPathComponent(type.collectionFolderName, isDirectory: true) guard FileManager.default.fileExists(atPath: collectionURL.path) else { return nil } let children = (try? FileManager.default.contentsOfDirectory( at: collectionURL, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles] )) ?? [] let childDirectoryCount = children.filter { (try? $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true }.count let modifiedDate = try? collectionURL.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate return CollectionSnapshot( folderName: type.collectionFolderName, modifiedDate: modifiedDate, childDirectoryCount: childDirectoryCount, fingerprint: [ type.collectionFolderName, String(childDirectoryCount), modifiedDate?.timeIntervalSince1970.formatted() ?? "nil" ].joined(separator: "::") ) } let itemSnapshots = source.rawItems.map { item in let relativePath = item.folderURL.path.replacingOccurrences(of: scanRootURL.path + "/", with: "") let metadata = packMetadataByItemID[item.id] return ItemSnapshot( id: item.id, relativePath: relativePath, modifiedDate: item.modifiedDate, sizeBytes: item.sizeBytes, packUUID: metadata?.uuid, packVersion: metadata?.version ) }.sorted { (lhs: ItemSnapshot, rhs: ItemSnapshot) in lhs.relativePath.localizedStandardCompare(rhs.relativePath) == .orderedAscending } let rootModifiedDate = try? scanRootURL .resourceValues(forKeys: [.contentModificationDateKey]) .contentModificationDate return SourceSnapshot( sourceID: source.id, rootModifiedDate: rootModifiedDate, collectionSnapshots: collectionSnapshots, itemSnapshots: itemSnapshots ) } private func availabilityStatus(for error: Error, defaultingTo currentAvailability: SourceAvailability) -> SourceAvailability { if let accessError = error as? SourceAccessError { switch accessError { case .deviceUnavailable: return .disconnected case .deviceNotTrusted: return .limited case .appNotAccessible, .minecraftFolderMissing, .accessFailed: return .unavailable } } return currentAvailability } private func shouldPreferPackItem(_ candidate: MinecraftContentItem, over existing: MinecraftContentItem) -> Bool { let candidateEmbedded = isEmbeddedWorldPack(candidate) let existingEmbedded = isEmbeddedWorldPack(existing) if candidateEmbedded != existingEmbedded { return !candidateEmbedded } if candidate.metadataLoaded != existing.metadataLoaded { return candidate.metadataLoaded } if (candidate.iconURL != nil) != (existing.iconURL != nil) { return candidate.iconURL != nil } if candidate.previewLoaded != existing.previewLoaded { return candidate.previewLoaded } if candidate.modifiedDate != existing.modifiedDate { return (candidate.modifiedDate ?? .distantPast) > (existing.modifiedDate ?? .distantPast) } return candidate.folderURL.path.localizedStandardCompare(existing.folderURL.path) == .orderedAscending } private func packOrigin(for item: MinecraftContentItem) -> PackSource { isEmbeddedWorldPack(item) ? .embeddedInWorld : .foundInCollection } private func isEmbeddedWorldPack(_ item: MinecraftContentItem) -> Bool { item.folderURL.pathComponents.contains(MinecraftContentType.world.collectionFolderName) } private func hostWorldItemID(for packItem: MinecraftContentItem, in rawWorlds: [MinecraftContentItem]) -> URL? { rawWorlds.first(where: { world in packItem.folderURL.path.hasPrefix(world.folderURL.path + "/") })?.id } private func packMetadata(for item: MinecraftContentItem, sourceRootURL: URL) -> PackMetadata { let uuid = item.packUUID let version = item.packVersion return PackMetadata( uuid: uuid, version: version, identity: PackIdentity( type: item.contentType, uuid: uuid, version: version, fallbackName: item.displayName, fallbackLocationHint: relativePathHint(for: item, sourceRootURL: sourceRootURL) ) ) } private func relativePathHint(for item: MinecraftContentItem, sourceRootURL: URL) -> String { item.folderURL.path.replacingOccurrences(of: sourceRootURL.path + "/", with: "") } } private struct PackMetadata { let uuid: String? let version: String? let identity: PackIdentity } private actor EnrichmentWorkQueue { private var pendingItems: [MinecraftContentItem] = [] private var isFinished = false private var waitingContinuations: [CheckedContinuation] = [] func enqueue(_ item: MinecraftContentItem) { if let continuation = waitingContinuations.first { waitingContinuations.removeFirst() continuation.resume(returning: item) return } pendingItems.append(item) } func next() async -> MinecraftContentItem? { if !pendingItems.isEmpty { return pendingItems.removeFirst() } if isFinished { return nil } return await withCheckedContinuation { continuation in waitingContinuations.append(continuation) } } func finish() { isFinished = true for continuation in waitingContinuations { continuation.resume(returning: nil) } waitingContinuations.removeAll() } } private struct SourceIndexSnapshot { let displayItems: [MinecraftContentItem] let rawItems: [MinecraftContentItem] let logicalPacks: [LogicalPack] let logicalWorlds: [LogicalWorld] let packInstances: [PackInstance] let worldPackRelationships: [WorldPackRelationship] let indexedItemCount: Int let indexedDetailCount: Int let scanStatus: String let scanProgress: Double? let isScanning: Bool let lastScanDate: Date? } private actor SourceIndexActor { private let sourceID: URL private let folderURL: URL private let publishInterval: TimeInterval = 0.12 private var orderedItemIDs: [URL] = [] private var itemsByID: [URL: MinecraftContentItem] = [:] private var packMetadataByItemID: [URL: PackMetadata] = [:] private var packIdentityByItemID: [URL: String] = [:] private var packIdentityValueByID: [String: PackIdentity] = [:] private var packItemIDsByIdentityID: [String: Set] = [:] private var packRepresentativeItemIDByIdentityID: [String: URL] = [:] private var indexedItemCount = 0 private var indexedDetailCount = 0 private var previewLoadedCount = 0 private var discoveryFinished = false private var metadataFinished = false private var previewsFinished = false private var sizesFinished = false private var lastPublishedAt: Date? init(sourceID: URL, folderURL: URL) { self.sourceID = sourceID self.folderURL = folderURL } func addDiscoveredItem(_ item: MinecraftContentItem, discoveredCount: Int) -> SourceIndexSnapshot? { orderedItemIDs.append(item.id) itemsByID[item.id] = item indexedItemCount = discoveredCount return snapshotIfNeeded() } func applyEnrichedItem(_ item: MinecraftContentItem) -> SourceIndexSnapshot? { let previous = itemsByID[item.id] itemsByID[item.id] = item if item.metadataLoaded, previous?.metadataLoaded != true { indexedDetailCount += 1 } if isLogicalPackType(item.contentType) { refreshTrackedPackIdentity(for: item, previousItem: previous) } return snapshotIfNeeded() } func applySizedItem(_ item: MinecraftContentItem) -> SourceIndexSnapshot? { guard var current = itemsByID[item.id] else { return nil } current.sizeBytes = item.sizeBytes current.sizeLoaded = item.sizeLoaded itemsByID[item.id] = current return snapshotIfNeeded() } func applyPreviewItem(_ item: MinecraftContentItem) -> SourceIndexSnapshot? { let previous = itemsByID[item.id] itemsByID[item.id] = item if item.previewLoaded, previous?.previewLoaded != true { previewLoadedCount += 1 } return snapshotIfNeeded() } func markDiscoveryFinished() -> SourceIndexSnapshot? { discoveryFinished = true return buildSnapshot(force: true) } func markMetadataFinished() -> SourceIndexSnapshot? { discoveryFinished = true metadataFinished = true return buildSnapshot(force: true) } func markPreviewsFinished() -> SourceIndexSnapshot? { discoveryFinished = true metadataFinished = true previewsFinished = true return buildSnapshot(force: true) } func finishScan() -> SourceIndexSnapshot? { discoveryFinished = true metadataFinished = true previewsFinished = true sizesFinished = true return buildSnapshot(force: true) } private func snapshotIfNeeded() -> SourceIndexSnapshot? { buildSnapshot(force: false) } private func buildSnapshot(force: Bool) -> SourceIndexSnapshot? { let now = Date() if !force, let lastPublishedAt, now.timeIntervalSince(lastPublishedAt) < publishInterval { return nil } lastPublishedAt = now let rawItems = orderedItemIDs.compactMap { itemsByID[$0] } let rawItemsByID = Dictionary(uniqueKeysWithValues: rawItems.map { ($0.id, $0) }) let logicalPacks = buildLogicalPacks(rawItemsByID: rawItemsByID) let dedupedDisplayItems = buildDisplayItems( from: rawItems, logicalPacks: logicalPacks, rawItemsByID: rawItemsByID ) let metadataFraction = progressFraction(completed: indexedDetailCount, total: indexedItemCount) let previewFraction = progressFraction(completed: previewLoadedCount, total: indexedItemCount) let sizeLoadedCount = rawItems.filter(\.sizeLoaded).count let sizeFraction = progressFraction(completed: sizeLoadedCount, total: indexedItemCount) let scanStatus: String if !discoveryFinished { scanStatus = indexedItemCount == 0 ? "Scanning Minecraft library..." : "Found \(indexedItemCount) items. Loading metadata..." return SourceIndexSnapshot( displayItems: dedupedDisplayItems, rawItems: rawItems, logicalPacks: logicalPacks, logicalWorlds: [], packInstances: [], worldPackRelationships: [], indexedItemCount: indexedItemCount, indexedDetailCount: indexedDetailCount, scanStatus: scanStatus, scanProgress: nil, isScanning: true, lastScanDate: nil ) } if !metadataFinished { scanStatus = indexedItemCount == 0 ? "No Minecraft items found." : "Loading metadata for \(indexedDetailCount) of \(indexedItemCount) items..." return SourceIndexSnapshot( displayItems: dedupedDisplayItems, rawItems: rawItems, logicalPacks: logicalPacks, logicalWorlds: [], packInstances: [], worldPackRelationships: [], indexedItemCount: indexedItemCount, indexedDetailCount: indexedDetailCount, scanStatus: scanStatus, scanProgress: progressAfterDiscovery(metadataFraction), isScanning: true, lastScanDate: nil ) } if !previewsFinished { scanStatus = indexedItemCount == 0 ? "No Minecraft items found." : "Loading previews for \(previewLoadedCount) of \(indexedItemCount) items..." return SourceIndexSnapshot( displayItems: dedupedDisplayItems, rawItems: rawItems, logicalPacks: logicalPacks, logicalWorlds: [], packInstances: [], worldPackRelationships: [], indexedItemCount: indexedItemCount, indexedDetailCount: indexedDetailCount, scanStatus: scanStatus, scanProgress: progressAfterMetadata(previewFraction), isScanning: true, lastScanDate: nil ) } let rawWorlds = rawItems.filter { $0.contentType == .world } var packInstances: [PackInstance] = [] for logicalPack in logicalPacks { for itemID in logicalPack.instanceItemIDs { guard let item = rawItemsByID[itemID] else { continue } packInstances.append( PackInstance( id: item.id, itemID: item.id, sourceID: sourceID, logicalPackID: logicalPack.id, origin: isEmbeddedWorldPack(item) ? .embeddedInWorld : .foundInCollection, hostWorldItemID: hostWorldItemID(for: item, in: rawWorlds) ) ) } } let logicalPacksByID = Dictionary(uniqueKeysWithValues: logicalPacks.map { ($0.id.canonicalKey, $0) }) var worldRelationships: [WorldPackRelationship] = [] var logicalWorlds: [LogicalWorld] = [] for world in rawWorlds { var usedPackIDsByID: [String: PackIdentity] = [:] var unresolvedReferences: [ContentPackReference] = [] for reference in world.packReferences { let referenceIdentity = PackIdentity( type: reference.type, uuid: reference.uuid, version: reference.version, fallbackName: reference.name, fallbackLocationHint: world.folderName ) let resolvedID = logicalPacksByID[referenceIdentity.canonicalKey]?.id if let resolvedID { usedPackIDsByID[resolvedID.id] = resolvedID } else { unresolvedReferences.append(reference) } worldRelationships.append( WorldPackRelationship( worldItemID: world.id, logicalPackID: resolvedID, reference: reference ) ) } logicalWorlds.append( LogicalWorld( id: world.id, itemID: world.id, usedPackIDs: usedPackIDsByID.values.sorted { $0.id.localizedStandardCompare($1.id) == .orderedAscending }, unresolvedReferences: unresolvedReferences ) ) } logicalWorlds.sort { guard let lhs = rawItemsByID[$0.itemID], let rhs = rawItemsByID[$1.itemID] else { return $0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending } return WorldScanner.sortItems(lhs, rhs) } if !sizesFinished { scanStatus = indexedItemCount == 0 ? "No Minecraft items found." : "Calculating sizes for \(sizeLoadedCount) of \(indexedItemCount) items..." } else { scanStatus = indexedItemCount == 0 ? "No Minecraft items found." : "Loaded \(indexedDetailCount) items." } return SourceIndexSnapshot( displayItems: dedupedDisplayItems, rawItems: rawItems, logicalPacks: logicalPacks, logicalWorlds: logicalWorlds, packInstances: packInstances.sorted { $0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending }, worldPackRelationships: worldRelationships, indexedItemCount: indexedItemCount, indexedDetailCount: indexedDetailCount, scanStatus: scanStatus, scanProgress: sizesFinished ? nil : progressAfterPreviews(sizeFraction), isScanning: !sizesFinished, lastScanDate: sizesFinished ? now : nil ) } private func progressFraction(completed: Int, total: Int) -> Double { guard total > 0 else { return 1 } return min(max(Double(completed) / Double(total), 0), 1) } private func progressAfterDiscovery(_ metadataFraction: Double) -> Double { 0.1 + (metadataFraction * 0.55) } private func progressAfterMetadata(_ previewFraction: Double) -> Double { 0.65 + (previewFraction * 0.1) } private func progressAfterPreviews(_ sizeFraction: Double) -> Double { 0.75 + (sizeFraction * 0.25) } private func buildDisplayItems( from rawItems: [MinecraftContentItem], logicalPacks: [LogicalPack], rawItemsByID: [URL: MinecraftContentItem] ) -> [MinecraftContentItem] { var normalizedItemIDs = Set() var normalizedItems: [MinecraftContentItem] = [] for item in rawItems where item.contentType == .world { guard normalizedItemIDs.insert(item.id).inserted else { continue } normalizedItems.append(item) } for logicalPack in logicalPacks { guard let item = rawItemsByID[logicalPack.representativeItemID], normalizedItemIDs.insert(item.id).inserted else { continue } normalizedItems.append(item) } for item in rawItems where item.contentType == .skinPack || item.contentType == .worldTemplate { guard normalizedItemIDs.insert(item.id).inserted else { continue } normalizedItems.append(item) } return normalizedItems } private func buildRawDisplayItems(from rawItems: [MinecraftContentItem]) -> [MinecraftContentItem] { rawItems.sorted(by: WorldScanner.sortItems) } private func buildLogicalPacks(rawItemsByID: [URL: MinecraftContentItem]) -> [LogicalPack] { packItemIDsByIdentityID.keys.sorted { let lhs = packRepresentativeItemIDByIdentityID[$0].flatMap { rawItemsByID[$0]?.displayName } ?? "" let rhs = packRepresentativeItemIDByIdentityID[$1].flatMap { rawItemsByID[$0]?.displayName } ?? "" let nameOrder = lhs.localizedStandardCompare(rhs) if nameOrder != .orderedSame { return nameOrder == .orderedAscending } return $0.localizedStandardCompare($1) == .orderedAscending }.compactMap { identityID in guard let identity = packIdentityValueByID[identityID], let representativeItemID = packRepresentativeItemIDByIdentityID[identityID], let representativeItem = rawItemsByID[representativeItemID], let instanceItemIDs = packItemIDsByIdentityID[identityID] else { return nil } let metadata = packMetadataByItemID[representativeItemID] return LogicalPack( id: identity, contentType: identity.type, displayName: representativeItem.displayName, uuid: metadata?.uuid, version: metadata?.version, representativeItemID: representativeItemID, instanceItemIDs: instanceItemIDs.sorted { $0.path.localizedStandardCompare($1.path) == .orderedAscending }, isSuspicious: identity.isSuspicious ) } } private func packMetadata(for item: MinecraftContentItem) -> PackMetadata { let uuid = item.packUUID let version = item.packVersion return PackMetadata( uuid: uuid, version: version, identity: PackIdentity( type: item.contentType, uuid: uuid, version: version, fallbackName: item.displayName, fallbackLocationHint: relativePathHint(for: item) ) ) } private func relativePathHint(for item: MinecraftContentItem) -> String { item.folderURL.path.replacingOccurrences(of: folderURL.path + "/", with: "") } private func shouldPreferPackItem(_ candidate: MinecraftContentItem, over existing: MinecraftContentItem) -> Bool { let candidateEmbedded = isEmbeddedWorldPack(candidate) let existingEmbedded = isEmbeddedWorldPack(existing) if candidateEmbedded != existingEmbedded { return !candidateEmbedded } if candidate.metadataLoaded != existing.metadataLoaded { return candidate.metadataLoaded } if (candidate.iconURL != nil) != (existing.iconURL != nil) { return candidate.iconURL != nil } if candidate.previewLoaded != existing.previewLoaded { return candidate.previewLoaded } if candidate.modifiedDate != existing.modifiedDate { return (candidate.modifiedDate ?? .distantPast) > (existing.modifiedDate ?? .distantPast) } return candidate.folderURL.path.localizedStandardCompare(existing.folderURL.path) == .orderedAscending } private func isEmbeddedWorldPack(_ item: MinecraftContentItem) -> Bool { item.folderURL.pathComponents.contains(MinecraftContentType.world.collectionFolderName) } private func hostWorldItemID(for packItem: MinecraftContentItem, in rawWorlds: [MinecraftContentItem]) -> URL? { rawWorlds.first(where: { world in packItem.folderURL.path.hasPrefix(world.folderURL.path + "/") })?.id } private func refreshTrackedPackIdentity(for item: MinecraftContentItem, previousItem: MinecraftContentItem?) { let previousIdentityID = packIdentityByItemID[item.id] let newMetadata = packMetadata(for: item) let newIdentity = newMetadata.identity let newIdentityID = newIdentity.canonicalKey packMetadataByItemID[item.id] = newMetadata packIdentityByItemID[item.id] = newIdentityID packIdentityValueByID[newIdentityID] = newIdentity if let previousIdentityID, previousIdentityID != newIdentityID { removePackItem(itemID: item.id, fromIdentityID: previousIdentityID) } packItemIDsByIdentityID[newIdentityID, default: []].insert(item.id) refreshRepresentative(forIdentityID: newIdentityID) if let previousItem, previousIdentityID == newIdentityID { guard let representativeItemID = packRepresentativeItemIDByIdentityID[newIdentityID], let currentRepresentative = itemsByID[representativeItemID] else { return } if shouldPreferPackItem(item, over: currentRepresentative) || representativeItemID == item.id { refreshRepresentative(forIdentityID: newIdentityID) } else if representativeItemID == previousItem.id { refreshRepresentative(forIdentityID: newIdentityID) } } } private func removePackItem(itemID: URL, fromIdentityID identityID: String) { guard var itemIDs = packItemIDsByIdentityID[identityID] else { return } itemIDs.remove(itemID) if itemIDs.isEmpty { packItemIDsByIdentityID[identityID] = nil packRepresentativeItemIDByIdentityID[identityID] = nil packIdentityValueByID[identityID] = nil } else { packItemIDsByIdentityID[identityID] = itemIDs if packRepresentativeItemIDByIdentityID[identityID] == itemID { refreshRepresentative(forIdentityID: identityID) } } } private func refreshRepresentative(forIdentityID identityID: String) { guard let itemIDs = packItemIDsByIdentityID[identityID] else { packRepresentativeItemIDByIdentityID[identityID] = nil return } let candidateIDs = itemIDs.sorted { $0.path.localizedStandardCompare($1.path) == .orderedAscending } guard let firstID = candidateIDs.first, let firstItem = itemsByID[firstID] else { packRepresentativeItemIDByIdentityID[identityID] = nil return } let representative = candidateIDs.dropFirst().compactMap { itemsByID[$0] }.reduce(firstItem) { current, candidate in shouldPreferPackItem(candidate, over: current) ? candidate : current } packRepresentativeItemIDByIdentityID[identityID] = representative.id } private func isLogicalPackType(_ contentType: MinecraftContentType) -> Bool { contentType == .behaviorPack || contentType == .resourcePack } }