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) {
|
func rebuildNormalizedIndex(for sourceID: URL) {
|
||||||
updateSource(sourceID) { source in
|
updateSource(sourceID) { source in
|
||||||
let rawItems = source.rawItems.sorted(by: WorldScanner.sortItems)
|
let index = SourceContentIndexer.buildIndex(for: source)
|
||||||
source.rawItems = rawItems
|
source.rawItems = index.rawItems
|
||||||
let rawItemsByID = Dictionary(uniqueKeysWithValues: rawItems.map { ($0.id, $0) })
|
source.logicalPacks = index.logicalPacks
|
||||||
|
source.logicalWorlds = index.logicalWorlds
|
||||||
let rawPacks = rawItems.filter {
|
source.packInstances = index.packInstances
|
||||||
$0.contentType == .behaviorPack || $0.contentType == .resourcePack
|
source.worldPackRelationships = index.worldPackRelationships
|
||||||
}
|
source.displayItems = index.displayItems
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -545,44 +407,6 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
|||||||
sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }
|
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) {
|
func persistSourceIfAvailable(withID sourceID: URL) {
|
||||||
SourcePersistenceCoordinator.persistSourceIfAvailable(
|
SourcePersistenceCoordinator.persistSourceIfAvailable(
|
||||||
withID: sourceID,
|
withID: sourceID,
|
||||||
@ -655,10 +479,6 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isLogicalPackType(_ contentType: MinecraftContentType) -> Bool {
|
|
||||||
contentType == .behaviorPack || contentType == .resourcePack
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func updateAvailability(for sourceID: URL, to newAvailability: SourceAvailability) -> (previous: SourceAvailability, becameAvailable: Bool) {
|
func updateAvailability(for sourceID: URL, to newAvailability: SourceAvailability) -> (previous: SourceAvailability, becameAvailable: Bool) {
|
||||||
let previousAvailability = source(withID: sourceID)?.availability ?? .unknown
|
let previousAvailability = source(withID: sourceID)?.availability ?? .unknown
|
||||||
@ -699,103 +519,4 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
|||||||
|
|
||||||
return diagnostic.localizedCaseInsensitiveContains("showing cached results")
|
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