Extract source content index builder
This commit is contained in:
parent
5baf25f601
commit
47d62a5d51
283
World Manager for Minecraft/Services/SourceContentIndex.swift
Normal file
283
World Manager for Minecraft/Services/SourceContentIndex.swift
Normal file
@ -0,0 +1,283 @@
|
||||
//
|
||||
// SourceContentIndex.swift
|
||||
// World Manager for Minecraft
|
||||
//
|
||||
// Created by OpenAI Codex on 2026-05-29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct SourceContentIndex {
|
||||
let rawItems: [MinecraftContentItem]
|
||||
let logicalPacks: [LogicalPack]
|
||||
let logicalWorlds: [LogicalWorld]
|
||||
let packInstances: [PackInstance]
|
||||
let worldPackRelationships: [WorldPackRelationship]
|
||||
let displayItems: [MinecraftContentItem]
|
||||
}
|
||||
|
||||
enum SourceContentIndexer {
|
||||
static func buildIndex(for source: MinecraftSource) -> SourceContentIndex {
|
||||
let rawItems = source.rawItems.sorted(by: WorldScanner.sortItems)
|
||||
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: source.id,
|
||||
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<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]?.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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let sortedLogicalWorlds = logicalWorlds.sorted {
|
||||
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)
|
||||
}
|
||||
|
||||
let sortedPackInstances = packInstances.sorted {
|
||||
$0.itemID.path.localizedStandardCompare($1.itemID.path) == .orderedAscending
|
||||
}
|
||||
|
||||
return SourceContentIndex(
|
||||
rawItems: rawItems,
|
||||
logicalPacks: logicalPacks,
|
||||
logicalWorlds: sortedLogicalWorlds,
|
||||
packInstances: sortedPackInstances,
|
||||
worldPackRelationships: worldRelationships,
|
||||
displayItems: buildDisplayItems(
|
||||
from: rawItems,
|
||||
logicalPacks: logicalPacks,
|
||||
rawItemsByID: rawItemsByID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private static func buildDisplayItems(
|
||||
from rawItems: [MinecraftContentItem],
|
||||
logicalPacks: [LogicalPack],
|
||||
rawItemsByID: [URL: MinecraftContentItem]
|
||||
) -> [MinecraftContentItem] {
|
||||
var normalizedItemIDs = Set<URL>()
|
||||
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 static 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 static func packOrigin(for item: MinecraftContentItem) -> PackSource {
|
||||
isEmbeddedWorldPack(item) ? .embeddedInWorld : .foundInCollection
|
||||
}
|
||||
|
||||
private static func isEmbeddedWorldPack(_ item: MinecraftContentItem) -> Bool {
|
||||
item.folderURL.pathComponents.contains(MinecraftContentType.world.collectionFolderName)
|
||||
}
|
||||
|
||||
private static func hostWorldItemID(for packItem: MinecraftContentItem, in rawWorlds: [MinecraftContentItem]) -> URL? {
|
||||
rawWorlds.first(where: { world in
|
||||
packItem.folderURL.path.hasPrefix(world.folderURL.path + "/")
|
||||
})?.id
|
||||
}
|
||||
|
||||
private static func packMetadata(for item: MinecraftContentItem, sourceRootURL: URL) -> IndexedPackMetadata {
|
||||
let uuid = item.packUUID
|
||||
let version = item.packVersion
|
||||
|
||||
return IndexedPackMetadata(
|
||||
uuid: uuid,
|
||||
version: version,
|
||||
identity: PackIdentity(
|
||||
type: item.contentType,
|
||||
uuid: uuid,
|
||||
version: version,
|
||||
fallbackName: item.displayName,
|
||||
fallbackLocationHint: relativePathHint(for: item, sourceRootURL: sourceRootURL)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private static func relativePathHint(for item: MinecraftContentItem, sourceRootURL: URL) -> String {
|
||||
item.folderURL.path.replacingOccurrences(of: sourceRootURL.path + "/", with: "")
|
||||
}
|
||||
}
|
||||
|
||||
private struct IndexedPackMetadata {
|
||||
let uuid: String?
|
||||
let version: String?
|
||||
let identity: PackIdentity
|
||||
}
|
||||
@ -257,151 +257,13 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
||||
|
||||
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<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]?.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
|
||||
)
|
||||
let index = SourceContentIndexer.buildIndex(for: source)
|
||||
source.rawItems = index.rawItems
|
||||
source.logicalPacks = index.logicalPacks
|
||||
source.logicalWorlds = index.logicalWorlds
|
||||
source.packInstances = index.packInstances
|
||||
source.worldPackRelationships = index.worldPackRelationships
|
||||
source.displayItems = index.displayItems
|
||||
}
|
||||
}
|
||||
|
||||
@ -545,44 +407,6 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
||||
sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }
|
||||
}
|
||||
|
||||
private func buildDisplayItems(
|
||||
from rawItems: [MinecraftContentItem],
|
||||
logicalPacks: [LogicalPack],
|
||||
rawItemsByID: [URL: MinecraftContentItem]
|
||||
) -> [MinecraftContentItem] {
|
||||
var normalizedItemIDs = Set<URL>()
|
||||
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
|
||||
}
|
||||
|
||||
func persistSourceIfAvailable(withID sourceID: URL) {
|
||||
SourcePersistenceCoordinator.persistSourceIfAvailable(
|
||||
withID: sourceID,
|
||||
@ -655,10 +479,6 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
||||
)
|
||||
}
|
||||
|
||||
private func isLogicalPackType(_ contentType: MinecraftContentType) -> Bool {
|
||||
contentType == .behaviorPack || contentType == .resourcePack
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func updateAvailability(for sourceID: URL, to newAvailability: SourceAvailability) -> (previous: SourceAvailability, becameAvailable: Bool) {
|
||||
let previousAvailability = source(withID: sourceID)?.availability ?? .unknown
|
||||
@ -699,103 +519,4 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
||||
|
||||
return diagnostic.localizedCaseInsensitiveContains("showing cached results")
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user